| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/hostbox" |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | execlease "github.com/carlosframework/platform/internal/hostexec" |
| 11 | ) |
| 12 | |
| 13 | // Acknowledged is region HA's read of main's participation for a |
| 14 | // relocating operation (rev 2, ruling 42). It adds no authority: it is |
| 15 | // Prepare, exactly as WorkSource.Prepare and the participation loop call |
| 16 | // it, then the slot's own Ready. Whichever caller comes first opens the |
| 17 | // box's one controller for {host, op}; every later caller gets that same |
| 18 | // controller's state. A lost or failed controller is main's retained |
| 19 | // denial (CC9), returned as its error and never replaced here. |
| 20 | func (p *productionClaimLocal) Acknowledged(ctx context.Context, o hostclaim.Observed) (execlease.State, error) { |
| 21 | if o.Claim == nil || o.Claim.Holder == nil || o.Claim.Op.Kind != hostclaim.KindRelocating { |
| 22 | return execlease.State{}, hostbox.ErrChanged |
| 23 | } |
| 24 | if err := p.Prepare(ctx, o); err != nil { |
| 25 | return execlease.State{}, err |
| 26 | } |
| 27 | p.mu.Lock() |
| 28 | slot := p.slots[o.Host] |
| 29 | p.mu.Unlock() |
| 30 | if slot == nil { |
| 31 | return execlease.State{}, execlease.ErrInactive |
| 32 | } |
| 33 | select { |
| 34 | case <-slot.Ready: |
| 35 | case <-ctx.Done(): |
| 36 | return execlease.State{}, ctx.Err() |
| 37 | } |
| 38 | if slot.Err != nil { |
| 39 | return execlease.State{}, slot.Err |
| 40 | } |
| 41 | if slot.Controller == nil { |
| 42 | return execlease.State{}, execlease.ErrInactive |
| 43 | } |
| 44 | return slot.Controller.Ready(ctx) |
| 45 | } |
| 46 | |
| 47 | // boundAssembly is the assembly Build bound this owner to: the copy whose |
| 48 | // reconcile adapter, census and guard reader Build configured. Nil before |
| 49 | // Build. N1's second accessor. |
| 50 | func (p *productionClaimLocal) boundAssembly() *ClaimAssembly { |
| 51 | p.mu.Lock() |
| 52 | defer p.mu.Unlock() |
| 53 | return p.assembly |
| 54 | } |
| 55 | |
| 56 | // DischargeCompleted is ruling 66's gate before a completed relocation's |
| 57 | // hold is released: main's own Prepare for the serving observation, which |
| 58 | // closes the participation slot (its cleanup is C10's discharge, first) |
| 59 | // and clears the .hostexec record, or, after a restart, clears the |
| 60 | // untracked record through clearUntracked (the same discharge, keyed by |
| 61 | // the persisted binding). It succeeds only once neither a slot nor a |
| 62 | // record is left for the host. N1's third accessor; no new authority. |
| 63 | func (p *productionClaimLocal) DischargeCompleted(ctx context.Context, o hostclaim.Observed) error { |
| 64 | if o.Claim == nil || o.Claim.Holder == nil || o.Claim.Op.Kind != hostclaim.KindServing { |
| 65 | return hostbox.ErrChanged |
| 66 | } |
| 67 | actor, err := p.Identity.Verify(ctx) |
| 68 | if err != nil { |
| 69 | return err |
| 70 | } |
| 71 | return p.dischargeCompletedAs(ctx, actor, o) |
| 72 | } |
| 73 | |
| 74 | // dischargeCompletedAs is DischargeCompleted for a verified actor: main's |
| 75 | // prepareWith, then nothing of the participation may be left. |
| 76 | func (p *productionClaimLocal) dischargeCompletedAs(ctx context.Context, actor hostbox.Actor, o hostclaim.Observed) error { |
| 77 | if err := p.prepareWith(ctx, actor, o); err != nil { |
| 78 | return err |
| 79 | } |
| 80 | p.mu.Lock() |
| 81 | slot := p.slots[o.Host] |
| 82 | p.mu.Unlock() |
| 83 | if slot != nil { |
| 84 | return execlease.ErrInactive |
| 85 | } |
| 86 | if !p.legacyParticipation { |
| 87 | return nil |
| 88 | } |
| 89 | if _, err := execlease.InspectLocal(p.Resources.DataDir, o.Host); !errors.Is(err, os.ErrNotExist) { |
| 90 | return errors.Join(execlease.ErrInactive, err) |
| 91 | } |
| 92 | return nil |
| 93 | } |
| 94 | |