From cd3bf35894b836015d62bf841e4fd20707a40cd8 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Sat, 10 Oct 2026 14:30:59 +0800 Subject: [PATCH] backend: make schema reflection data-only --- CHANGELOG.md | 5 +++ docs/architecture.md | 17 +++++++++ rivet-cli/codegen/model.rkt | 52 +++++++++++----------------- rivet/backend.rkt | 53 +++++++++++++++++++++++------ rivet/scribblings/rivet.scrbl | 34 ++++++++---------- tests/backend.rkt | 18 ++++++++++ tests/state-backpressure.rkt | 12 ++++++- tests/state-cancel-backpressure.rkt | 14 ++++++-- tests/state-order-lock-init.rkt | 4 ++- 9 files changed, 141 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6c01b7..c01a5747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Replace the accidental public backend registry structs and mutable State + cell accessors with an opaque `state?` handle and a single immutable + `backend-schema` reflection snapshot. Pre-1.0 tooling that used + `registered-*` or `*-info-*` should read the corresponding schema entry; + applications should use `state-ref` and `state-set!`. - Add first-party portable update installation for Windows, macOS, and Linux: verified ZIP/AppImage payloads are staged, platform-signature checked, atomically replaced, health-gated, durably committed, and recoverable after diff --git a/docs/architecture.md b/docs/architecture.md index 18a1b9ea..ffa02c38 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -130,6 +130,23 @@ The Racket server creates a custodian and lightweight Racket thread for each req Cancellation shuts down the request custodian and responds with an Error frame. Native callers never receive a raw Racket exception; errors cross the protocol as data. +## Racket backend boundary + +`rivet/backend` has a deliberately small application surface: declaration +forms, Event/State operations, `serve`/`serve-fds`, diagnostics, and schema +snapshots. The registry structs, RPC procedures, mutable State cells, locks, +and request custodians are runtime-owned implementation details. + +Build tools load the application in a fresh namespace and call +`backend-schema`. That one immutable, data-only snapshot is the supported +reflection boundary; generators do not reach into runtime structs. This keeps +code generation deterministic and lets the server's concurrency machinery +evolve without turning internal representation into an accidental API. + +Before 1.0, callers that used `registered-*` or `*-info-*` should migrate to +the corresponding `backend-schema` entry. Application State access should use +`state-ref` and `state-set!`; `state?` is available when a predicate is needed. + ## Data ownership No native language binding retains a raw Racket value across calls. This avoids coupling ordinary UI code to Chez Scheme's collector and object movement rules. diff --git a/rivet-cli/codegen/model.rkt b/rivet-cli/codegen/model.rkt index 00411760..b3c16739 100644 --- a/rivet-cli/codegen/model.rkt +++ b/rivet-cli/codegen/model.rkt @@ -26,40 +26,26 @@ (define ns (make-base-namespace)) (parameterize ([current-namespace ns]) (dynamic-require backend #f) - (define get-rpcs (dynamic-require 'rivet/backend 'registered-rpcs)) - (define rpc-name (dynamic-require 'rivet/backend 'rpc-info-name)) - (define rpc-arg-names (dynamic-require 'rivet/backend 'rpc-info-arg-names)) - (define rpc-arg-types (dynamic-require 'rivet/backend 'rpc-info-arg-types)) - (define rpc-result-type (dynamic-require 'rivet/backend 'rpc-info-result-type)) - (define get-events (dynamic-require 'rivet/backend 'registered-events)) - (define event-name (dynamic-require 'rivet/backend 'event-info-name)) - (define event-type (dynamic-require 'rivet/backend 'event-info-type)) - (define get-states (dynamic-require 'rivet/backend 'registered-states)) - (define state-name (dynamic-require 'rivet/backend 'state-info-name)) - (define state-type (dynamic-require 'rivet/backend 'state-info-type)) - (define get-records (dynamic-require 'rivet/backend 'registered-records)) - (define record-name (dynamic-require 'rivet/backend 'record-info-name)) - (define record-field-names (dynamic-require 'rivet/backend 'record-info-field-names)) - (define record-field-types (dynamic-require 'rivet/backend 'record-info-field-types)) - (define get-enums (dynamic-require 'rivet/backend 'registered-enums)) - (define enum-name (dynamic-require 'rivet/backend 'enum-info-name)) - (define enum-cases (dynamic-require 'rivet/backend 'enum-info-cases)) + (define get-schema (dynamic-require 'rivet/backend 'backend-schema)) + (define schema (get-schema)) (values - (for/list ([info (in-list (get-rpcs))]) - (schema-rpc (rpc-name info) - (rpc-arg-names info) - (rpc-arg-types info) - (rpc-result-type info))) - (for/list ([info (in-list (get-events))]) - (schema-event (event-name info) (event-type info))) - (for/list ([info (in-list (get-states))]) - (schema-state (state-name info) (state-type info))) - (for/list ([info (in-list (get-records))]) - (schema-record (record-name info) - (record-field-names info) - (record-field-types info))) - (for/list ([info (in-list (get-enums))]) - (schema-enum (enum-name info) (enum-cases info)))))) + (for/list ([info (in-list (hash-ref schema 'rpcs))]) + (define arguments (hash-ref info 'arguments)) + (schema-rpc (hash-ref info 'name) + (map (lambda (argument) (hash-ref argument 'name)) arguments) + (map (lambda (argument) (hash-ref argument 'type)) arguments) + (hash-ref info 'result))) + (for/list ([info (in-list (hash-ref schema 'events))]) + (schema-event (hash-ref info 'name) (hash-ref info 'type))) + (for/list ([info (in-list (hash-ref schema 'states))]) + (schema-state (hash-ref info 'name) (hash-ref info 'type))) + (for/list ([info (in-list (hash-ref schema 'records))]) + (define fields (hash-ref info 'fields)) + (schema-record (hash-ref info 'name) + (map (lambda (field) (hash-ref field 'name)) fields) + (map (lambda (field) (hash-ref field 'type)) fields))) + (for/list ([info (in-list (hash-ref schema 'enums))]) + (schema-enum (hash-ref info 'name) (hash-ref info 'cases)))))) (define (schema-name value) (if (symbol? value) (symbol->string value) value)) diff --git a/rivet/backend.rkt b/rivet/backend.rkt index f9f5cfa0..be0a7e50 100644 --- a/rivet/backend.rkt +++ b/rivet/backend.rkt @@ -12,6 +12,7 @@ define-event emit-event! define-state + state? state-ref state-set! define-record @@ -21,27 +22,23 @@ serve serve-fds current-rivet-diagnostic-sink - registered-rpcs - registered-events - registered-states - registered-records - registered-enums + backend-schema rpc-schema event-schema state-schema record-schema - enum-schema - (struct-out rpc-info) - (struct-out event-info) - (struct-out state-info) - (struct-out record-info) - (struct-out enum-info)) + enum-schema) (struct rpc-info (name arg-names arg-types result-type procedure) #:transparent) (struct event-info (name type) #:transparent) (struct state-info (name type cell lock) #:transparent) +;; State descriptors are deliberately opaque at the public boundary. The +;; mutable cell and synchronization primitives are runtime implementation +;; details; applications only need a predicate plus state-ref/state-set!. +(define (state? value) (state-info? value)) + (struct pending-request (custodian terminal-owned cancel-deferred cancel-requested) #:mutable) (struct record-info (name field-names field-types) #:transparent) (struct record-value (name fields) #:transparent) @@ -659,6 +656,40 @@ (hasheq 'name (symbol->string (enum-info-name info)) 'cases (map symbol->string (enum-info-cases info))))) +;; Code generation consumes one immutable, data-only snapshot instead of +;; depending on Rivet's private registry structs and their mutable State cells. +;; Keeping the descriptor values as symbols and type s-expressions avoids the +;; lossy display formatting used by the human-facing `*-schema` procedures. +(define (backend-schema) + (hasheq + 'rpcs + (for/list ([info (in-list (registered-rpcs))]) + (hasheq 'name (rpc-info-name info) + 'arguments + (for/list ([name (in-list (rpc-info-arg-names info))] + [type (in-list (rpc-info-arg-types info))]) + (hasheq 'name name 'type type)) + 'result (rpc-info-result-type info))) + 'events + (for/list ([info (in-list (registered-events))]) + (hasheq 'name (event-info-name info) + 'type (event-info-type info))) + 'states + (for/list ([info (in-list (registered-states))]) + (hasheq 'name (state-info-name info) + 'type (state-info-type info))) + 'records + (for/list ([info (in-list (registered-records))]) + (hasheq 'name (record-info-name info) + 'fields + (for/list ([name (in-list (record-info-field-names info))] + [type (in-list (record-info-field-types info))]) + (hasheq 'name name 'type type)))) + 'enums + (for/list ([info (in-list (registered-enums))]) + (hasheq 'name (enum-info-name info) + 'cases (enum-info-cases info))))) + (define-syntax define-rpc (syntax-rules (:) [(_ (name [arg : arg-type] ... : result-type) body ...) diff --git a/rivet/scribblings/rivet.scrbl b/rivet/scribblings/rivet.scrbl index b61592ff..77e759c0 100644 --- a/rivet/scribblings/rivet.scrbl +++ b/rivet/scribblings/rivet.scrbl @@ -98,24 +98,10 @@ packaged dependency-closure, and clean-machine checks. @defthing[Bytes rivet-type?] @defthing[List rivet-type?] -@defstruct*[state-info - ([name symbol?] - [type any/c] - [cell box?] - [lock semaphore?])]{ -Represents a registered shared state value. Applications normally create one -with @racket[define-state] instead of calling the constructor directly.} - -@defstruct*[record-info - ([name symbol?] - [field-names list?] - [field-types list?])]{ -Represents the ordered schema of a named Record. Applications normally create -one with @racket[define-record] instead of calling the constructor directly.} - -@defstruct*[enum-info ([name symbol?] [cases list?])]{ -Represents the ordered cases of a named Enum. Applications normally create one -with @racket[define-enum] instead of calling the constructor directly.} +State descriptors and the registry records behind RPCs, Events, Records, and +Enums are intentionally opaque. Applications declare them with the forms in +this section; code generators consume the immutable @racket[backend-schema] +snapshot instead of depending on mutable runtime representation. @defform[(define-rpc (name [arg : type] ... : result-type) body ...)]{ Defines a Racket procedure named @racket[name] and registers it as an RPC for @@ -157,9 +143,12 @@ state event. State values cannot have type @racket[Void].} Emits an already registered event value on the active server connection. The procedure reports an error when no server is active on the current thread.} -@defproc[(state-ref [state state-info?]) any/c]{Returns the current state value.} +@defproc[(state? [value any/c]) boolean?]{Reports whether @racket[value] is an +opaque State descriptor produced by @racket[define-state].} + +@defproc[(state-ref [state state?]) any/c]{Returns the current state value.} -@defproc[(state-set! [state state-info?] [value any/c]) void?]{ +@defproc[(state-set! [state state?] [value any/c]) void?]{ Validates and atomically commits a state value, then admits its native event in the same update order.} @@ -190,6 +179,11 @@ backpressure.} void?]{ Adapts native file descriptors to binary ports and calls @racket[serve].} +@defproc[(backend-schema) hash?]{Returns one immutable, data-only snapshot with +@racket['rpcs], @racket['events], @racket['states], @racket['records], and +@racket['enums] entries. This is the supported reflection boundary for code +generation and tooling; registry structs and mutable State cells are private.} + @defproc[(rpc-schema) list?]{Returns the registered RPC schema used by code generation.} @defproc[(event-schema) list?]{Returns the registered event schema.} @defproc[(state-schema) list?]{Returns the registered state schema.} diff --git a/tests/backend.rkt b/tests/backend.rkt index 23d9148e..110dd7c7 100644 --- a/tests/backend.rkt +++ b/tests/backend.rkt @@ -70,11 +70,29 @@ (check-equal? (state-schema) (list (hasheq 'name "counter" 'type "Int64") (hasheq 'name "payload" 'type "Any"))) +(check-true (state? counter)) +(check-false (state? 10)) (check-equal? (state-ref counter) 10) (check-equal? (state-ref payload) "initial") (check-exn exn:fail? (lambda () (state-set! counter "wrong-type"))) +;; Tooling gets immutable data, never registry structs or mutable State cells. +(define reflected-schema (backend-schema)) +(check-true (immutable? reflected-schema)) +(check-equal? (sort (hash-keys reflected-schema) symbolnamespace (module-path-index-resolve backend-module))) +(define state-info-cell + (parameterize ([current-namespace backend-namespace]) + (eval 'state-info-cell))) + (define-event state-backpressure-progress : Int64) (define-state state-backpressure-counter : Int64 0) diff --git a/tests/state-cancel-backpressure.rkt b/tests/state-cancel-backpressure.rkt index 2443abde..3d9380c9 100644 --- a/tests/state-cancel-backpressure.rkt +++ b/tests/state-cancel-backpressure.rkt @@ -1,9 +1,19 @@ #lang racket/base -(require rackunit +(require racket/runtime-path + rackunit "../rivet/backend.rkt" "../rivet/protocol.rkt") +;; Raw-cell observation is intentionally limited to this white-box ordering +;; test; state descriptors are opaque to application code. +(define-runtime-module-path-index backend-module "../rivet/backend.rkt") +(define backend-namespace + (module->namespace (module-path-index-resolve backend-module))) +(define state-info-cell + (parameterize ([current-namespace backend-namespace]) + (eval 'state-info-cell))) + (define-event state-cancel-progress : Int64) (define-state state-cancel-counter : Int64 0) @@ -114,4 +124,4 @@ (write-frame (frame message:shutdown 0 #"") client-out) (check-equal? (sync/timeout 2 server-result) 'completed) -(thread-wait server-thread) \ No newline at end of file +(thread-wait server-thread) diff --git a/tests/state-order-lock-init.rkt b/tests/state-order-lock-init.rkt index 2aebb403..e9e72f09 100644 --- a/tests/state-order-lock-init.rkt +++ b/tests/state-order-lock-init.rkt @@ -21,9 +21,11 @@ (backend-private 'state-update-lock)) (define state-update-locks-lock (backend-private 'state-update-locks-lock)) +(define make-state-info + (backend-private 'state-info)) (define state - (state-info 'state-order-lock-init 'Int64 (box 0) (make-semaphore 1))) + (make-state-info 'state-order-lock-init 'Int64 (box 0) (make-semaphore 1))) (define worker-count 32) (define ready (make-channel)) (define results (make-async-channel))