shop
Deviations of examples/shop from [С V.5]#
The package follows the Standard's "shop" example. Everything below differs from it; each entry says why. State after the stage 4 acceptance (wave 7).
Errata of V.5 approved by the customer: request messages of import proto services flattened into rpc parameters (D-5334, no longer a deviation), the next pipeline step gets the respond value (D-5116/D-5334, no longer a deviation), the cart fix (D-5300: checkout waits for removeOrdered, the background step is recs.learn), trace_sample in the cluster block (D-5310).
Environment overrides of the acceptance (customer decision 13, not source deviations): --seeds static[…] / oxl dev instead of k8s, --tls-cert/--tls-key instead of ACME, a local JWKS (OXL_AUTH_JWKS) instead of the issuer, OXL_SHARDS=16 instead of 256 shards (customer decision 2), APP_*_ADDR for the three fake gRPC servers (tests/shop/fakes), APP_DB_URL=builtin://shop (the dev profile uses sqlite::memory: for unit tests).
Removed since stage 3: config Features is now dynamic Features; the saga talks to the warehouse / payments through import proto clients (dep inventory: Client<Inventory>, provide … for role saga); the full V.5 cluster block (seeds, auth, defaults, trace_sample, roles api/storage/saga/gpu/ml, assign by env else label else auto); cluster.ids.next(); topics OrderEvents (with watch), StatsUpdates, PhotoEvents; @shard on Order; the leader task and the per-node relay in main; the recommendation model is the Client<Recs> of proto/recs.proto.
| # | V.5 | here | reason | status |
|---|---|---|---|---|
| 1 | Client<Recs> as a module dep used by checkout; OutOfStock/Declined visible to tests | reserve(items: …) takes the proto message Line, charge(user:, amount: snap.total.minor); ReserveResp/ChargeResp carry a second field so that r.reservationId / it.receiptId read as in V.5; dep recs is a dep of impl Shop (module-level dep works only in sagas and pipelines, D-5101); the types of .proto files are private to checkout, tests use the aliases OutOfStockError / DeclinedError; rpc names in the .proto files are lower case as V.5 calls them; Money is int64 on the wire; paymentsKey is not sent (Connect has no auth: parameter); the messages are package-visible since D-6108 (tests write ReserveResp{ … }) | import proto types | kept (compiler) |
| 2 | STORAGE_SHARDS: u32 | STORAGE_SHARDS / ORDER_SHARDS (i64): shards n of a role and @shard(.., count: ORDER_SHARDS) want i64 (the constant in @shard(count:) is evaluated since D-5480; the literal 64 was removed, D-7440) | compiler gaps | kept |
| 3 | UserId(ctx.userId()?); caller.isUser(id) with a UserId | callerId(ctx) parses Ctx.userId() (a str, D-2905: the JWT sub) into UserId; AccessDenied if absent or not a number; policies compare the text: caller.isUser("{id}") (users), caller.isUser("{userId}") (Cart, enforced since D-5523) | Ctx.userId() / isUser take str | kept |
| 4 | main: leader stats computes computeStats(&db) and emits StatsUpdates; subscribes in a background task | the leader calls the @internal rpc Orders.publishStats (it owns db) which publishes into the topic StatsUpdates; the per-node subscription is Orders.relayStats() (written when a topic of another module was not importable by name; it is since D-5491) | module-level dep db in main is not implemented | kept |
| 5 | Orders.create: atomic(db) { db.insert(o); emit OrderEvents(…) } (outbox); OrderEvents … sharded by .userId; watch = OrderEvents.subscribe(key: ctx.userId()?, from: .Latest) | create is the V.5 form again (D-5520/D-5521/D-5524: the acknowledged-but-lost orders of D-5488 came from a transaction leaked by a cancelled handler, plus commits across a leader change); the topic is not sharded (a sharded by .field needs a field of the element, an enum has none); watch filters the stream by the owner of the event | enum element cannot be sharded | kept (topic sharding only) |
| 6 | photos: Image from imagekit, BlobStore, makeThumbs/moderate in photos/ml.oxl | the image is bytes (fewer than four bytes is not an image), Blobs is a local actor per node, makeThumbs / moderate are stubs that print photo-step … node= (the acceptance reads it to prove the hop to the gpu / ml nodes); the owner for PhotoReady is the pipeline parameter; decodePhoto is not pure (cluster.ids) | no imagekit | kept |
| 7 | asUser(id) { … } from std.test; mock Inventory {…} / mock Payments {…}; test sim … seeds 1..=300; test integ with cluster(nodes: 3); Shop.addToCart(TEST_ITEM)? inside asUser; asUser(uid) { a -> b }?; node.kill(..) after 50ms | the four V.5 tests are in tests/checkout_test.oxl as written, with mock / S.calls (D-6100..D-6105; S.calls(.m) is the List<Call> of the Standard and .count is the length of a List, D-7250) and the integ test on 3 real node processes (D-6110). Differences: asUser and TEST_ITEM (a static … : frozen Item) are tests/fixtures.oxl; ? inside the asUser block is ! (? would return from the block); the chain is awaited explicitly (await (a -> b)); the sim test runs on 6 logical nodes of the simulation (one per role, two for saga; D-6252) for seeds 1..=300 (stage 5 wave 2: services, saga journal and routing of production on simulated nodes, D-6250..D-6262; the membership and the shared storage are simplified, D-6256/D-6257) and writes node.kill("saga-0", after: 50ms) with std.test. qualified; the other saga tests (out of stock, declined payment) still call the fake gRPC servers (tests/shop/run_shop_unit.py) | ? in a block, chain value is a task | partly fixed (stage 5 waves 1-2) |
| 8 | policy blocks, @owner | written as in V.5; service and actor policies are compiled (D-5010, D-5523); the @owner row filter is applied to every query of a User principal without a bypass role (D-5030; conformance §13.4 owner-filters-rows-of-other-users, D-7190) | — | resolved |
| 9 | fields of pub(api) data written without pub | pub(api) on every field used by another module | fields are private by default (§4.2): erratum of V.5 | erratum |
| 10 | THUMB_SIZES: [u32; 2] = [256, 1024], PhotoId/Name… in model | THUMB_SIZES: List<u32> = [64, 256]; PhotoId lives in photos | small | kept |
| 11 | provide Db = Db.open(App.get().dbUrl); BlobStore; provide Client<Recs> | Db.open(…)!; ActorRef<Blobs> (local actor) instead of BlobStore; profile dev sets dbUrl = "sqlite::memory:" for unit tests (the cluster runs with APP_DB_URL=builtin://shop) | no BlobStore; dev is the default profile | kept |
| 12 | cluster block expose 443 tls(acme: …), seeds k8s(…) | written as in V.5 and overridden by flags / env in the acceptance | not applicable on one machine | environment |
| 13 | no test ab in V.5 (§18.5 names "search v2" only) | ERRATUM V.5 (customer answer 1a, D-6866), now also in the Standard: Shop.search(q) -> List<str>, src/search/search.oxl (variants searchByName / searchByTag, chosen by ab.variant("search v2"), user metric search_found), tests/search_ab_test.oxl (test ab "search v2", guard latency.p95 < SEARCH_P95_LIMIT), traffic/search.oxlrec (400 requests of 200 users, synthesized with scripts/oxlrec.py synth in the format oxl trace --record writes); runs with oxl test examples/shop --kind ab | erratum | erratum |
Not done at all: oxl.lock, --inspect-ui of ./shop --role api, oxl build --image (OCI), k8s/ manifests, oxl debug.
Файлы проекта#
DEVIATIONS.mdoxl.tomlproto/inventory.protoproto/payments.protoproto/recs.protosrc/cluster.oxlsrc/main.oxlsrc/cart/cart.oxlsrc/checkout/checkout.oxlsrc/config/config.oxlsrc/limits/limits.oxlsrc/model/model.oxlsrc/orders/orders.oxlsrc/photos/photos.oxlsrc/search/search.oxlsrc/users/users.oxltests/cart_test.oxltests/checkout_test.oxltests/fake_admin.prototests/fixtures.oxltests/money_test.oxltests/search_ab_test.oxltests/users_test.oxl
Исходник: examples/shop