package hostbox import ( "context" "fmt" "github.com/carlosframework/platform/internal/park" "strings" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/release" ) // Writer supplies transition adapters, not stop, certificate or marker evidence. type Writer struct { HomeRegion string Loss *LossAdmission Claims release.Store Data genselect.Reader Bucket, Region, Box string AdmissionEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error PublicationEvidence func(context.Context, hostclaim.Observed) error NewID func() string } func (w Writer) Baseline(ctx context.Context, observed hostclaim.Observed) (*genselect.Plan, error) { if w.Data == nil || w.Bucket == "" || w.Region == "" || observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil { return nil, ErrUnsupported } return genselect.Select(ctx, w.Data, genselect.Request{Bucket: w.Bucket, Region: w.Region, Host: observed.Host, Holder: park.HolderRegionView(*observed.Claim.Holder, w.HomeRegion, w.Region)}) } func (w Writer) Admit(ctx context.Context, observed hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) (hostclaim.Observed, error) { if in.Mode != "cold" || in.PredecessorCertified { return hostclaim.Observed{}, ErrUnsupported } if w.Claims == nil || w.AdmissionEvidence == nil || w.Box == "" || w.Region == "" || observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil || in.Box != w.Box || in.Region != w.Region { return hostclaim.Observed{}, ErrUnsupported } if record, pending := hostclaim.PendingMoveFenceLoss(observed.Claim); pending { return w.admitFencedLoss(ctx, observed, in, baseline, record) } lossID, err := hostclaim.OperationLossID(observed.Claim) if err != nil { return hostclaim.Observed{}, err } if lossID != "" && !hostclaim.OperationLossConsumed(observed.Claim, lossID) { return w.admitLoss(ctx, observed, in, baseline, lossID) } if in.LossID != "" { if lossID == "" || in.LossID != lossID || !hostclaim.OperationLossConsumed(observed.Claim, lossID) { return hostclaim.Observed{}, hostclaim.ErrLossRecord } in.LossID = "" } if baseline != nil && baseline.MaxTXID != in.RemoteMaxTXID { return hostclaim.Observed{}, ErrChanged } if err := w.AdmissionEvidence(ctx, observed, in, baseline); err != nil { return hostclaim.Observed{}, err } // Exactly one attempt. A conflict requires new evidence, not an internal retry. return hostclaim.Update(ctx, w.Claims, observed, hostclaim.AdmitWriter(in)) } func (w Writer) Published(ctx context.Context, observed hostclaim.Observed) (hostclaim.Observed, error) { if w.Claims == nil || w.PublicationEvidence == nil || w.NewID == nil || w.Box == "" || w.Region == "" || observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil || observed.Claim.Op.Exec != w.Box { return hostclaim.Observed{}, ErrUnsupported } if err := w.PublicationEvidence(ctx, observed); err != nil { return hostclaim.Observed{}, err } c := observed.Claim return hostclaim.Update(ctx, w.Claims, observed, hostclaim.PublishGeneration(c.Op.ID, c.Op.Phase, c.Holder.Session, c.Holder.Generation, w.Region, w.NewID)) } // GenerationPrefix is a holder's generation prefix. Twins of its rule: // genselect's prefix and coordinates, and moves.ReplicaSource (which also // covers the root generation); the three change together. func GenerationPrefix(holder hostclaim.Holder) (string, error) { generation := holder.Generation if holder.Session == 0 || generation == "" || generation == hostclaim.RootGeneration || generation == "." || generation == ".." || strings.ContainsAny(generation, "/\\\x00") || holder.Location == "" { return "", ErrUnsupported } return fmt.Sprintf("%s/g-%s/", strings.TrimSuffix(holder.Location, "/"), generation), nil }