relay-flow 0.3.7-alpha → 0.3.9-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/cmd/relay-flow/observability_test.go +28 -0
- package/cmd/relay-flow/render.go +20 -4
- package/cmd/relay-flow/scenario_test.go +15 -13
- package/cmd/relay-flow/serve.go +116 -103
- package/internal/execution/goworkflows/activities.go +17 -0
- package/internal/execution/goworkflows/cancellation_test.go +50 -0
- package/internal/execution/goworkflows/engine.go +278 -9
- package/internal/execution/goworkflows/fakes_test.go +10 -1
- package/internal/execution/goworkflows/interpreter.go +17 -1
- package/internal/execution/goworkflows/projection.go +8 -0
- package/internal/execution/goworkflows/recovery_test.go +482 -0
- package/internal/execution/projection/detail_test.go +38 -0
- package/internal/execution/projection/projection.go +80 -15
- package/internal/execution/temporal/activities.go +6 -0
- package/internal/execution/temporal/recovery.go +4 -0
- package/internal/harness/opencode/opencode_test.go +1 -1
- package/internal/harness/opencode/repo_setup.go +1 -1
- package/internal/recover/recover.go +4 -0
- package/internal/repo/binding_test.go +92 -0
- package/internal/repo/poller.go +3 -0
- package/internal/repo/repo.go +82 -6
- package/internal/run/manager.go +41 -0
- package/internal/run/run_manager_test.go +56 -0
- package/internal/server/observability.go +1 -1
- package/internal/task/beads/beads.go +42 -4
- package/internal/task/beads/beads_test.go +15 -0
- package/internal/task/factory.go +41 -2
- package/internal/task/jira/jira.go +52 -28
- package/internal/workflow/service.go +53 -0
- package/internal/workflow/store.go +413 -19
- package/internal/workflow/store_test.go +235 -0
- package/internal/workflow/workflow.go +71 -0
- package/package.json +1 -1
|
@@ -27,8 +27,10 @@ import (
|
|
|
27
27
|
"github.com/rajpopat27/relay-flow/internal/harness"
|
|
28
28
|
"github.com/rajpopat27/relay-flow/internal/identity"
|
|
29
29
|
"github.com/rajpopat27/relay-flow/internal/repo"
|
|
30
|
+
"github.com/rajpopat27/relay-flow/internal/retry"
|
|
30
31
|
"github.com/rajpopat27/relay-flow/internal/run"
|
|
31
32
|
"github.com/rajpopat27/relay-flow/internal/runner"
|
|
33
|
+
"github.com/rajpopat27/relay-flow/internal/task"
|
|
32
34
|
"github.com/rajpopat27/relay-flow/internal/workflow"
|
|
33
35
|
)
|
|
34
36
|
|
|
@@ -61,6 +63,7 @@ type Engine struct {
|
|
|
61
63
|
runtime run.RuntimePolicy
|
|
62
64
|
|
|
63
65
|
mu sync.RWMutex
|
|
66
|
+
cancelMu sync.Mutex // serializes cancellation history check + request
|
|
64
67
|
snapshots map[run.ID]*workflow.Workflow // in-memory cache; history is authoritative
|
|
65
68
|
|
|
66
69
|
workerCtx context.Context
|
|
@@ -177,6 +180,11 @@ func (e *Engine) Start(ctx context.Context) error {
|
|
|
177
180
|
if err := e.actWorker.Start(e.workerCtx); err != nil {
|
|
178
181
|
return fmt.Errorf("start activity worker: %w", err)
|
|
179
182
|
}
|
|
183
|
+
// A cancellation request can outlive the engine instance that accepted it.
|
|
184
|
+
// Reconcile those projections before normal pollers start; a missing
|
|
185
|
+
// workflow instance is a confirmed terminal execution boundary, while
|
|
186
|
+
// other lookup failures remain retryable and are logged below.
|
|
187
|
+
e.reconcileCancelingRuns(ctx)
|
|
180
188
|
// Startup retention sweep (pre-poller window): remove old terminal
|
|
181
189
|
// projection rows and their engine histories; nonterminal runs stay.
|
|
182
190
|
cutoff := time.Now().Add(-e.retention)
|
|
@@ -202,6 +210,7 @@ func (e *Engine) registerActivities() error {
|
|
|
202
210
|
a.EnsureEnvironment,
|
|
203
211
|
a.SetEnvironmentStatus,
|
|
204
212
|
a.LoadNodeRuntime,
|
|
213
|
+
a.LoadCancellationReason,
|
|
205
214
|
a.EnsureNodeRuntime,
|
|
206
215
|
a.CloseTerminals,
|
|
207
216
|
a.CleanupRun,
|
|
@@ -462,26 +471,286 @@ func (e *Engine) workflowOf(ctx context.Context, id run.ID) (*workflow.Workflow,
|
|
|
462
471
|
return nil, fmt.Errorf("no workflow snapshot in history for run %s", id)
|
|
463
472
|
}
|
|
464
473
|
|
|
465
|
-
// CancelRun
|
|
466
|
-
//
|
|
474
|
+
// CancelRun requests cancellation of the durable workflow. The projection
|
|
475
|
+
// transition is compare-and-set: a concurrent completion wins if it reaches a
|
|
476
|
+
// terminal state first, while a persisted canceling state is the durable
|
|
477
|
+
// request that startup reconciliation retries.
|
|
467
478
|
func (e *Engine) CancelRun(ctx context.Context, id run.ID, reason string) error {
|
|
468
|
-
|
|
479
|
+
r, err := e.runs.beginCancellation(ctx, id, reason)
|
|
480
|
+
if err != nil {
|
|
469
481
|
return fmt.Errorf("resolve run %s: %w", id, err)
|
|
470
482
|
}
|
|
471
|
-
if
|
|
483
|
+
if r.State == run.StateCompleted || r.State == run.StateCanceled {
|
|
484
|
+
return nil
|
|
485
|
+
}
|
|
486
|
+
if r.State != run.StateCanceling {
|
|
487
|
+
return fmt.Errorf("cancel %s: state changed to %s", id, r.State)
|
|
488
|
+
}
|
|
489
|
+
// The reason persisted by the first successful CAS is authoritative for
|
|
490
|
+
// every later cancellation request.
|
|
491
|
+
return e.reconcileCancellation(ctx, r, r.LastError)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// isMissingWorkflowInstance identifies only confirmed absence of the active
|
|
495
|
+
// go-workflows execution. Database and context failures remain retryable.
|
|
496
|
+
func isMissingWorkflowInstance(err error) bool {
|
|
497
|
+
return errors.Is(err, sql.ErrNoRows) || errors.Is(err, backend.ErrInstanceNotFound)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// detachedContext preserves a caller deadline while detaching cancellation
|
|
501
|
+
// from the caller. Startup cleanup must not be able to outlive its bounded
|
|
502
|
+
// reconciliation window.
|
|
503
|
+
func detachedContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
504
|
+
base := context.WithoutCancel(ctx)
|
|
505
|
+
if deadline, ok := ctx.Deadline(); ok {
|
|
506
|
+
return context.WithDeadline(base, deadline)
|
|
507
|
+
}
|
|
508
|
+
return base, func() {}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// lookupInstance returns the active execution when present. If only a finished
|
|
512
|
+
// row remains, finished is true; a missing row is the only absence outcome.
|
|
513
|
+
func (e *Engine) lookupInstance(ctx context.Context, id run.ID) (*goworkflow.Instance, bool, error) {
|
|
514
|
+
var execID string
|
|
515
|
+
var state int
|
|
516
|
+
err := e.db.QueryRowContext(ctx, `
|
|
517
|
+
SELECT execution_id, state FROM instances WHERE id = ?
|
|
518
|
+
ORDER BY CASE WHEN state = 0 THEN 0 ELSE 1 END, rowid DESC LIMIT 1`, string(id)).Scan(&execID, &state)
|
|
519
|
+
if err != nil {
|
|
520
|
+
return nil, false, fmt.Errorf("workflow instance %s not found: %w", id, err)
|
|
521
|
+
}
|
|
522
|
+
return &goworkflow.Instance{InstanceID: string(id), ExecutionID: execID}, state != 0, nil
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// reconcileCancellation retries the durable cancellation request while an
|
|
526
|
+
// execution is active and reconciles finished/missing executions separately.
|
|
527
|
+
func (e *Engine) reconcileCancellation(ctx context.Context, r run.Run, reason string) error {
|
|
528
|
+
inst, finished, err := e.lookupInstance(ctx, r.ID)
|
|
529
|
+
if err != nil {
|
|
530
|
+
if isMissingWorkflowInstance(err) {
|
|
531
|
+
return e.finalizeMissingCancellation(ctx, r, reason)
|
|
532
|
+
}
|
|
533
|
+
return fmt.Errorf("resolve workflow instance: %w", err)
|
|
534
|
+
}
|
|
535
|
+
if finished {
|
|
536
|
+
return e.reconcileFinishedCancellation(ctx, r, inst, reason)
|
|
537
|
+
}
|
|
538
|
+
if e.client == nil {
|
|
539
|
+
return errors.New("workflow engine is not started")
|
|
540
|
+
}
|
|
541
|
+
if err := e.requestCancellation(ctx, inst); err != nil {
|
|
542
|
+
if isMissingWorkflowInstance(err) {
|
|
543
|
+
return e.finalizeMissingCancellation(ctx, r, reason)
|
|
544
|
+
}
|
|
472
545
|
return err
|
|
473
546
|
}
|
|
474
|
-
|
|
547
|
+
return nil
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// requestCancellation checks durable history and appends at most one
|
|
551
|
+
// cancellation event for an execution. The mutex closes the check/request
|
|
552
|
+
// race between concurrent operator calls in this process; the relay lock
|
|
553
|
+
// prevents another relay-flow server from owning the same database.
|
|
554
|
+
func (e *Engine) requestCancellation(ctx context.Context, inst *goworkflow.Instance) error {
|
|
555
|
+
e.cancelMu.Lock()
|
|
556
|
+
defer e.cancelMu.Unlock()
|
|
557
|
+
var requested int
|
|
558
|
+
if err := e.db.QueryRowContext(ctx, `
|
|
559
|
+
SELECT EXISTS(
|
|
560
|
+
SELECT 1 FROM history
|
|
561
|
+
WHERE instance_id = ? AND execution_id = ? AND event_type = ?
|
|
562
|
+
UNION ALL
|
|
563
|
+
SELECT 1 FROM pending_events
|
|
564
|
+
WHERE instance_id = ? AND execution_id = ? AND event_type = ?
|
|
565
|
+
)`,
|
|
566
|
+
inst.InstanceID, inst.ExecutionID, history.EventType_WorkflowExecutionCanceled,
|
|
567
|
+
inst.InstanceID, inst.ExecutionID, history.EventType_WorkflowExecutionCanceled,
|
|
568
|
+
).Scan(&requested); err != nil {
|
|
569
|
+
return fmt.Errorf("inspect cancellation history: %w", err)
|
|
570
|
+
}
|
|
571
|
+
if requested != 0 {
|
|
572
|
+
return nil
|
|
573
|
+
}
|
|
574
|
+
return e.client.CancelWorkflowInstance(ctx, inst)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// finalizeMissingCancellation performs the same roll-forward cleanup as the
|
|
578
|
+
// workflow's disconnected cancellation path when the workflow instance has
|
|
579
|
+
// genuinely disappeared. It intentionally does not recreate an execution.
|
|
580
|
+
func (e *Engine) finalizeMissingCancellation(ctx context.Context, r run.Run, reason string) error {
|
|
581
|
+
cleanupCtx, cleanupCancel := detachedContext(ctx)
|
|
582
|
+
defer cleanupCancel()
|
|
583
|
+
current, err := e.runs.get(cleanupCtx, r.ID)
|
|
584
|
+
if err != nil {
|
|
585
|
+
return fmt.Errorf("reconcile canceled run %s projection: %w", r.ID, err)
|
|
586
|
+
}
|
|
587
|
+
if current.State == run.StateCompleted || current.State == run.StateCanceled {
|
|
588
|
+
return nil
|
|
589
|
+
}
|
|
590
|
+
if current.State != run.StateCanceling {
|
|
591
|
+
return fmt.Errorf("reconcile canceled run %s: state changed to %s", r.ID, current.State)
|
|
592
|
+
}
|
|
593
|
+
if inst, finished, lookupErr := e.lookupInstance(cleanupCtx, r.ID); lookupErr == nil {
|
|
594
|
+
if finished {
|
|
595
|
+
return e.reconcileFinishedCancellation(cleanupCtx, current, inst, reason)
|
|
596
|
+
}
|
|
597
|
+
return fmt.Errorf("reconcile canceled run %s: workflow instance reappeared", r.ID)
|
|
598
|
+
} else if !isMissingWorkflowInstance(lookupErr) {
|
|
599
|
+
return fmt.Errorf("reconcile canceled run %s instance lookup: %w", r.ID, lookupErr)
|
|
600
|
+
}
|
|
601
|
+
return e.finalizeCancellationEffects(cleanupCtx, current, reason)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// finalizeCancellationEffects is shared by missing and already-canceled
|
|
605
|
+
// engine executions. The final projection transition is conditional so a
|
|
606
|
+
// concurrent terminal projection cannot be overwritten.
|
|
607
|
+
func (e *Engine) finalizeCancellationEffects(ctx context.Context, r run.Run, reason string) error {
|
|
608
|
+
if r.State == run.StateCompleted || r.State == run.StateCanceled {
|
|
609
|
+
return nil
|
|
610
|
+
}
|
|
611
|
+
if r.State != run.StateCanceling {
|
|
612
|
+
return fmt.Errorf("finalize canceled run %s: state changed to %s", r.ID, r.State)
|
|
613
|
+
}
|
|
614
|
+
repoInfo, ok := e.activities.Repos.Get(r.Repo)
|
|
615
|
+
if !ok {
|
|
616
|
+
return fmt.Errorf("reconcile canceled run %s: repo %q is no longer registered", r.ID, r.Repo)
|
|
617
|
+
}
|
|
618
|
+
work := run.Work{
|
|
619
|
+
RunID: r.ID,
|
|
620
|
+
LogicalID: r.LogicalID,
|
|
621
|
+
AttemptID: r.AttemptID,
|
|
622
|
+
Repo: r.Repo,
|
|
623
|
+
Workflow: r.Workflow,
|
|
624
|
+
Parent: r.Ticket,
|
|
625
|
+
Runtime: e.runtime,
|
|
626
|
+
}
|
|
627
|
+
if reason == "" {
|
|
628
|
+
reason = "operator requested cancellation"
|
|
629
|
+
}
|
|
630
|
+
if err := e.activities.FinalizeNodeRuntimes(ctx, work, repoInfo.Path, e.runtime); err != nil {
|
|
631
|
+
return fmt.Errorf("finalize canceled run %s terminals: %w", r.ID, err)
|
|
632
|
+
}
|
|
633
|
+
markerID := r.LogicalID
|
|
634
|
+
if markerID == "" {
|
|
635
|
+
markerID = r.ID
|
|
636
|
+
}
|
|
637
|
+
if err := e.activities.Comment(ctx, r.Repo, run.CommentWork{
|
|
638
|
+
RunID: r.ID,
|
|
639
|
+
Item: task.Target{Parent: r.Ticket},
|
|
640
|
+
Body: "Run canceled: " + reason,
|
|
641
|
+
Marker: run.CancellationMarker(markerID),
|
|
642
|
+
}); err != nil {
|
|
643
|
+
return fmt.Errorf("finalize canceled run %s comment: %w", r.ID, err)
|
|
644
|
+
}
|
|
645
|
+
finished := time.Now().UTC()
|
|
646
|
+
updated, err := e.runs.updateStateIf(ctx, r.ID, run.StateCanceling, run.StateCanceled, "", &finished)
|
|
475
647
|
if err != nil {
|
|
476
|
-
return fmt.Errorf("
|
|
648
|
+
return fmt.Errorf("finalize canceled run %s projection: %w", r.ID, err)
|
|
477
649
|
}
|
|
478
|
-
if
|
|
479
|
-
|
|
650
|
+
if !updated {
|
|
651
|
+
latest, getErr := e.runs.get(ctx, r.ID)
|
|
652
|
+
if getErr == nil && (latest.State == run.StateCanceled || latest.State == run.StateCompleted) {
|
|
653
|
+
return nil
|
|
654
|
+
}
|
|
655
|
+
if getErr != nil {
|
|
656
|
+
return fmt.Errorf("finalize canceled run %s projection after race: %w", r.ID, getErr)
|
|
657
|
+
}
|
|
658
|
+
return fmt.Errorf("finalize canceled run %s: state changed to %s", r.ID, latest.State)
|
|
480
659
|
}
|
|
660
|
+
slog.Info("run canceled", "ticket", r.Ticket.Key, "runID", string(r.ID),
|
|
661
|
+
"repo", r.Repo, "workflow", r.Workflow, "state", run.StateCanceled)
|
|
481
662
|
return nil
|
|
482
663
|
}
|
|
483
664
|
|
|
484
|
-
//
|
|
665
|
+
// reconcileFinishedCancellation reads the terminal engine event before
|
|
666
|
+
// deciding whether a canceling projection should become completed or canceled.
|
|
667
|
+
func (e *Engine) reconcileFinishedCancellation(ctx context.Context, r run.Run, inst *goworkflow.Instance, reason string) error {
|
|
668
|
+
if e.backend == nil {
|
|
669
|
+
return errors.New("workflow backend is not started")
|
|
670
|
+
}
|
|
671
|
+
events, err := e.backend.GetWorkflowInstanceHistory(ctx, inst, nil)
|
|
672
|
+
if err != nil {
|
|
673
|
+
return fmt.Errorf("read finished workflow history: %w", err)
|
|
674
|
+
}
|
|
675
|
+
var finishedEvent *history.Event
|
|
676
|
+
canceledBeforeFinish := false
|
|
677
|
+
for _, event := range events {
|
|
678
|
+
switch event.Type {
|
|
679
|
+
case history.EventType_WorkflowExecutionCanceled:
|
|
680
|
+
// TicketWorkflow records this cancellation request first; after
|
|
681
|
+
// cancelCleanup returns, go-workflows may append a normal
|
|
682
|
+
// WorkflowExecutionFinished event. The earlier cancellation event
|
|
683
|
+
// remains authoritative for relay-flow semantics.
|
|
684
|
+
if finishedEvent == nil {
|
|
685
|
+
canceledBeforeFinish = true
|
|
686
|
+
}
|
|
687
|
+
case history.EventType_WorkflowExecutionFinished:
|
|
688
|
+
if finishedEvent == nil {
|
|
689
|
+
finishedEvent = event
|
|
690
|
+
}
|
|
691
|
+
case history.EventType_WorkflowExecutionTerminated:
|
|
692
|
+
return fmt.Errorf("workflow %s terminated without a cancellation or completion result", r.ID)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if canceledBeforeFinish {
|
|
696
|
+
return e.finalizeCancellationEffects(ctx, r, reason)
|
|
697
|
+
}
|
|
698
|
+
if finishedEvent == nil {
|
|
699
|
+
return fmt.Errorf("finished workflow %s has no terminal history event", r.ID)
|
|
700
|
+
}
|
|
701
|
+
{
|
|
702
|
+
finished := finishedEvent.Timestamp
|
|
703
|
+
updated, err := e.runs.updateStateIf(ctx, r.ID, run.StateCanceling, run.StateCompleted, "", &finished)
|
|
704
|
+
if err != nil {
|
|
705
|
+
return fmt.Errorf("reconcile completed run %s projection: %w", r.ID, err)
|
|
706
|
+
}
|
|
707
|
+
if !updated {
|
|
708
|
+
latest, getErr := e.runs.get(ctx, r.ID)
|
|
709
|
+
if getErr == nil && (latest.State == run.StateCompleted || latest.State == run.StateCanceled) {
|
|
710
|
+
return nil
|
|
711
|
+
}
|
|
712
|
+
if getErr != nil {
|
|
713
|
+
return fmt.Errorf("reconcile completed run %s projection after race: %w", r.ID, getErr)
|
|
714
|
+
}
|
|
715
|
+
return fmt.Errorf("reconcile completed run %s: state changed to %s", r.ID, latest.State)
|
|
716
|
+
}
|
|
717
|
+
return nil
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// reconcileCancelingRuns retries the durable cancellation request for every
|
|
722
|
+
// stale canceling projection. An active instance is not skipped: its cancel
|
|
723
|
+
// event is re-submitted until the workflow accepts it.
|
|
724
|
+
func (e *Engine) reconcileCancelingRuns(ctx context.Context) {
|
|
725
|
+
active := true
|
|
726
|
+
runs, err := e.runs.list(ctx, run.Filter{Active: &active})
|
|
727
|
+
if err != nil {
|
|
728
|
+
slog.Warn("reconcile canceling runs unavailable", "error", err)
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
for _, r := range runs {
|
|
732
|
+
if r.State != run.StateCanceling {
|
|
733
|
+
continue
|
|
734
|
+
}
|
|
735
|
+
if err := e.retryStartupCancellation(ctx, r); err != nil {
|
|
736
|
+
slog.Warn("reconcile canceling run failed", "runID", r.ID, "error", err)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// retryStartupCancellation gives a durable canceling projection a bounded
|
|
742
|
+
// startup retry window. Normal polling deliberately does not recreate or
|
|
743
|
+
// advance canceling runs, so an active execution must receive its cancel
|
|
744
|
+
// event here or remain visibly retryable for the next restart.
|
|
745
|
+
func (e *Engine) retryStartupCancellation(ctx context.Context, r run.Run) error {
|
|
746
|
+
retryCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
747
|
+
defer cancel()
|
|
748
|
+
return retry.Do(retryCtx, retry.DefaultBackoffPolicy, func() error {
|
|
749
|
+
return e.reconcileCancellation(retryCtx, r, r.LastError)
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// instance resolves the current active execution for the durable run ID.
|
|
485
754
|
func (e *Engine) instance(ctx context.Context, id run.ID) (*goworkflow.Instance, error) {
|
|
486
755
|
var execID string
|
|
487
756
|
err := e.db.QueryRowContext(ctx,
|
|
@@ -226,7 +226,16 @@ func (s *fakeTaskSystem) Comment(_ context.Context, target task.Target, body, ma
|
|
|
226
226
|
if target.Mailbox != nil {
|
|
227
227
|
key = target.Mailbox.Key
|
|
228
228
|
}
|
|
229
|
-
|
|
229
|
+
s.mu.Lock()
|
|
230
|
+
for _, comment := range s.comments {
|
|
231
|
+
if comment.Key == key && comment.Marker == marker {
|
|
232
|
+
s.mu.Unlock()
|
|
233
|
+
return nil
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
fail := s.failComments
|
|
237
|
+
s.mu.Unlock()
|
|
238
|
+
if fail {
|
|
230
239
|
s.log.add("commentFail:" + key)
|
|
231
240
|
return errTransient
|
|
232
241
|
}
|
|
@@ -75,7 +75,23 @@ func (a *Activities) TicketWorkflow(ctx goworkflow.Context, start run.Start) err
|
|
|
75
75
|
WorkflowTaskConfig: start.Workflow.TaskConfig,
|
|
76
76
|
Runtime: start.Runtime,
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// Cancellation events do not carry relay-flow's operator reason. Read
|
|
79
|
+
// the reason persisted by CancelRun through the durable disconnected
|
|
80
|
+
// retry path so cleanup cannot write a permanently wrong comment when
|
|
81
|
+
// the projection is temporarily unavailable.
|
|
82
|
+
dctx := goworkflow.NewDisconnectedContext(ctx)
|
|
83
|
+
reason, reasonErr := retryLoop(dctx, start.ID, a, work, "",
|
|
84
|
+
func(ctx2 goworkflow.Context) goworkflow.Future[string] {
|
|
85
|
+
return goworkflow.ExecuteActivity[string](ctx2, noNativeRetries,
|
|
86
|
+
a.LoadCancellationReason, start.ID)
|
|
87
|
+
})
|
|
88
|
+
if reasonErr != nil {
|
|
89
|
+
return reasonErr
|
|
90
|
+
}
|
|
91
|
+
if reason == "" {
|
|
92
|
+
reason = "canceled"
|
|
93
|
+
}
|
|
94
|
+
return a.cancelCleanup(ctx, work, start.RepoPath, reason)
|
|
79
95
|
}
|
|
80
96
|
return err
|
|
81
97
|
}
|
|
@@ -46,6 +46,14 @@ func (p *RunProjection) updateState(ctx context.Context, id run.ID, state run.St
|
|
|
46
46
|
return p.shared().UpdateState(ctx, id, state, lastErr, finished)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
func (p *RunProjection) beginCancellation(ctx context.Context, id run.ID, reason string) (run.Run, error) {
|
|
50
|
+
return p.shared().BeginCancellation(ctx, id, reason)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (p *RunProjection) updateStateIf(ctx context.Context, id run.ID, expected, state run.State, lastErr string, finished *time.Time) (bool, error) {
|
|
54
|
+
return p.shared().UpdateStateIf(ctx, id, expected, state, lastErr, finished)
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
func (p *RunProjection) updateRetry(ctx context.Context, id run.ID, status *run.RetryStatus) error {
|
|
50
58
|
return p.shared().UpdateRetry(ctx, id, status)
|
|
51
59
|
}
|