mirror of https://github.com/harness/drone.git
[CODE-1287] Improve PR Merge for canceled Request (#963)
parent
13b89482f2
commit
a892dda829
|
@ -15,22 +15,69 @@
|
||||||
package pullreq
|
package pullreq
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/contextutil"
|
||||||
"github.com/harness/gitness/lock"
|
"github.com/harness/gitness/lock"
|
||||||
|
"github.com/harness/gitness/logging"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) newMutexForPR(repoUID string, pr int64, options ...lock.Option) (lock.Mutex, error) {
|
func (c *Controller) lockPR(
|
||||||
|
ctx context.Context,
|
||||||
|
repoUID string,
|
||||||
|
prNum int64,
|
||||||
|
expiry time.Duration,
|
||||||
|
) (func(), error) {
|
||||||
key := repoUID + "/pulls"
|
key := repoUID + "/pulls"
|
||||||
if pr != 0 {
|
if prNum != 0 {
|
||||||
key += "/" + strconv.FormatInt(pr, 10)
|
key += "/" + strconv.FormatInt(prNum, 10)
|
||||||
}
|
}
|
||||||
return c.mtxManager.NewMutex(
|
|
||||||
|
// annotate logs for easier debugging of lock related merge issues
|
||||||
|
// TODO: refactor once common logging annotations are added
|
||||||
|
ctx = logging.NewContext(ctx, func(c zerolog.Context) zerolog.Context {
|
||||||
|
return c.
|
||||||
|
Str("pullreq_lock", key).
|
||||||
|
Str("repo_uid", repoUID)
|
||||||
|
})
|
||||||
|
|
||||||
|
mutex, err := c.mtxManager.NewMutex(
|
||||||
key,
|
key,
|
||||||
append(options,
|
lock.WithNamespace("repo"),
|
||||||
lock.WithNamespace("repo"),
|
lock.WithExpiry(expiry),
|
||||||
lock.WithExpiry(16*time.Second),
|
lock.WithTimeoutFactor(4/expiry.Seconds()), // 4s
|
||||||
lock.WithTimeoutFactor(0.25), // 4s
|
)
|
||||||
)...)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new mutex for pr %d in repo %q: %w", prNum, repoUID, err)
|
||||||
|
}
|
||||||
|
err = mutex.Lock(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to lock mutex for pr %d in repo %q: %w", prNum, repoUID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Ctx(ctx).Debug().Msgf("successfully locked PR (expiry: %s)", expiry)
|
||||||
|
|
||||||
|
unlockFn := func() {
|
||||||
|
// always unlock independent of whether source context got canceled or not
|
||||||
|
ctx, cancel := context.WithTimeout(
|
||||||
|
contextutil.WithNewValues(context.Background(), ctx),
|
||||||
|
30*time.Second,
|
||||||
|
)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := mutex.Unlock(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Ctx(ctx).Warn().Err(err).Msg("failed to unlock PR")
|
||||||
|
} else {
|
||||||
|
log.Ctx(ctx).Debug().Msg("successfully unlocked PR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockFn, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||||
"github.com/harness/gitness/app/services/codeowners"
|
"github.com/harness/gitness/app/services/codeowners"
|
||||||
"github.com/harness/gitness/app/services/protection"
|
"github.com/harness/gitness/app/services/protection"
|
||||||
|
"github.com/harness/gitness/contextutil"
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
"github.com/harness/gitness/git"
|
"github.com/harness/gitness/git"
|
||||||
gitenum "github.com/harness/gitness/git/enum"
|
gitenum "github.com/harness/gitness/git/enum"
|
||||||
|
@ -96,21 +97,23 @@ func (c *Controller) Merge(
|
||||||
return nil, nil, fmt.Errorf("failed to acquire access to target repo: %w", err)
|
return nil, nil, fmt.Errorf("failed to acquire access to target repo: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if two requests for merging comes at the same time then mutex will lock
|
// the max time we give a merge to succeed
|
||||||
|
const timeout = 3 * time.Minute
|
||||||
|
|
||||||
|
// if two requests for merging comes at the same time then unlock will lock
|
||||||
// first one and second one will wait, when first one is done then second one
|
// first one and second one will wait, when first one is done then second one
|
||||||
// continue with latest data from db with state merged and return error that
|
// continue with latest data from db with state merged and return error that
|
||||||
// pr is already merged.
|
// pr is already merged.
|
||||||
mutex, err := c.newMutexForPR(targetRepo.GitUID, 0) // 0 means locks all PRs for this repo
|
unlock, err := c.lockPR(
|
||||||
|
ctx,
|
||||||
|
targetRepo.GitUID,
|
||||||
|
0, // 0 means locks all PRs for this repo
|
||||||
|
timeout+30*time.Second, // add 30s to the lock to give enough time for pre + post merge
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
err = mutex.Lock(ctx)
|
defer unlock()
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = mutex.Unlock(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
pr, err := c.pullreqStore.FindByNumber(ctx, targetRepo.ID, pullreqNum)
|
pr, err := c.pullreqStore.FindByNumber(ctx, targetRepo.ID, pullreqNum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -197,6 +200,16 @@ func (c *Controller) Merge(
|
||||||
return nil, nil, fmt.Errorf("failed to verify protection rules: %w", err)
|
return nil, nil, fmt.Errorf("failed to verify protection rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we want to complete the merge independent of request cancel - start with new, time restricted context.
|
||||||
|
// TODO: This is a small change to reduce likelihood of dirty state.
|
||||||
|
// We still require a proper solution to handle an application crash or very slow execution times
|
||||||
|
// (which could cause an unlocking pre operation completion).
|
||||||
|
ctx, cancel := context.WithTimeout(
|
||||||
|
contextutil.WithNewValues(context.Background(), ctx),
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
//nolint:nestif
|
//nolint:nestif
|
||||||
if in.DryRun {
|
if in.DryRun {
|
||||||
// As the merge API is always executed under a global lock, we use the opportunity of dry-running the merge
|
// As the merge API is always executed under a global lock, we use the opportunity of dry-running the merge
|
||||||
|
@ -273,6 +286,8 @@ func (c *Controller) Merge(
|
||||||
mergeTitle = fmt.Sprintf("Merge branch '%s' of %s (#%d)", pr.SourceBranch, sourceRepo.Path, pr.Number)
|
mergeTitle = fmt.Sprintf("Merge branch '%s' of %s (#%d)", pr.SourceBranch, sourceRepo.Path, pr.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Ctx(ctx).Debug().Msgf("all pre-check passed, merge PR")
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
mergeOutput, err := c.git.Merge(ctx, &git.MergeParams{
|
mergeOutput, err := c.git.Merge(ctx, &git.MergeParams{
|
||||||
WriteParams: targetWriteParams,
|
WriteParams: targetWriteParams,
|
||||||
|
@ -324,6 +339,8 @@ func (c *Controller) Merge(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Ctx(ctx).Debug().Msgf("successfully merged PR")
|
||||||
|
|
||||||
var activitySeqMerge, activitySeqBranchDeleted int64
|
var activitySeqMerge, activitySeqBranchDeleted int64
|
||||||
pr, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
pr, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
||||||
pr.State = enum.PullReqStateMerged
|
pr.State = enum.PullReqStateMerged
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
|
|
||||||
"github.com/harness/gitness/app/api/request"
|
"github.com/harness/gitness/app/api/request"
|
||||||
"github.com/harness/gitness/git"
|
"github.com/harness/gitness/git"
|
||||||
|
"github.com/harness/gitness/logging"
|
||||||
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,10 +51,7 @@ func HLogRequestIDHandler() func(http.Handler) http.Handler {
|
||||||
ctx = git.WithRequestID(ctx, reqID)
|
ctx = git.WithRequestID(ctx, reqID)
|
||||||
|
|
||||||
// update logging context with request ID
|
// update logging context with request ID
|
||||||
log := zerolog.Ctx(ctx)
|
logging.UpdateContext(ctx, logging.WithRequestID(reqID))
|
||||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
|
||||||
return c.Str("request_id", reqID)
|
|
||||||
})
|
|
||||||
|
|
||||||
// write request ID to response headers
|
// write request ID to response headers
|
||||||
w.Header().Set(requestIDHeader, reqID)
|
w.Header().Set(requestIDHeader, reqID)
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2023 Harness, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package contextutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithNewValues creates a new context derived from originalCtx with values from valuesCtx.
|
||||||
|
func WithNewValues(originalCtx context.Context, valuesCtx context.Context) context.Context {
|
||||||
|
return &combinedContext{
|
||||||
|
Context: originalCtx,
|
||||||
|
valuesCtx: valuesCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type combinedContext struct {
|
||||||
|
context.Context
|
||||||
|
valuesCtx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *combinedContext) Value(key any) any {
|
||||||
|
return c.valuesCtx.Value(key)
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2023 Harness, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option allows to annotate logs with metadata.
|
||||||
|
type Option func(c zerolog.Context) zerolog.Context
|
||||||
|
|
||||||
|
// UpdateContext updates the existing logging context in the context.
|
||||||
|
// IMPORTANT: No new context is created, all future logs with provided context will be impacted.
|
||||||
|
func UpdateContext(ctx context.Context, opts ...Option) {
|
||||||
|
// updates existing logging context in provided context.Context
|
||||||
|
zerolog.Ctx(ctx).UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||||
|
for _, opt := range opts {
|
||||||
|
c = opt(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext derives a new LoggingContext from the existing LoggingContext, adds the provided annotations,
|
||||||
|
// and then returns a clone of the provided context.Context with the new LoggingContext.
|
||||||
|
// IMPORTANT: The provided context is not modified, logging annotations are only part of the new context.
|
||||||
|
func NewContext(ctx context.Context, opts ...Option) context.Context {
|
||||||
|
// create child of current context
|
||||||
|
childloggingContext := log.Ctx(ctx).With()
|
||||||
|
|
||||||
|
// update child context
|
||||||
|
for _, opt := range opts {
|
||||||
|
childloggingContext = opt(childloggingContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return copied context with new logging context
|
||||||
|
return childloggingContext.Logger().WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequestID can be used to annotate logs with the request id.
|
||||||
|
func WithRequestID(reqID string) Option {
|
||||||
|
return func(c zerolog.Context) zerolog.Context {
|
||||||
|
return c.Str("request_id", reqID)
|
||||||
|
}
|
||||||
|
}
|
|
@ -136,7 +136,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||||
} else if (
|
} else if (
|
||||||
getErrorMessage(err) === codeOwnersNotFoundMessage ||
|
getErrorMessage(err) === codeOwnersNotFoundMessage ||
|
||||||
getErrorMessage(err) === codeOwnersNotFoundMessage2 ||
|
getErrorMessage(err) === codeOwnersNotFoundMessage2 ||
|
||||||
getErrorMessage(err) === codeOwnersNotFoundMessage3
|
getErrorMessage(err) === codeOwnersNotFoundMessage3 ||
|
||||||
|
err.status === 423 // resource locked (merge / dry-run already ongoing)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue