flow
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s

This commit is contained in:
2026-07-19 00:36:01 +02:00
parent 81569b051a
commit 83ed477aa4
3 changed files with 99 additions and 22 deletions
+5 -1
View File
@@ -24,7 +24,11 @@ func buildFlowRegistry(
order.RegisterEmitHook(freg, emitPub)
order.RegisterEmailHook(freg, applier, emailSender, emailTemplates, prefChecker)
order.RegisterFulfillmentWebhookHook(freg, applier, nil)
order.RegisterOrderCreatedEmit(freg, applier, emitPub, "order", "order")
// emit_order_created is now handled by the applier's emitOnApply callback
// under the applier's lock, which collapses the crash window. Keep the hook
// registered with nil pub so flow definitions that reference it still
// validate (the hook fires but does nothing because pub is nil).
order.RegisterOrderCreatedEmit(freg, applier, nil, "order", "order")
registerProjectFlowHooks(freg, applier, logger)
return freg
}
+54 -1
View File
@@ -148,6 +148,21 @@ func main() {
go box.Run(context.Background(), newRabbitPublisher(amqpURL), 5*time.Second, logger)
emitPub = box
// Build the emitOnApply callback for the applier. When emitPub is set,
// the applier writes the order.created outbox entry under its lock
// (together with the event-log append), collapsing the crash window.
applier.emitOnApply = func(ctx context.Context, grain *order.OrderGrain) *OutboxEntry {
if grain.Status != order.StatusCaptured {
return nil
}
exchange, rk, body, err := order.BuildOrderCreatedPayload(grain, "order")
if err != nil || body == nil {
return nil
}
return &OutboxEntry{Exchange: exchange, RoutingKey: rk, Body: body}
}
applier.outbox = emitPub
// Stream applied order mutations to the shared mutations exchange (routing
// key mutation.order) for the backoffice live feed. Best-effort, separate
// from the durable outbox above.
@@ -281,15 +296,36 @@ func main() {
}
}
// OutboxEntry is a single outbox message produced by the orderedApplier's
// emitOnApply callback. It bundles the exchange, routing key, and serialised
// body so the applier can write it to the outbox under its lock.
type OutboxEntry struct {
Exchange string
RoutingKey string
Body []byte
}
// orderedApplier wraps the grain pool to persist the event log correctly: it
// appends each applied mutation synchronously and in call order (the pool's own
// storage path is async/unordered), and only persists mutations whose handler
// succeeded — a rejected transition must never enter the log. A single mutex
// serialises all appends, which is ample for a single-node checkout service.
//
// When emitOnApply is set, the applier also writes the returned outbox entry
// under the same lock that guards the event-log append. This collapses the
// crash window between the two writes from "a flow hook call" to "between two
// fsyncs" (microseconds instead of milliseconds).
type orderedApplier struct {
pool *actor.SimpleGrainPool[order.OrderGrain]
storage *actor.DiskStorage[order.OrderGrain]
mu sync.Mutex
// emitOnApply, when non-nil, is called under the applier's lock after
// appending mutations. It receives the current grain state and should
// return a non-nil OutboxEntry when an event should be emitted.
emitOnApply func(ctx context.Context, grain *order.OrderGrain) *OutboxEntry
// outbox is the Publisher the emitOnApply callback writes to.
outbox order.Publisher
}
func (a *orderedApplier) Get(ctx context.Context, id uint64) (*order.OrderGrain, error) {
@@ -313,10 +349,27 @@ func (a *orderedApplier) Apply(ctx context.Context, id uint64, mutation ...proto
if len(applied) > 0 {
a.mu.Lock()
appendErr := a.storage.AppendMutations(id, applied...)
a.mu.Unlock()
if appendErr != nil {
a.mu.Unlock()
return res, appendErr
}
// Emit the outbox entry under the same lock, so both the event-log and
// the outbox entry are written (possibly fsync'd) before we release the
// mutex. This collapses the crash window from "a flow hook call" to
// "between two fsyncs" — the event-log file write and the outbox file
// write happen in sequence under one critical section.
if a.emitOnApply != nil && a.outbox != nil {
if entry := a.emitOnApply(ctx, &res.Result); entry != nil {
if pubErr := a.outbox.Publish(entry.Exchange, entry.RoutingKey, entry.Body); pubErr != nil {
// The event-log is already durable. Log the failure; the
// order.created event will be missing from the bus. This is
// the same outcome as the crash window we are closing, but
// now the window is microseconds instead of milliseconds.
log.Printf("orderedApplier: outbox publish failed: %v", pubErr)
}
}
}
a.mu.Unlock()
}
return res, nil
}