Skip to content

feat(tern): add static target router#339

Draft
aparajon wants to merge 3 commits into
mainfrom
aparajon/static-target-resolver
Draft

feat(tern): add static target router#339
aparajon wants to merge 3 commits into
mainfrom
aparajon/static-target-resolver

Conversation

@aparajon

@aparajon aparajon commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Split in progress. This PR is being broken into reviewable leaves:

Once #340 and #341 merge, this PR will be reduced to just tern.TargetRouter rebased on main. It currently still contains the code from those two leaves so it compiles and passes CI on its own.

Why

A data-plane process needs a reusable way to serve many opaque execution targets without each embedder rebuilding inventory lookup and per-target client caching.

What

  • tern.TargetRouter, a tern.Client that resolves a target via pkg/inventory and caches LocalClients keyed by target/type/environment (a data plane has one deployment and many targets).
  • Routes target-scoped pull, plan, apply, progress, controls, and operator resume through the cached clients.
  • Preserves the plan-time target on stored applies and recovers it from stored plans for restart/control routing.
╭───────────────╮     ╭────────────────╮     ╭─────────────╮
│ Tern request  │────▶│ Target router  │────▶│ LocalClient │
│ target: key   │     │ key → client   │     │ per target  │
╰───────────────╯     ╰───────┬────────╯     ╰─────────────╯
                              │
                              ▼
                       ╭────────────────╮
                       │ inventory      │
                       │ key → DSN/meta │
                       ╰────────────────╯

Risk

Low — additive and not wired into the default server path yet. Existing clients keep using the current routing unless a caller explicitly constructs the router.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 14, 2026 14:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new data-plane routing seam for Tern by adding a targetresolver contract plus a tern.TargetRouter client that resolves opaque targets into connection inputs and caches per-target local clients. This is intended to let deployments start with static target configuration and later swap to dynamic/inventory-backed resolution without reimplementing lookup/caching.

Changes:

  • Added pkg/targetresolver with a Resolver interface and a YAML-configurable StaticResolver backend.
  • Added pkg/tern/target_router.go, a tern.Client implementation that resolves targets and routes operations through cached LocalClients.
  • Added unit tests for both the target router and the static resolver.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/tern/target_router.go Adds TargetRouter that resolves targets and routes/caches local clients, including observer handling.
pkg/tern/target_router_test.go Adds unit tests validating basic routing/caching and observer attachment behavior.
pkg/targetresolver/resolver.go Defines the resolver request/result contract for opaque target resolution.
pkg/targetresolver/static.go Implements a static, YAML-backed resolver with secret expansion for DSNs.
pkg/targetresolver/static_test.go Adds unit tests for static resolution, validation, and metadata cloning behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/tern/target_router.go
Comment on lines +38 to +42
mu sync.Mutex
clientsByTarget map[string]Client
activeObservers map[int64]ProgressObserver
pendingObservers map[string]ProgressObserver
}
Comment thread pkg/tern/target_router.go
Comment on lines +360 to +369
func (r *TargetRouter) clientForStoredApply(ctx context.Context, apply *storage.Apply) (Client, error) {
if apply == nil {
return nil, fmt.Errorf("stored apply is required for target routing")
}
target := apply.GetOptions().Target
if target == "" {
target = apply.Database
}
return r.clientOnlyForTarget(ctx, target, apply.DatabaseType, apply.Environment)
}
Comment on lines +193 to +210
func TestTargetRouterRoutesStoredApplyByTargetOption(t *testing.T) {
resolver := newStaticTargetResolver(t)
created := make(map[string]*targetRouterRecordingClient)
apply := &storage.Apply{
ID: 42,
ApplyIdentifier: "apply-42",
Database: "orders",
DatabaseType: storage.DatabaseTypeMySQL,
Environment: "production",
}
apply.SetOptions(storage.ApplyOptions{Target: "dsid-orders-prod"})
store := targetRouterApplyStore{
byID: map[int64]*storage.Apply{42: apply},
byIdentifier: map[string]*storage.Apply{"apply-42": apply},
}
router := newTargetRouterForTest(t, resolver, store, created)
router.SetObserver(42, targetRouterNoopObserver{})

@aparajon aparajon force-pushed the aparajon/static-target-resolver branch 3 times, most recently from 27358b5 to 0b1c8b0 Compare June 14, 2026 21:45
@aparajon aparajon force-pushed the aparajon/static-target-resolver branch from 0b1c8b0 to 6346e3c Compare June 14, 2026 21:47
aparajon and others added 2 commits June 14, 2026 18:40
…-free targets

Move MySQL schema selection out of target resolution and into LocalClient
execution. A target DSN that already names a database keeps the local-DSN
behavior, where the namespace is an organizational label and Spirit connects to
the DSN's database. A namespace-free target DSN is the inventory/data-plane
shape: the concrete namespace is the connection schema and is injected per
Spirit operation at plan, pull, apply, stop, cutover, and resume.

This lets the target router cache one namespace-free LocalClient per target
while each schema change connects to its own namespace, and keeps the inventory
contract free of execution namespaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
controlSetup feeds Volume and other control operations; resolve the task's
namespace credentials so namespace-free MySQL targets connect to the right
schema instead of the namespace-free target DSN.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

2 participants