// An instance's environment binding and a seed's store (final review F3). // A seed marks the store it makes (objstore.Declared.Seed), and its finish // tears that store down while the mark stands and no other instance names // the environment (seedfinish.go). An instance that comes to name the // environment afterwards holds the store from then on, so the binding // clears the mark under the store's walk lock, the lock the finish holds // from its last check through the teardown. The lock is this process's: // one console per deployment is a rollout invariant of the seeds series. package console import ( "context" "log" "net/http" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/provision" ) // lockEnvironmentUpdate is the update door's binding: when the patch moves // rec to another environment, it takes that environment's store walk lock // and moves the store to rec (takeSeedStore). The caller holds unlock // across its record write. A store that cannot be read or written answers // main's update retry sentence, and ok is false. func (s *Server) lockEnvironmentUpdate(ctx context.Context, w http.ResponseWriter, stored, rec provision.Record) (unlock func(), ok bool) { if rec.Environment == stored.Environment { return func() {}, true } unlock = s.storeWalks.lock(rec.Account, rec.App, rec.Environment) if err := s.takeSeedStore(ctx, rec); err != nil { unlock() log.Printf("console: api instances update %s/%s/%s: environment %q: %v", rec.Account, rec.App, rec.Host, rec.Environment, err) jsonError(w, http.StatusBadGateway, "could not update the instance — try again") return nil, false } return unlock, true } // bindEnvironmentCreate is the create door's binding for an instance that // names an environment and provisions no store of its own (a private // storage create's grant clears the mark itself, by GrantInFor's rule). It // moves the store to rec under the lock. A failure answers main's create // retry sentence, and the answer is false. func (s *Server) bindEnvironmentCreate(ctx context.Context, w http.ResponseWriter, rec provision.Record) bool { if rec.Environment == "" { return true } defer s.storeWalks.lock(rec.Account, rec.App, rec.Environment)() if err := s.takeSeedStore(ctx, rec); err != nil { log.Printf("console: api instances create %s/%s/%s: environment %q: %v", rec.Account, rec.App, rec.Host, rec.Environment, err) jsonError(w, http.StatusBadGateway, "could not create the instance — try again") return false } return true } // takeSeedStore clears the seed mark on the store of rec's environment // when it is not rec's own seed's: GrantInFor's rule (objstore // claimRawGrantIn) for a binding that grants nothing. Whoever binds the // store now holds it, and the seed's finish must never tear it down; an // aborted seed's bucket can leak that way, which is the safe direction. // The caller holds the environment's store walk lock. func (s *Server) takeSeedStore(ctx context.Context, rec provision.Record) error { store, etag, err := objstore.GetIn(ctx, s.cfg.Releases, rec.Account, rec.App, rec.Environment) if err != nil { return err } mine := "" if rec.Seed != nil { mine = rec.Seed.ID } if store == nil || store.Declared == nil || store.Declared.Seed == "" || store.Declared.Seed == mine { return nil } seed := store.Declared.Seed store.Declared.Seed = "" if _, err := objstore.PutIn(ctx, s.cfg.Releases, rec.Account, rec.App, rec.Environment, store, etag); err != nil { return err } log.Printf("console: instance %s/%s/%s binds environment %q: the store seed %s made (bucket %s) is no longer that seed's, and its finish leaves it standing", rec.Account, rec.App, rec.Host, rec.Environment, seed, storeBucketName(store)) return nil }