Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 real Linux installer lifecycle gates: build x64 rpm/AppImage artifacts,
install and launch the rpm on Fedora 44 under native Wayland, launch the
AppImage outside the build tree on Ubuntu 24.04, uninstall the rpm, and
Expand Down
17 changes: 17 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 19 additions & 33 deletions rivet-cli/codegen/model.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
53 changes: 42 additions & 11 deletions rivet/backend.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
define-event
emit-event!
define-state
state?
state-ref
state-set!
define-record
Expand All @@ -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)
Expand Down Expand Up @@ -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 ...)
Expand Down
34 changes: 14 additions & 20 deletions rivet/scribblings/rivet.scrbl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.}

Expand Down Expand Up @@ -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.}
Expand Down
18 changes: 18 additions & 0 deletions tests/backend.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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) symbol<?)
'(enums events records rpcs states))
(check-equal? (map (lambda (entry) (hash-ref entry 'name))
(hash-ref reflected-schema 'states))
'(counter payload))
(check-equal?
(for/first ([entry (in-list (hash-ref reflected-schema 'rpcs))]
#:when (eq? (hash-ref entry 'name) 'increment))
entry)
(hasheq 'name 'increment
'arguments (list (hasheq 'name 'value 'type 'Int64))
'result 'Int64))

(define-values (server-in client-out) (make-pipe))
(define-values (client-in server-out) (make-pipe))

Expand Down
12 changes: 11 additions & 1 deletion tests/state-backpressure.rkt
Original file line number Diff line number Diff line change
@@ -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-backpressure-progress : Int64)
(define-state state-backpressure-counter : Int64 0)

Expand Down
14 changes: 12 additions & 2 deletions tests/state-cancel-backpressure.rkt
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -114,4 +124,4 @@

(write-frame (frame message:shutdown 0 #"") client-out)
(check-equal? (sync/timeout 2 server-result) 'completed)
(thread-wait server-thread)
(thread-wait server-thread)
4 changes: 3 additions & 1 deletion tests/state-order-lock-init.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading