Skip to content

Commit efb61fd

Browse files
fix: admit selected workloads before native service startup
1 parent ba15066 commit efb61fd

24 files changed

Lines changed: 1293 additions & 177 deletions

‎cmd/run/launcher.go‎

Lines changed: 116 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ type launcher struct {
9999
pendingWithdrawals map[string]bool
100100
envelope *gitops.EnvelopeWriter
101101
stop chan struct{}
102+
stopOnce sync.Once
102103

103104
// schedule bounds each workload's wait for the host; waiters are the
104105
// deliveries in flight, one per workload.
@@ -115,31 +116,51 @@ var (
115116
// attachLauncher wires the run as the composition's cluster when its
116117
// environment declares a host and the run is native. It is a no-op otherwise.
117118
func attachLauncher(ctx context.Context, flow *orchestration.Flow, workspace *resources.Workspace, env *environments.Environment, fixture string) error {
118-
if env == nil || env.Host == nil || runtimeContext == resources.RuntimeContextContainer {
119-
return nil
120-
}
121-
w := wool.Get(ctx).In("launcher")
122119
launcherMu.Lock()
123120
defer launcherMu.Unlock()
124121
if launcherActive != nil {
125122
launcherActive.close()
126123
launcherActive = nil
127124
}
128-
if err := env.Host.Validate(); err != nil {
125+
cluster, err := newNativeLauncher(ctx, flow, workspace, env, runtimeContext, fixture)
126+
if err != nil {
129127
return err
130128
}
129+
launcherActive = cluster
130+
return nil
131+
}
132+
133+
// AttachNativeLauncher gives test entry points the same native authority
134+
// admission as run, with explicit configuration and flow-owned teardown.
135+
// Call after InitManagers determines whether this test starts any service.
136+
func AttachNativeLauncher(ctx context.Context, flow *orchestration.Flow, workspace *resources.Workspace, env *environments.Environment, selectedRuntime, fixture string) error {
137+
if len(flow.LocalRuntimeServices()) == 0 {
138+
return nil
139+
}
140+
_, err := newNativeLauncher(ctx, flow, workspace, env, selectedRuntime, fixture)
141+
return err
142+
}
143+
144+
func newNativeLauncher(ctx context.Context, flow *orchestration.Flow, workspace *resources.Workspace, env *environments.Environment, selectedRuntime, fixture string) (*launcher, error) {
145+
if env == nil || env.Host == nil || selectedRuntime == resources.RuntimeContextContainer {
146+
return nil, nil
147+
}
148+
w := wool.Get(ctx).In("launcher")
149+
if err := env.Host.Validate(); err != nil {
150+
return nil, err
151+
}
131152
identities, err := gitops.ResolveNativeIdentities(ctx, workspace, env)
132153
if err != nil {
133-
return err
154+
return nil, err
134155
}
135156
signer, err := signing.LoadDeveloperKey()
136157
if err != nil {
137-
return fmt.Errorf("a native run delivers with the developer key, and none is held: %w", err)
158+
return nil, fmt.Errorf("a native run delivers with the developer key, and none is held: %w", err)
138159
}
139160
stateDir := filepath.Join(workspace.Dir(), ".codefly", "run", env.Name)
140161
launcherIdentity, err := identity.Start(env.Host.TrustDomain, env.Host.Audience, stateDir)
141162
if err != nil {
142-
return err
163+
return nil, err
143164
}
144165
cluster := &launcher{
145166
vouch: launcherIdentity, workspace: workspace, env: env, flow: flow, fixture: fixture,
@@ -154,30 +175,23 @@ func attachLauncher(ctx context.Context, flow *orchestration.Flow, workspace *re
154175
}
155176
parts := strings.Split(env.Host.Delivery, "/")
156177
if len(parts) != 3 {
157-
return fmt.Errorf("host.delivery %q is not <module>/<service>/<endpoint>", env.Host.Delivery)
178+
return nil, fmt.Errorf("host.delivery %q is not <module>/<service>/<endpoint>", env.Host.Delivery)
158179
}
159180
cluster.hostModule, cluster.hostService = parts[0], parts[1]
160181
// The run vouches for exactly what it composes: each identity workload,
161182
// each module's delivery carrier, and the applied observer it acts as.
162183
cluster.declare(identities)
163184
if err := cluster.projectHost(); err != nil {
164-
return err
185+
return nil, err
165186
}
166187
world := flow.World()
167188
world.RenderOwnedGroups = cluster.renderOwnedGroups
168189
world.RuntimeOverrides = cluster.runtimeOverrides
169-
cluster.registerRunningHook(ctx, flow.WithRunningHook)
190+
world.BeginRuntimeStart = cluster.beginRuntimeStart
170191
go cluster.refreshTokens(ctx)
171192
w.Info("native run: the run vouches for the composition's workloads", wool.Field("anchor", launcherIdentity.AnchorFingerprint()[:16]))
172-
launcherActive = cluster
173-
return nil
174-
}
175-
176-
// registerRunningHook binds delivery to the lifetime of this native run.
177-
func (cluster *launcher) registerRunningHook(ctx context.Context, register func(func(string, int))) {
178-
register(func(service string, _ int) {
179-
go cluster.onRunning(ctx, service)
180-
})
193+
world.BeforeRuntimeStop = cluster.close
194+
return cluster, nil
181195
}
182196

183197
func stopLauncher() {
@@ -190,11 +204,11 @@ func stopLauncher() {
190204
}
191205

192206
func (cluster *launcher) close() {
193-
select {
194-
case <-cluster.stop:
195-
default:
196-
close(cluster.stop)
197-
}
207+
cluster.stopOnce.Do(func() {
208+
if cluster.stop != nil {
209+
close(cluster.stop)
210+
}
211+
})
198212
}
199213

200214
// declare records identity workloads the run vouches for, with their delivery
@@ -338,12 +352,7 @@ func (cluster *launcher) runtimeOverrides(service *resources.Service) map[string
338352
if ok {
339353
// A module-identity service reads its token from the path a cell's
340354
// render mounts unless told otherwise; this is the otherwise.
341-
credential, err := cluster.project(unique, identity)
342-
if err != nil {
343-
wool.Get(context.Background()).In("launcher").Warn("cannot project the module's credential", wool.Field("service", unique), wool.ErrField(err))
344-
return out
345-
}
346-
out[workloadTokenFileVariable] = credential.File
355+
out[workloadTokenFileVariable] = cluster.vouch.ProjectionPath(identity.Workload(cluster.env.Host.TrustDomain))
347356
}
348357
if unique == resources.ServiceUnique(cluster.hostModule, cluster.hostService) {
349358
out[nativeidentity.AnchorEnvironment] = cluster.hostIdentityAnchor
@@ -369,11 +378,11 @@ func (cluster *launcher) renderOwnedGroups(ctx context.Context, service *resourc
369378
out = append(out, group)
370379
}
371380
if slices.Contains(service.WorkspaceConfigurationDependencies, environments.ArtifactCacheConfiguration) {
372-
group, err := artifactCacheGroup(cluster.composed().Dir())
373-
if err != nil {
374-
return nil, err
381+
cacheGroup, cacheErr := artifactCacheGroup(cluster.composed().Dir())
382+
if cacheErr != nil {
383+
return nil, cacheErr
375384
}
376-
out = append(out, group)
385+
out = append(out, cacheGroup)
377386
}
378387
if !slices.Contains(service.WorkspaceConfigurationDependencies, environments.ModuleAuthorityConfiguration) {
379388
return out, nil
@@ -482,39 +491,25 @@ func (cluster *launcher) refreshTokens(ctx context.Context) {
482491
}
483492
}
484493

485-
// onRunning is the cell's delivery, for one identity workload that just
486-
// started: its module's manifest, listing it — once the host is ready to take
487-
// it, retrying while it is not serving yet (hostDelivery). A workload that is
488-
// not delivered is said so plainly: the gateway routes nothing to its module.
489-
func (cluster *launcher) onRunning(ctx context.Context, unique string) {
490-
w := wool.Get(ctx).In("launcher.deliver", wool.Field("service", unique))
491-
identity, ok := cluster.identity(unique)
492-
if !ok || !cluster.delivers(identity) {
493-
return
494-
}
495-
cluster.mu.Lock()
496-
cluster.running[unique] = true
497-
cluster.mu.Unlock()
498-
waiter := cluster.waitFor(unique)
494+
// runReadyDelivery publishes the latest readiness after an attempt. Failed
495+
// restarts remove stale routes through the same nonterminal signed manifest.
496+
func (cluster *launcher) runReadyDelivery(ctx context.Context, unique string, identity gitops.NativeIdentity, waiter *deliveryWaiter, failed bool) {
499497
defer cluster.doneWaiting(unique, waiter)
498+
w := wool.Get(ctx).In("launcher.deliver", wool.Field("service", unique))
500499
err := hostDelivery{
501500
schedule: cluster.schedule, stop: cluster.stop, superseded: waiter.superseded,
502501
log: w.With(wool.Field("module", identity.Module)),
503502
ready: func(ctx context.Context) error {
504-
// A refreshed composition can withdraw the workload while its
505-
// delivery waits; nothing is then delivered on its behalf.
506503
if _, composed := cluster.identity(unique); !composed {
507504
return errWorkloadGone
508505
}
509506
return cluster.hostReady(ctx)
510507
},
511-
deliver: func(ctx context.Context) error { return cluster.deliver(ctx, identity.Module) },
508+
deliver: func(ctx context.Context) error { return cluster.deliverReadiness(ctx, identity.Module, failed) },
512509
}.run(ctx)
513510
if deliveryAbandoned(err) {
514511
w.Debug("delivery abandoned", wool.ErrField(err))
515-
return
516-
}
517-
if err != nil {
512+
} else if err != nil {
518513
w.Error(fmt.Sprintf("delivery completion was not confirmed for module %s, workload %s; routes to module %s may be unavailable (the host may already have applied the manifest)", identity.Module, unique, identity.Module), wool.ErrField(err))
519514
}
520515
}
@@ -560,7 +555,8 @@ func (cluster *launcher) newDeliverer(ctx context.Context) (*gitops.NativeDelive
560555
return gitops.Roles(m.Dir())
561556
})
562557
return &gitops.NativeDeliverer{
563-
Workspace: cluster.composed(), Env: cluster.env, Signer: cluster.signer,
558+
CloseTransport: func() { _ = conn.Close() },
559+
Workspace: cluster.composed(), Env: cluster.env, Signer: cluster.signer,
564560
StateDir: filepath.Join(cluster.stateDir, "delivery"), DeliveryURL: deliveryURL,
565561
Token: func(module string) (string, error) {
566562
return cluster.vouch.Issue(cluster.vouch.Workload(module, workloadidentity.DeliveryService))
@@ -616,7 +612,7 @@ func (cluster *launcher) withdraw(ctx context.Context, uniques []string) ([]stri
616612
sort.Strings(pending)
617613
var withdrawn []string
618614
for _, module := range pending {
619-
if len(cluster.startedIdentities(module)) > 0 {
615+
if len(cluster.declaredIdentities(module)) > 0 {
620616
if err := cluster.deliver(ctx, module); err != nil {
621617
return withdrawn, fmt.Errorf("%s: deliver the workloads that remain: %w", module, err)
622618
}
@@ -659,6 +655,9 @@ func (cluster *launcher) withdrawModule(ctx context.Context, module string) (boo
659655
if err != nil {
660656
return false, err
661657
}
658+
if deliverer.CloseTransport != nil {
659+
defer deliverer.CloseTransport()
660+
}
662661
receipt, err := deliverer.Deliver(ctx, withdrawal)
663662
if err != nil {
664663
return false, err
@@ -723,61 +722,96 @@ func (cluster *launcher) startedIdentities(module string) []gitops.NativeIdentit
723722
// that runs now, with the addresses this run allocated for its routed
724723
// endpoints, after making sure the host holds the composition's envelope.
725724
func (cluster *launcher) deliver(ctx context.Context, module string) error {
725+
return cluster.deliverReadiness(ctx, module, false)
726+
}
727+
728+
func (cluster *launcher) deliverReadiness(ctx context.Context, module string, early bool) error {
726729
if err := cluster.acquireDelivery(ctx); err != nil {
727730
return err
728731
}
729732
defer cluster.delivery.Release(1)
730-
started := cluster.startedIdentities(module)
731-
if len(started) == 0 {
733+
// Failure may precede admission. Replace a previous host snapshot, but do
734+
// not admit a new module just to clean up an unstarted attempt.
735+
if early && cluster.delivered[module] == nil {
732736
return nil
733737
}
734-
loaded, err := cluster.composed().LoadModuleFromName(ctx, module)
738+
if len(cluster.declaredIdentities(module)) == 0 {
739+
return nil
740+
}
741+
deliverer, gatewayURL, err := cluster.newDeliverer(ctx)
735742
if err != nil {
736743
return err
737744
}
745+
if deliverer.CloseTransport != nil {
746+
defer deliverer.CloseTransport()
747+
}
748+
manifest, err := cluster.renderDelivery(ctx, deliverer, module, early)
749+
if err != nil {
750+
return err
751+
}
752+
return cluster.applyDelivery(ctx, deliverer, gatewayURL, module, manifest)
753+
}
754+
755+
func (cluster *launcher) renderDelivery(ctx context.Context, deliverer *gitops.NativeDeliverer, module string, early bool) (*solutionhost.ModuleManifest, error) {
756+
loaded, err := cluster.composed().LoadModuleFromName(ctx, module)
757+
if err != nil {
758+
return nil, err
759+
}
738760
services := make([]*resources.Service, 0, len(loaded.ServiceReferences))
739761
for _, reference := range loaded.ServiceReferences {
740762
service, loadErr := cluster.composed().LoadService(ctx, &resources.ServiceWithModule{Name: reference.Name, Module: module})
741763
if loadErr != nil {
742-
return loadErr
764+
return nil, loadErr
743765
}
744766
services = append(services, service)
745767
}
746-
deliverer, gatewayURL, err := cluster.newDeliverer(ctx)
747-
if err != nil {
748-
return err
749-
}
750-
// A host reached through the composition interface holds the manifest as the
751-
// ceiling and takes no envelope.
752-
if gatewayURL != "" {
753-
if err = cluster.ensureEnvelope(ctx, gatewayURL); err != nil {
754-
return err
768+
var bundle *solutionhost.FrontendPayload
769+
if early {
770+
// Deferral removes a page. Keep a previously published payload during
771+
// unrelated partial startup instead of accidentally unpublishing it.
772+
if previous := cluster.delivered[module]; previous != nil && previous.Frontend != nil {
773+
bundle = previous.Frontend.Payload
774+
}
775+
} else {
776+
bundle, err = cluster.frontendBundle(ctx, loaded)
777+
if err != nil {
778+
return nil, err
755779
}
756780
}
757-
bundle, err := cluster.frontendBundle(ctx, loaded)
758-
if err != nil {
759-
return err
781+
started := cluster.startedIdentities(module)
782+
var ready []string
783+
for _, identity := range started {
784+
ready = append(ready, resources.ServiceUnique(identity.Module, identity.Service))
760785
}
761-
carriers, err := cluster.flow.EndpointCarriers(ctx, module, resources.NewNativeNetworkAccess())
786+
carriers, err := cluster.flow.ReadyEndpointCarriers(ctx, module, resources.NewNativeNetworkAccess(), ready)
762787
if err != nil {
763-
return err
788+
return nil, err
764789
}
765-
manifest, err := deliverer.Render(ctx, &gitops.NativeWorkload{Module: loaded, Services: services, Started: started, Carriers: carriers, FrontendBundle: bundle})
766-
if err != nil {
767-
return err
790+
return deliverer.Render(ctx, &gitops.NativeWorkload{
791+
Module: loaded, Services: services, Declared: cluster.declaredIdentities(module),
792+
Started: started, Carriers: carriers, FrontendBundle: bundle, DeferFrontend: early && bundle == nil,
793+
})
794+
}
795+
796+
// Caller holds delivery across rendering, generation reservation and Applied.
797+
func (cluster *launcher) applyDelivery(ctx context.Context, deliverer *gitops.NativeDeliverer, gatewayURL, module string, manifest *solutionhost.ModuleManifest) error {
798+
if gatewayURL != "" {
799+
if err := cluster.ensureEnvelope(ctx, gatewayURL); err != nil {
800+
return err
801+
}
768802
}
769803
receipt, err := deliverer.Deliver(ctx, manifest)
770804
if err != nil {
771805
return err
772806
}
773807
cluster.delivered[module] = manifest
774-
wool.Get(ctx).In("launcher.deliver").Info("delivered", wool.Field("module", module), wool.Field("workloads", len(manifest.Workloads)),
808+
w := wool.Get(ctx).In("launcher.deliver")
809+
w.Info("delivered", wool.Field("module", manifest.Alias), wool.Field("workloads", len(manifest.Workloads)),
775810
wool.Field("disposition", receipt.Disposition), wool.Field("generation", receipt.Generation))
776-
// Accepted into the inbox is not applied. The host says which, and why.
777811
if err := deliverer.WaitForApplied(ctx, manifest); err != nil {
778-
return fmt.Errorf("module %s is delivered and not served: %w", module, err)
812+
return fmt.Errorf("module %s is delivered and not served: %w", manifest.Alias, err)
779813
}
780-
wool.Get(ctx).In("launcher.deliver").Info("applied", wool.Field("module", module), wool.Field("generation", manifest.Generation))
814+
w.Info("applied", wool.Field("module", manifest.Alias), wool.Field("generation", manifest.Generation))
781815
return nil
782816
}
783817

0 commit comments

Comments
 (0)