Skip to content

[CRE-6176] move registration handles ack to dispatcher - #23640

Open
agparadiso wants to merge 1 commit into
developfrom
CRE-6176_move_registration_handles_ack_to_dispatcher
Open

[CRE-6176] move registration handles ack to dispatcher#23640
agparadiso wants to merge 1 commit into
developfrom
CRE-6176_move_registration_handles_ack_to_dispatcher

Conversation

@agparadiso

@agparadiso agparadiso commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

This pr is part 1 of CRE-6176

Its basically creating the dispatcher component that holds registration, handles and ACK. I've split the ticket in two tickets to make it easier to review. The code added here is not called anywhere yet, the wiring and clean up of the methods moved from the engine will be tackled on the next pr.

Requires

Supports

@agparadiso agparadiso changed the title Cre 6176 move registration handles ack to dispatcher [CRE-6176] move registration handles ack to dispatcher Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

I see you updated files related to core. Please run make gocs in the root directory to add a changeset as well as in the text include at least one of the following tags:

  • #added For any new functionality added.
  • #breaking_change For any functionality that requires manual action for the node to boot.
  • #bugfix For bug fixes.
  • #changed For any change to the existing functionality.
  • #db_update For any feature that introduces updates to database schema.
  • #deprecation_notice For any upcoming deprecation functionality.
  • #internal For changesets that need to be excluded from the final changelog.
  • #nops For any feature that is NOP facing and needs to be in the official Release Notes for the release.
  • #removed For any functionality/config that is removed.
  • #updated For any functionality that is updated.
  • #wip For any change that is not ready yet and external communication about it should be held off till it is feature complete.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

✅ No conflicts with other open PRs targeting develop

@trunk-io

trunk-io Bot commented Sep 3, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@agparadiso
agparadiso force-pushed the CRE-6176_move_registration_handles_ack_to_dispatcher branch 2 times, most recently from d73a447 to 887ba51 Compare September 9, 2026 11:56
@agparadiso
agparadiso force-pushed the CRE-6176_move_registration_handles_ack_to_dispatcher branch from 887ba51 to 5b2a083 Compare September 10, 2026 11:06
@agparadiso
agparadiso marked this pull request as ready for review September 10, 2026 11:11
@agparadiso
agparadiso requested a review from a team as a code owner September 10, 2026 11:11
@cl-sonarqube-production

Copy link
Copy Markdown

@bolekk bolekk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid re-using the name "Dispatcher" please? It will cause confusion with the don2don Dispatcher

d.mu.Unlock()

// Unregister with the capability registry outside the lock.
ctx := context.Background()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pass a context derived from the d.eng.NewCtx()?f

Suggested change
ctx := context.Background()
ctx, cancel := d.eng.NewCtx()
defer cancel()

Comment on lines +318 to +329
d.startReader(ctx, wid, idx, subs[idx], triggerEventCh)
}
d.lggr.Infow("All triggers registered successfully", "numTriggers", len(subs), "triggerIDs", triggerCapIDs)
d.metrics.IncrementWorkflowRegisteredCounter(ctx)
return triggerCapIDs, nil
}

// startReader runs one reader goroutine per subscription. It resolves the
// engine from the registry at delivery time and never holds an engine
// reference; if the engine is gone the reader exits.
func (d *triggerDispatcher) startReader(ctx context.Context, wid types.WorkflowID, idx int, sub *sdkpb.TriggerSubscription, triggerEventCh <-chan capabilities.TriggerResponse) {
d.eng.GoCtx(context.WithoutCancel(ctx), func(ctx context.Context) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about moving d.eng.GoCtx to the caller, so that this method can be defined more simply and with less indentation?

Comment on lines +158 to +162
// start is a no-op: the dispatcher has no background work of its own — reader
// goroutines are started per subscription by RegisterTriggers and tracked by
// the embedded services.Engine. The method exists to satisfy the
// services.Service Start hook contract.
func (d *triggerDispatcher) start(context.Context) error { return nil }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit this when using services.Engine - it nil checks.

}

// register to all triggers concurrently
regCtx, regCancel, err := d.regTime.WithTimeout(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes when there are multiple contexts in scope, there is a natural point to split out a helper func so that each independent scope can have one clean context named idiomatically as ctx. What if we introduced a method for this block of logic?

Payload: handle.payload,
Method: handle.method,
}); err != nil {
d.lggr.Errorw("Failed to unregister trigger", "registrationId", registrationID, "err", err)

@jmank88 jmank88 Sep 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these errors significant? I'm worried it could be overlooked that we only log them. Easy change for readability could be to call it tryUnregisterAll or similar. Or another option could be to collect a batch of errors to return to the caller (maybe more testable this way).

@cedric-cordenier

Copy link
Copy Markdown
Contributor

+1 to Bolek's comment -- let's find another name :) Router maybe?

// ReleaseHandles drops the retained handles for a workflow, after which
// its in-flight executions can no longer acknowledge. Called by the syncer
// once the engine has drained and closed.
ReleaseHandles(workflowID string) error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concept of a handle didn't exist before -- what are we doing under the hood? Releasing the subscription? disabling routing?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more: why do we need ReleaseHandles? What is it doing that UnregisterTriggers couldn't do for us?

// starts one reader goroutine per subscription, and returns the registered
// trigger capability IDs. On any registration failure it rolls back the
// successful registrations.
func (d *triggerDispatcher) RegisterTriggers(ctx context.Context, cre contexts.CRE, params RegistrationParams, subs []*sdkpb.TriggerSubscription) ([]string, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I wouldn't pass cre in expilictly, pass it in via the ctx -- since the engine owns the root context here we should be able to ensure that all calls to it have cre context


// ReleaseHandles drops the retained handles for a workflow, after which its
// in-flight executions can no longer acknowledge. Safe to call multiple times.
func (d *triggerDispatcher) ReleaseHandles(workflowID string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can UnregisterTriggers call ReleaseHandles for us?

// and calling AckEvent on the trigger capability. Engines call this after
// execution starts, on duplicate executions, and on shard-ownership denials —
// the point at which the event is fully handled and must not be redelivered.
func (d *triggerDispatcher) Ack(ctx context.Context, triggerCapID, triggerRegistrationID, eventID string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is called be the engine, it will know what the wid is, so you can just make that part of the interface rather than having to maintain a registration -> wid index

@cedric-cordenier

Copy link
Copy Markdown
Contributor

Code location comment: why is this code located in the syncer? Is that the best place for it?


// triggerHandle is a registered trigger capability plus the registration
// payload/method needed to unregister it.
type triggerHandle struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this triggerRegistration and give it an Unregister method that adds in your payload and method and anything else you need

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with AckEvent etc


// ReportWorkflowLimitPerOwner records a per-owner workflow count limit rejection.
func (d *triggerDispatcher) ReportWorkflowLimitPerOwner(ctx context.Context) {
d.metrics.IncrementWorkflowLimitPerOwnerCounter(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why these metrics are exposed here -- who calls it? This feels like a separate responsibility not related to triggers

if err2 != nil {
return nil, fmt.Errorf("invalid chain selector for ID %s: %w", sub.Id, err2)
}
_ = chainSelector // chain access check moves with the limiter split (CRE-6177)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is line 188->194 doing? it looks like we're not using this code at all; we should remove it if we're not using it because we may change our decision later

for registrationID, handle := range wt.handles {
if unregErr := handle.UnregisterTrigger(ctx, capabilities.TriggerRegistrationRequest{
TriggerID: registrationID,
Metadata: capabilities.RequestMetadata{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata here is different to what we send with RegisterTrigger; they should be the same though, to keep the calls symmetric. Is this what the current implementation does?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants