| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "github.com/carlosframework/platform/internal/park" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/genselect" |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | ) |
| 13 | |
| 14 | // Writer supplies transition adapters, not stop, certificate or marker evidence. |
| 15 | type Writer struct { |
| 16 | HomeRegion string |
| 17 | Loss *LossAdmission |
| 18 | Claims release.Store |
| 19 | Data genselect.Reader |
| 20 | Bucket, Region, Box string |
| 21 | AdmissionEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error |
| 22 | PublicationEvidence func(context.Context, hostclaim.Observed) error |
| 23 | NewID func() string |
| 24 | } |
| 25 | |
| 26 | func (w Writer) Baseline(ctx context.Context, observed hostclaim.Observed) (*genselect.Plan, error) { |
| 27 | if w.Data == nil || w.Bucket == "" || w.Region == "" || observed.Absent || observed.Claim == nil || observed.Claim.Holder == nil { |
| 28 | return nil, ErrUnsupported |
| 29 | } |
| 30 | 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)}) |
| 31 | } |
| 32 | |
| 33 | func (w Writer) Admit(ctx context.Context, observed hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) (hostclaim.Observed, error) { |
| 34 | if in.Mode != "cold" || in.PredecessorCertified { |
| 35 | return hostclaim.Observed{}, ErrUnsupported |
| 36 | } |
| 37 | 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 { |
| 38 | return hostclaim.Observed{}, ErrUnsupported |
| 39 | } |
| 40 | if record, pending := hostclaim.PendingMoveFenceLoss(observed.Claim); pending { |
| 41 | return w.admitFencedLoss(ctx, observed, in, baseline, record) |
| 42 | } |
| 43 | lossID, err := hostclaim.OperationLossID(observed.Claim) |
| 44 | if err != nil { |
| 45 | return hostclaim.Observed{}, err |
| 46 | } |
| 47 | if lossID != "" && !hostclaim.OperationLossConsumed(observed.Claim, lossID) { |
| 48 | return w.admitLoss(ctx, observed, in, baseline, lossID) |
| 49 | } |
| 50 | if in.LossID != "" { |
| 51 | if lossID == "" || in.LossID != lossID || !hostclaim.OperationLossConsumed(observed.Claim, lossID) { |
| 52 | return hostclaim.Observed{}, hostclaim.ErrLossRecord |
| 53 | } |
| 54 | in.LossID = "" |
| 55 | } |
| 56 | if baseline != nil && baseline.MaxTXID != in.RemoteMaxTXID { |
| 57 | return hostclaim.Observed{}, ErrChanged |
| 58 | } |
| 59 | if err := w.AdmissionEvidence(ctx, observed, in, baseline); err != nil { |
| 60 | return hostclaim.Observed{}, err |
| 61 | } |
| 62 | // Exactly one attempt. A conflict requires new evidence, not an internal retry. |
| 63 | return hostclaim.Update(ctx, w.Claims, observed, hostclaim.AdmitWriter(in)) |
| 64 | } |
| 65 | |
| 66 | func (w Writer) Published(ctx context.Context, observed hostclaim.Observed) (hostclaim.Observed, error) { |
| 67 | 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 { |
| 68 | return hostclaim.Observed{}, ErrUnsupported |
| 69 | } |
| 70 | if err := w.PublicationEvidence(ctx, observed); err != nil { |
| 71 | return hostclaim.Observed{}, err |
| 72 | } |
| 73 | c := observed.Claim |
| 74 | 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)) |
| 75 | } |
| 76 | |
| 77 | // GenerationPrefix is a holder's generation prefix. Twins of its rule: |
| 78 | // genselect's prefix and coordinates, and moves.ReplicaSource (which also |
| 79 | // covers the root generation); the three change together. |
| 80 | func GenerationPrefix(holder hostclaim.Holder) (string, error) { |
| 81 | generation := holder.Generation |
| 82 | if holder.Session == 0 || generation == "" || generation == hostclaim.RootGeneration || generation == "." || generation == ".." || strings.ContainsAny(generation, "/\\\x00") || holder.Location == "" { |
| 83 | return "", ErrUnsupported |
| 84 | } |
| 85 | return fmt.Sprintf("%s/g-%s/", strings.TrimSuffix(holder.Location, "/"), generation), nil |
| 86 | } |
| 87 | |