Skip to content

Latest commit

 

History

History
281 lines (230 loc) · 18.3 KB

configuration.md

File metadata and controls

281 lines (230 loc) · 18.3 KB

Configuration reference

configure(options) creates a runtime and installs it as the default used by Actor.ref(). createRuntime(options) creates an isolated runtime addressed through runtime.ref(ActorClass, actorId). Both validate options immediately.

Core runtime options

Option Default Contract
database required A Database adapter.
tableNamePrefix "solid_objects_" Lowercase letters, digits, and underscores; must start with a letter.
pollingIntervalMilliseconds 100 Positive durable-work polling interval.
idlePollingIntervalMilliseconds 1_000 Positive ceiling after consecutive empty polling passes.
syncPollingIntervalMilliseconds 50 Positive result-wait polling interval.
leaseDurationMilliseconds 30_000 Positive activation lease; must exceed renewal interval.
leaseRenewalIntervalMilliseconds 10_000 Positive activation renewal cadence.
idleDeactivationTimeoutMilliseconds 30_000 Non-negative hydrated activation cache lifetime; 0 disables idle retention.
maxMailboxLength 10_000 Positive maximum ready and claimed messages for one actor.
maxPayloadBytes 1_048_576 Positive byte limit for operation arguments and personalized payloads.
maxStateBytes 5_242_880 Positive persisted actor-state byte limit.
warnStateBytes 131_072 Positive persisted actor-state byte threshold that reports one warning.
maxResultBytes 1_048_576 Positive operation-result byte limit.
maxAttempts 5 Positive attempt limit evaluated on handler failure.
maxMessagesPerActivationPass 50 Positive integer turn budget before fairness yield.
maxActivationDurationMilliseconds 5_000 Positive elapsed-time budget before fairness yield.
claimScanLimit 100 Positive integer ready-actor candidates considered per global claim.
retryDelayMilliseconds exponential 1-60 seconds Receives a retry index; returns milliseconds (see below).

The limits measure normalized JSON encoded as UTF-8. maxResultBytes also limits each computed snapshot getter and effect-handler result. Explicit observables() values are validated as JSON but do not currently have a separate aggregate byte limit. This includes values wrapped in broadcastInvalidation(): they are compared in memory even though only their names enter the broadcast outbox. retryDelayMilliseconds should return a non-negative finite number; an invalid application callback will fail the affected failure path rather than schedule an invalid timestamp.

Interrupted actor messages use the existing retryDelayMilliseconds callback on recovery. After the first claimed attempt they are immediately eligible; after persisted attempts 2, 3, 4, ... the callback receives 1, 2, 3, ... . Its default delays are 1, 2, 4 seconds up to 60 seconds. This is the total claim count, including earlier handler failures, not a separate lease-loss counter. The availability timestamp is stored transactionally in ready membership, survives worker replacement and stale-process cleanup, and keeps the message ahead of later messages for that actor. Other actors remain eligible. Custom delays, including zero, remain authoritative.

Lease loss alone never creates a dead letter, including with maxAttempts: 1. A handler failure still uses the full attempt count with maxAttempts and passes that count to retryDelayMilliseconds, as before. Applications needing a hard retry or elapsed-time bound must observe activation.lost and stop or intervene. No setting or schema change is required.

maxStateBytes is a hard limit that fails the turn. warnStateBytes is a soft threshold that keeps the turn. A commit above the threshold reports one solid_objects.state.large instrumentation event with the actor type, the actor ID, the byte count, and the threshold. The runtime reports it only after the commit succeeds, so a turn that rolls back stays silent. The event holds no application state. The runtime measures the size only when an instrumentation callback is configured. Throughput falls as the persisted state grows, so treat the warning as an instruction to divide the actor. See State size and throughput.

Runtime roles and supervision

Option Default Contract
workerCount 1 Non-negative actor workers.
effectWorkerCount 1 Non-negative effect workers.
broadcastWorkerCount 1 Non-negative broadcast workers when realtime is enabled.
reminderSchedulerCount 1 Non-negative reminder schedulers.
processHeartbeatIntervalMilliseconds 15_000 Positive persisted heartbeat cadence.
processAliveThresholdMilliseconds 60_000 Positive age after which an owner is stale.
shutdownTimeoutMilliseconds 15_000 Positive shared graceful-shutdown budget.
supervisorRestartDelayMilliseconds 100 Positive initial failed-role replacement delay.
supervisorMaximumRestartDelayMilliseconds 10_000 Positive cap no smaller than the initial delay.
wakeUp "automatic" A name or an adapter implementing watch, notify, and close.
logger console methods Structured debug, info, warn, and error sink.

Counts may be zero, but the complete configuration must leave at least one runtime role enabled. Broadcast workers are started only when broadcast or authorizeSubscription is configured.

pollingIntervalMilliseconds is the fast interval after work or a wake-up. Consecutive empty passes double it up to idlePollingIntervalMilliseconds. Actor workers never wait longer than leaseRenewalIntervalMilliseconds. Set the fast and idle values equal for a fixed cadence. A custom wake-up adapter must return true for a notification and false for a timeout. An older adapter that returns void remains compatible and keeps the fast cadence. Wake-ups reduce latency, while database polling remains the correctness path.

Busy SQL workers and testing drains give the event loop a turn between passes. In Node this lets timers and input run even with synchronous SQLite calls. An instrumentation callback can abort runtime.run() before its next pass. This does not interrupt a synchronous handler: responsiveness is bounded by one pass, or by a single overlong handler that must return first.

Retention and cleanup

Option Default Contract
retentionIntervalMilliseconds 3_600_000 Non-negative automatic history-pruning cadence; 0 disables it.
deadProcessCleanupIntervalMilliseconds 60_000 Non-negative stale-owner recovery cadence; 0 disables it.
messageRetentionMilliseconds 30 days Positive default completed-message retention.
messageRetentionByActorType {} Positive per-type message-retention overrides.
instanceRetentionByActorType {} Positive per-type instance-expiration opt-ins.
processRetentionMilliseconds 7 days Positive stopped-process retention.
pruneBatchSize 1_000 Positive integer maximum rows rechecked per pruning transaction.
retainedIdempotencyKeys 64 Positive integer idempotency keys an actor remembers.
retainedIdempotencyKeysBytes 16_384 Positive integer serialized size those keys must fit.
redriveBatchSize 100 Positive integer rows a redrive moves per transaction.
redriveBatchPauseMilliseconds 50 Non-negative pause between redrive batches.
administrationIdentity String(context) Names the operator recorded in an administration event.

Automatic retention prunes messages and stopped process records. Actor instance expiration remains an explicit administration action even when a type opts in.

Authorization, instrumentation, and broadcast

All authorization callbacks default to false:

  • authorizeMessage({ actorType, actorId, operation, arguments, authorizationContext })
  • authorizeQuery({ actorType, actorId, operation, arguments, authorizationContext })
  • authorizeDestroy({ actorType, actorId, authorizationContext })
  • authorizeAdministration({ action, resource, resourceId, authorizationContext })
  • authorizeSubscription({ actorType, actorId, authorizationContext })

Callbacks may return a boolean or promise. Authorization contexts are application-owned server values and are never persisted. See Authorization for the entry-point matrix.

instrumentation(event) is synchronous. The runtime freezes metadata-only events, catches sink failures, and reports the sink error through logger.

broadcast(event) forwards committed realtime invalidations through an application-owned shared transport. A receiving process calls runtime.realtime.publish(event). It is unnecessary for a single-process server.

Database adapters

Database value mapping

DatabaseConnection.get<Row>() and all<Row>() trust the caller-supplied row type; they do not validate or convert driver results. Type rows to the adapter's runtime representation or normalize them at the application boundary.

SQL value SQLite (node:sqlite) PostgreSQL (pg) MySQL (mysql2)
ordinary integer bigint number for int2/int4; bigint for int8 number for ordinary integer types
arbitrary precision integer/decimal bigint for SQLite INTEGER numeric remains the driver's decimal string BIGINT and DECIMAL are strings
floating point number number number
text string string string
binary Uint8Array Buffer Buffer
null null null null

RunResult.changes is always a number. lastInsertId, when an adapter provides it, is a decimal string; SQLite and MySQL set it for nonzero generated IDs, while PostgreSQL callers should use RETURNING with get().

SQLite

sqlite({ path, timeoutMilliseconds = 5_000, lockRetryAttempts = 10, synchronous = "normal" }) uses Node's built-in node:sqlite. lockRetryAttempts is a positive integer. The timeout controls SQLite's busy timeout; transaction acquisition retries use short capped backoff only when no synchronous deadline is active.

A file database uses write-ahead logging (WAL). On its first access, the adapter sets journal_mode = WAL and the synchronous level, with the same retries as a transaction. WAL keeps two more files next to the database: <path>-wal and <path>-shm. Committed data can be in the -wal file, so a copy of the database file alone can miss it. To back up a database that is in use, run VACUUM INTO '<backup path>' or use the SQLite backup command. Copy the files only when no process has the database open. An in-memory database (":memory:") keeps its memory journal.

synchronous is "normal" or "full":

  • "normal", the default, is the level that the Ruby gem gets from Active Record. A commit makes almost no fsync call. A process crash loses no commit. A power loss or an operating system crash can lose the last commits, but the database stays consistent.
  • "full" flushes each commit to disk before the commit returns. A commit costs about one fsync call, and node:sqlite blocks the event loop while it waits for the disk.

PostgreSQL

postgresql(options) accepts:

Option Default
connectionString required
maximumConnections pg default
idleTimeoutMilliseconds pg default
connectionTimeoutMilliseconds pg default
applicationName "solid-objects"
onPoolError structured console.error

wakeUp takes a name or an adapter, as config.cache_store does in Rails. "automatic" is the default. It prefers SOLID_OBJECTS_REDIS_URL, then PostgreSQL notifications, then polling. "in_process" opts out, "postgresql" and "redis" force one, and an unknown name throws rather than polls quietly.

A requested adapter that the environment cannot provide polls instead and says so. "postgresql" on a database with no notification channel, "redis" without SOLID_OBJECTS_REDIS_URL, and a Redis URL without the redis package each log solid_objects.wake_up.unavailable once and report the reason in the capability. Only a name that does not exist is refused, because a typo cannot be honoured at all.

Selection runs once per runtime, on first use. runtime.wakeUpCapability() reports what it chose, whether that choice crosses processes, its measured floor in milliseconds, and why. The doctor reports the same record, and the polling-only warning fires on what was installed rather than on whether a setting was set. An adapter that declares its own defaultCapability keeps it, so a configured InProcessWakeUpAdapter still warns.

Automatic selection proves the PostgreSQL path before it chooses it. It listens on a probe channel, sends one NOTIFY from a second connection, and waits up to two seconds for it to arrive. A probe that does not deliver falls back to polling and warns once, because LISTEN does not survive a transaction pooler such as PgBouncer.

database.wakeUp(options) creates a dedicated notification adapter using the same connection string. Its options are channelPrefix = "solid_objects", applicationName = "solid-objects-wake-up", and onListenerError. Use a direct or session-pooled connection because PostgreSQL LISTEN is session-scoped.

postgresqlWakeUp({ connectionString, ...options }) constructs that adapter without a database instance. The adapter translates portable ? parameters; write ?? in SQL passed through DatabaseConnection when PostgreSQL needs the literal JSON existence operator.

MySQL

mysql(options) accepts:

Option Default
connectionString required
maximumConnections 10
idleTimeoutMilliseconds 60_000
queueLimit 0 (unbounded driver queue)

Counts are non-negative except maximumConnections, which is positive. Tables use InnoDB. mysqlSql(sql) translates the limited portable conflict syntax used by database integrations into MySQL syntax.

Redis wake-up

redisWakeUp({ url, channelPrefix = "solid_objects", connectionTimeoutMilliseconds = 1_000, onError }) uses separate lazy publisher and subscriber clients. Connection, subscription, and publication failures are reported and bounded; database polling continues.

Lifecycle order

Call runtime.install() before serving traffic or starting roles. Run the supervisor with an application-owned abort signal. To shut down, abort, await runtime.run(), then close adapters through runtime.close():

const controller = new AbortController()
const running = runtime.run(controller.signal)

controller.abort()
await running
await runtime.close()

Calling close() while run() is active raises an error so database and wake-up connections cannot disappear beneath live roles.