Skip to content

rfc(submitqueue): add modular queue wiring#290

Open
JamyDev wants to merge 10 commits into
mainfrom
jamy/rfc-modular-queue-wiring
Open

rfc(submitqueue): add modular queue wiring#290
JamyDev wants to merge 10 commits into
mainfrom
jamy/rfc-modular-queue-wiring

Conversation

@JamyDev

@JamyDev JamyDev commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

RFC at doc/rfc/submitqueue/modular-queue-wiring.md proposing a declare-don't-assemble engine for the orchestrator's per-queue extension wiring, topic registry, and controller lifecycle:

  1. platform/lifecycleComponent interface (Start/Stop) and ordered Group with rollback-on-partial-failure
  2. platform/pipelineConstruct[D] engine that unifies topic registry, controller creation, DLQ pairing, and consumer lifecycle into one typed call over a []Stage[D] table
  3. Service self-declaration — each service exports a Deps struct (seams), a Stages slice (topology), and a Controllers struct; no assembly code
  4. Host-private profiles — per-queue extension selection stays in the host's profiles.go, never crosses the library boundary
  5. Fluent buildersubmitqueue.New().WithQueue(...).Create() wraps the engine for the common single-orchestrator case

Recommends profiles stay in Go code (Option C) rather than adding hints to QueueConfig. Also adds the RFC to doc/rfc/index.md.

Doc-only change, no code modifications.

Test Plan

CI/Build validation

Revert Plan

git revert

API Changes

No API changes

Monitoring and Alerts

Standard monitoring via existing dashboards

@JamyDev JamyDev changed the title docs(submitqueue): add modular queue wiring RFC Modularize orchestrator wiring: extract queue profiles, topic registry builder, and controller registration Jun 30, 2026
Comment thread doc/rfc/submitqueue/modular-queue-wiring.md Outdated
@JamyDev
JamyDev marked this pull request as ready for review July 10, 2026 17:39
@JamyDev
JamyDev requested review from a team, behinddwalls and sbalabanov as code owners July 10, 2026 17:39
@JamyDev
JamyDev force-pushed the jamy/rfc-modular-queue-wiring branch from 8801dd0 to 3d8d47f Compare July 13, 2026 22:32
Comment thread doc/rfc/submitqueue/modular-queue-wiring.md Outdated
Comment thread doc/rfc/submitqueue/modular-queue-wiring.md Outdated
Comment thread doc/rfc/submitqueue/modular-queue-wiring.md
@behinddwalls

Copy link
Copy Markdown
Collaborator

Design counter-proposal: declare, don't assemble

Full end-to-end walkthrough (every construct defined, every file shown step by step): gist — declare, don't assemble. Summary below.

The test that drove it: could another repo import submitqueue as a library and build an fx / plain-main / test-harness service from it?

The core move: the RFC's extractions (and our first drafts of this feedback) still leave each service implementing assembly — four copies kept aligned by convention and review. The counter-proposal: each service declares itself as data, one shared engine assembles.

piece owner form
engine — construction, lifecycle, DLQ pairing, start/stop ordering platform/pipeline.Construct + platform/lifecycle — written once, tested once code
service definition — Deps (seams) · Stages (queue controllers, one row = controller + topic + DLQ reconciler) · Controllers (RPC, unbound to any wire contract) each domain pkg data + plain funcs
deployment policy — impl selection (profiles.go), creds, topic names, classifiers, transport, signals each main (our service/ mains become reference hosts) host code

Highlights, each shown concretely in the gist:

  • Adding a stage = one table row. Today it takes 3 hand-coordinated edits (topic config, registration, registerDLQControllers); "every consumed stage has a DLQ pair + reconciler" becomes true by construction instead of by review — just in time for the speculation stages.
  • One lifecycle.Component handle; the library exposes no server, reads no env, traps no signals. An fx host maps 1:1 (each block an fx.Provide, Construct one fx.Invoke, lifecycle two fx.Hook lines).
  • Per-queue impl variation is the existing Factory contract ("identity in, resolve internally") — profiles stay host-private in profiles.go, so core/queueprofile stays trigger-gated exactly per the in-code TODO.
  • Controllers are the portable Go surface (proto messages, no server interface — binding to a proto service/transport is ~10 lines of host glue), and the committed api/ protos are the cross-language surface: other languages regenerate stubs from the same sources, one contract, two codegens.
  • Not the DI framework this RFC rightly rejects: no runtime graph, no reflection, no topo-sort — a typed engine over declarative data; one-offs enter via Options.

The problem statement and the QueueConfig A/B/C analysis survive nearly verbatim — this mostly changes the extraction plan's shape. Happy to pair on the rewrite.

@albertywu

albertywu commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@JamyDev @behinddwalls can we have something fluent like this?

app, err := submitqueue.New().
    WithStorage(store).
    WithMessageQueue(mq).
    WithQueue(
        submitqueue.Queue("go-code").
            WithChangeProvider(changeProvider).
            WithBuildRunner(buildkiteBuildRunner).
            WithScorer(defaultScorer).
            WithConflictAnalyzer(tango),
    ).
    WithQueue(
        submitqueue.Queue("java-code").
            WithChangeProvider(changeProvider).
            WithBuildRunner(buildkiteBuildRunner).
            WithScorer(defaultScorer).
            WithConflictAnalyzer(tango),
    ).
    Build()
if err != nil {
    return fmt.Errorf("build SubmitQueue app: %w", err)
}

if err := app.Start(ctx); err != nil {
    return fmt.Errorf("start SubmitQueue app: %w", err)
}
defer app.Stop(context.Background())

return app.Wait(ctx)

or if some config needs to come from YAML for practical reasons (Uber infra):

cfg := loadDeploymentConfig()

app, err := submitqueue.New().
    WithStorage(newStorage(cfg.Database)).
    WithMessageQueue(newMessageQueue(cfg.MessageQueue)).
    WithQueue(newGoCodeQueue(cfg.GoCode)).
    WithQueue(newJavaCodeQueue(cfg.JavaCode)).
    Build()
func newGoCodeQueue(cfg MonorepoConfig) submitqueue.QueueBuilder {
    return submitqueue.Queue("go-code").
        WithBuildRunner(
            buildkite.New(buildkite.Config{
                Endpoint: cfg.BuildEndpoint,
                Timeout:  cfg.BuildTimeout,
            }),
        ).
        WithScorer(
            heuristic.New(goCodeScoringBuckets),
        ).
        WithConflictAnalyzer(
            targets.NewGoAnalyzer(cfg.Repository),
        ).
        WithPrioritizer(
            sticky.New(cfg.MaxBuilds),
        )
}

@JamyDev

JamyDev commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

I think we can add something on top to allow for a fluent builder yea

JamyDev and others added 4 commits July 15, 2026 17:36
Propose extracting per-queue extension profiles, topic-registry builder,
and controller registration from the orchestrator example main.go into
reusable domain packages, plus data-driven profile hints in QueueConfig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onfig removal

Analyze three options for per-queue extension selection in light of
potential QueueConfig removal: hints in the entity, a separate profile
config file, or keeping profiles in Go code only. Recommends landing
the mechanical extractions (steps 1-3) unconditionally and deferring
step 4 until the QueueConfig question is resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pproach

Incorporate the counter-proposal from the design gist: replace the
original 5-step extraction plan with a unified pipeline.Construct[D]
engine + lifecycle.Component approach. Services declare themselves as
Deps struct + Stages slice + Controllers struct; the engine handles
topic registry, DLQ pairing, consumer lifecycle, and startup ordering.

Key changes:
- Add vocabulary section and library/host boundary diagram
- Replace steps 1-5 with the engine-based Steps 1-5 (lifecycle,
  pipeline, service self-declaration, host profiles, host main)
- Simplify QueueConfig trade-off: recommend option C (profiles stay
  in Go code, host-private) over data-driven profile hints
- Show how the pattern generalizes across all four services
- Add "what the engine enforces" invariants table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JamyDev
JamyDev force-pushed the jamy/rfc-modular-queue-wiring branch from 3d8d47f to 0387fe7 Compare July 15, 2026 17:37
@JamyDev
JamyDev requested a review from behinddwalls July 15, 2026 17:37
@JamyDev JamyDev changed the title Modularize orchestrator wiring: extract queue profiles, topic registry builder, and controller registration rfc(submitqueue): add modular queue wiring Jul 16, 2026
JamyDev and others added 3 commits July 16, 2026 18:35
Avoids confusion between the Builder type and its terminal method,
per review feedback from @behindwalls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous description referenced adding profile hints to QueueConfig,
which the RFC's recommended option (C) explicitly avoids. Updated to
reflect the current design: a typed engine, service self-declaration,
and host-owned profiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JamyDev
JamyDev enabled auto-merge July 16, 2026 19:37

func New() *Builder { return &Builder{perQueue: map[string]Profile{}} }

func (b *Builder) WithLogger(l *zap.SugaredLogger) *Builder { b.logger = l; return b }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we can drop With feels verbose

Renames WithLogger→Logger, WithScope→Scope, WithStorage→Storage,
WithMessageQueue→MessageQueue, WithQueue→Queue, WithOption→Option,
WithChangeProvider→ChangeProvider, WithBuildRunner→BuildRunner,
WithScorer→Scorer, WithConflictAnalyzer→ConflictAnalyzer.

Also renames Queue() constructor to NewQueue() since Queue is now a
Builder method name. Per review feedback from @behindwalls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

3 participants