| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/hex" |
| 6 | "reflect" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | // Parking selectors carry identities only. Root derives all paths, receipts, |
| 11 | // sizes and deadlines independently. |
| 12 | type ServingArtifactIdentity struct { |
| 13 | Host, Box, Region, Incarnation, Location, Generation string |
| 14 | Stamp, Session uint64 |
| 15 | } |
| 16 | |
| 17 | type ServingArtifactDirectory struct { |
| 18 | Device, Inode, Mount uint64 |
| 19 | } |
| 20 | |
| 21 | type ServingLegacyPredecessor struct { |
| 22 | Session uint64 |
| 23 | Generation string |
| 24 | Empty bool |
| 25 | } |
| 26 | |
| 27 | type ServingParkRequest struct { |
| 28 | Successor ServingArtifactIdentity |
| 29 | Predecessor ServingArtifactIdentity |
| 30 | Legacy ServingLegacyPredecessor |
| 31 | Retirement string |
| 32 | } |
| 33 | |
| 34 | type ServingParkResult struct { |
| 35 | TagsFrom uint64 |
| 36 | ID string |
| 37 | Legacy bool |
| 38 | Predecessor ServingArtifactIdentity |
| 39 | OriginalSuccessor ServingArtifactIdentity |
| 40 | Successor ServingArtifactIdentity |
| 41 | Directory ServingArtifactDirectory |
| 42 | } |
| 43 | |
| 44 | // Keep this tuple frozen when the public wire structs grow. |
| 45 | func journalServingParkV4(p *ServingParkRequest) any { |
| 46 | if p == nil { |
| 47 | return nil |
| 48 | } |
| 49 | identity := func(s ServingArtifactIdentity) any { |
| 50 | return []any{s.Host, s.Box, s.Region, s.Incarnation, s.Location, s.Generation, s.Stamp, s.Session} |
| 51 | } |
| 52 | return []any{identity(p.Successor), identity(p.Predecessor), p.Legacy.Session, p.Legacy.Generation, p.Legacy.Empty, p.Retirement} |
| 53 | } |
| 54 | |
| 55 | func ServingParkOperation(op string) bool { |
| 56 | return op == "serving-park" || op == "serving-park-legacy" || op == "serving-park-complete" || op == "serving-park-pressure" |
| 57 | } |
| 58 | |
| 59 | func validateServingParkRequest(q JournalRequest) error { |
| 60 | if q.V != 4 || q.ServingPark == nil || !ServingParkOperation(q.Op) { |
| 61 | return ErrTypedJournal |
| 62 | } |
| 63 | // Reject every field belonging to a different journal operation, including |
| 64 | // fields omitted from v4's hash. Only these selectors are remotely writable. |
| 65 | canonical := JournalRequest{V: q.V, ID: q.ID, Op: q.Op, Nonce: q.Nonce, IssuedAt: q.IssuedAt, ServingPark: q.ServingPark} |
| 66 | if !reflect.DeepEqual(q, canonical) { |
| 67 | return ErrTypedJournal |
| 68 | } |
| 69 | nonce, err := hex.DecodeString(q.Nonce) |
| 70 | if err != nil || len(nonce) != 16 || q.IssuedAt <= 0 { |
| 71 | return ErrTypedJournal |
| 72 | } |
| 73 | if q.ID != journalRequestID(q) { |
| 74 | return ErrChanged |
| 75 | } |
| 76 | p := *q.ServingPark |
| 77 | s := p.Successor |
| 78 | if s.Host == "" || s.Box == "" || s.Region == "" || s.Incarnation == "" || s.Location == "" || s.Generation == "" || s.Stamp == 0 || s.Session != s.Stamp { |
| 79 | return ErrTypedJournal |
| 80 | } |
| 81 | switch q.Op { |
| 82 | case "serving-park": |
| 83 | if p.Legacy != (ServingLegacyPredecessor{}) || p.Retirement != "" { |
| 84 | return ErrTypedJournal |
| 85 | } |
| 86 | case "serving-park-legacy": |
| 87 | if p.Predecessor != (ServingArtifactIdentity{}) || p.Retirement != "" || (p.Legacy.Session == 0) != (p.Legacy.Generation == "") || p.Legacy.Session >= s.Session || (p.Legacy.Empty && p.Legacy.Session != 0) { |
| 88 | return ErrTypedJournal |
| 89 | } |
| 90 | case "serving-park-complete": |
| 91 | id, err := hex.DecodeString(p.Retirement) |
| 92 | if err != nil || len(id) != 32 || p.Predecessor != (ServingArtifactIdentity{}) || p.Legacy != (ServingLegacyPredecessor{}) { |
| 93 | return ErrTypedJournal |
| 94 | } |
| 95 | case "serving-park-pressure": |
| 96 | // Pressure also needs a preadmission observed-claim selector. It remains |
| 97 | // unavailable until the independent sizing and relief mechanism exists. |
| 98 | return ErrUnsupported |
| 99 | } |
| 100 | return nil |
| 101 | } |
| 102 | |
| 103 | type ServingParkClient struct{ Call JournalCall } |
| 104 | |
| 105 | func (c ServingParkClient) request(ctx context.Context, op string, p ServingParkRequest) (ServingParkResult, error) { |
| 106 | if c.Call == nil { |
| 107 | return ServingParkResult{}, ErrUnsupported |
| 108 | } |
| 109 | q, err := journalPrepare(JournalRequest{Op: op, ServingPark: &p}, time.Now()) |
| 110 | if err != nil { |
| 111 | return ServingParkResult{}, err |
| 112 | } |
| 113 | out, err := journalSend(ctx, c.Call, q, false) |
| 114 | if err != nil { |
| 115 | return ServingParkResult{}, err |
| 116 | } |
| 117 | if err = servingParkResponse(q, out); err != nil { |
| 118 | return ServingParkResult{}, err |
| 119 | } |
| 120 | return *out.ServingPark, nil |
| 121 | } |
| 122 | |
| 123 | func (c ServingParkClient) Park(ctx context.Context, successor, predecessor ServingArtifactIdentity) (ServingParkResult, error) { |
| 124 | return c.request(ctx, "serving-park", ServingParkRequest{Successor: successor, Predecessor: predecessor}) |
| 125 | } |
| 126 | |
| 127 | func (c ServingParkClient) ParkLegacy(ctx context.Context, successor ServingArtifactIdentity, predecessor ServingLegacyPredecessor) (ServingParkResult, error) { |
| 128 | return c.request(ctx, "serving-park-legacy", ServingParkRequest{Successor: successor, Legacy: predecessor}) |
| 129 | } |
| 130 | |
| 131 | func (c ServingParkClient) Complete(ctx context.Context, id string, successor ServingArtifactIdentity) error { |
| 132 | _, err := c.request(ctx, "serving-park-complete", ServingParkRequest{Successor: successor, Retirement: id}) |
| 133 | return err |
| 134 | } |
| 135 | |
| 136 | func (c ServingParkClient) ReplayUncertain(ctx context.Context, uncertain *JournalUncertainError) (JournalResponse, error) { |
| 137 | if c.Call == nil || uncertain == nil || validateServingParkRequest(uncertain.Request) != nil { |
| 138 | return JournalResponse{}, ErrUnsupported |
| 139 | } |
| 140 | out, err := journalSend(ctx, c.Call, uncertain.Request, true) |
| 141 | if err == nil { |
| 142 | err = servingParkResponse(uncertain.Request, out) |
| 143 | } |
| 144 | return out, err |
| 145 | } |
| 146 | |
| 147 | func servingParkResponse(q JournalRequest, out JournalResponse) error { |
| 148 | p := q.ServingPark |
| 149 | if p == nil || out.ServingPark == nil || out.ServingPark.Successor != p.Successor || out.ServingPark.ID == "" || (p.Retirement != "" && out.ServingPark.ID != p.Retirement) { |
| 150 | return &JournalUncertainError{Request: q, Cause: ErrJournalUncertain} |
| 151 | } |
| 152 | return nil |
| 153 | } |
| 154 | |