Repository navigation
v0.10.0
The third release under the plan of plans: a PostgreSQL job queue with cron, a broker-agnostic
messaging API with a transactional outbox and Redis Streams, mail with templates and delivery
through the queue, object storage, a published event-sourcing crate, and an operator console;
plus HTTP client defaults, tracing of outbound calls and queries, log redaction, config hot
reload, fault injection, id-effect dev and id-effect generate job. The Ledger reference app
uses all of it in CI. Upgrading from 0.9: see the
0.10 migration guide.
Added
- New crates (ADR 0010 Amendment 6):
id_effect_jobs,id_effect_messaging,id_effect_mail,
id_effect_console,id_effect_storage(tierplatform; the Ledger is their CI-tested
consumer) andid_effect_events, promoted fromlabs/(tierintegration). id_effect_jobs:Jobtrait (payloadSchema,NAME,QUEUE, priority, max attempts,
unique key, timeout, retrySchedule),JobRegistry,JobQueue(enqueue,cancel,
retry_now,stats,list) andJobStorewithPgJobQueueandMemoryJobQueue.
enqueue_in(&tx, …)enqueues inside the caller's transaction; a unique-key duplicate never
aborts it. Oneid_effect_jobstable, shipped asmigrator()orensure_schema().id_effect_jobs:Workerclaiming withFOR UPDATE SKIP LOCKEDby priority thenrun_at,
per-queue concurrency, lease heartbeats and rescue of expired leases, retries replayed from the
job'sSchedule, dead jobs after the last attempt, graceful drain,JobObserverhooks; "now"
comes from the app's clock. Book part6/ch31-01-jobs.md; example010_jobs.id_effect_jobsfeaturecron:enqueue_on_tick(one job per tick, deduplicated by
tick_keyacross schedulers) andCronLeader, which runs the schedule on one node through a
ClusterSingletonoverPgAdvisoryLease(re-exported withNodeName/Pid). Core
id_effect: additiveCronJob::new_at, whose factory receives the fire time.id_effect_messaging: object-safeMessageBroker(publish,subscribeper
ConsumerGroupwith prefetch, concurrency, max deliveries, dead-letter topic and start
position);Deliverysettled withack/nack(Requeue | DeadLetter);Messagewith id, key,
headers and aSchema-typed payload;MessageBrokerExt::consume,ack_on_success;
MemoryBrokerwith test helpers. Book part6/ch31-02-messaging.md.id_effect_messaging: transactionalOutbox::stage(&tx, …), aRelayto any broker
(SKIP LOCKEDclaims under a lease, retries on aSchedule, dead rows, pruning; at-least-once)
and an idempotentInboxdeduplicating on (consumer group, message id) in the handler's
transaction;PgOutbox,MemoryOutbox,MemoryInbox,migrator(). Example030_outbox_inbox.id_effect_redisfeaturestreams:RedisStreamsBroker, aMessageBrokeron Redis Streams
(≥ 6.2): consumer groups,XAUTOCLAIMredelivery, prefetch credit, requeue, dead-lettering
that cannot run twice. Example030_streams; book ch28-04.id_effect_mail:Message/MessageBuilder(attachments, inlinecid:parts, header
injection refused), theMailercapability,SmtpMailer(lettre, rustls),SendGridMailer
(POST never retried) andTestMailboxwith assertions;MailError::is_transient.id_effect_mail: minijinja templates ({mail}.html/.txt/.subject.txt, layouts and
partials, HTML autoescaped); featurepreviewserves/_mail/previews; featurejobsadds
deliver_later/deliver_later_in(&tx, …)throughid_effect_jobs(12 attempts over ~5 h,
permanent failures go dead at once, payloads capped at 2 MiB). Book part6/ch31-03-mail.md.id_effect_console: guarded operator console.Console::new()….router(guard)is the only way
to get its routes; JSON API under/_console/apifor fibers, supervisors, metrics, health and
the runtime log level (GET / PUT); a static dashboard page at/_consoleunder a strict CSP;
featurejobsadds a jobs panel with retry / cancel actions. Book part6/ch27-03-console.md.id_effect_storage:ObjectStorecapability (put,put_stream,get,get_stream,
head,exists,delete,list,signed_url) withMemoryStore,LocalStoreand
S3Store(features3); streaming uploads hold at most one part; HMACUrlSignerfor the
local backends; re-exportsbytes::Bytes. Book part6/ch28-05-storage.md.id_effect_events(published):EventStore::append_expectedwith optimistic concurrency,
SqlEventJournalover anySqlClient(PostgreSQL and SQLite), CQRS dispatch and projections
asEffects, snapshots (SnapshotStore,load_aggregate), a gap-free globalposition,
EventLog::read_alland checkpointedCatchUpSubscriptions. Book part5/ch23.id_effect_authfeatureaccounts: registration, email verification and password reset
(Accounts,AccountRoutes,PgAccountTokenStore+ migration
0002_id_effect_account_tokens);id_effect_mailfeatureauth:AccountMails. Book
part6/ch30-03.- Core
id_effectfeaturefiber-registry(off by default):fiber_dump()lists every live
fiber with parent, children, state, age and span chain.Schema::with_decode_unknown_allis
public. id_effect_logger:RedactionPolicy(provide_redaction_policy),SamplingBackend/
SamplingStats,LogRecord::targetwithEffectLogger::log_target, and a process-wide
minimum level (set_global_minimum_log_level).id_effect_opentelemetry: configurable sampling (OtelConfig::sampler,OtelSampler,
OTEL_TRACES_SAMPLER[_ARG]);TracedHttpClient(one client span per call,traceparent
injected;layer(inner)); featuresqlwithinstrumented_sql_client/
instrumented_sql_layer; a reloadableEnvFilter(EnvFilterHandle);otel.kindsets the
span kind. Book ch27-00.id_effect_platform:HttpClientConfig,RetryPolicy/RetryingHttpClient,
ReqwestHttpClientProvider::layer_with,provide_reqwest_http_client_with,
ReqwestHttpClient::with_timeouts. Example040_http_client_retry.id_effect_sql:SqlError::ConnectionFailed/TlsFailed,SqlError::sqlstate(),
is_unique_violation(),SQLSTATE_UNIQUE_VIOLATION;InstrumentedSqlClientemits a
db.queryspan per statement (no bound parameters);SqlClientforArc<T>andBox<T>;
PgPoolConfig::connect_optionsis public;TestSqlClient::delay_queries.id_effect_config:Profiles::watch/watch_for/watch_withreturn aProfileWatch
that hot-reloads the profile files and publishes each good value through aSubscriptionRef.
Book ch16-03; example007_watch.id_effect_axum:track_in_flight(router, &host_drain)counts requests for the drain;
SecurityLayer::csrf_exempt_bearer().id_effect_test: featurefault(Fault, seededFaultPlanon the virtual clock; book
ch15-09, example030_faults); Mailpit and S3-compatible (SeaweedFS) containers.id_effect_tool:id-effect dev(watch, rebuild, restart on success; a failed build keeps the
running app;--servicesstarts compose services). Book ch34-02.id_effect_tool:id-effect generate job <name> [--queue] [--cron]adds a job module, its
registration, a test and theid_effect_jobsdependency to a generated project; never
overwrites.id_effect_tool:id-effect new --template servicegains--jobs(a job registry, queue and
worker; Postgres-backed with--db pg) and--console(the operator console on a loopback
port); every template ships a.devcontainer/; the service's Kamal config checks/readyand
out-waits the drain. Book ch34 "Rolling restarts and drain".id_effect_kit: featuresjobs,jobs-cron,messaging,redis-streams,mail,
console,storage,storage-s3,http-client,logger,eventsandauth-accounts, all
infull;testincludesid_effect_test/fault;sqlturns on the OTel SQL spans when
otelis on. Tierplatform.examples/reference/ledger: monthly statements (cron leader → one job per account → CSV in S3
→ mail with an attachment or signed link), postings through outbox → Redis Streams → inbox,
registration / verification / password reset,/_consolebehind an operator role with TOTP,
CSRF protection, traced HTTP and DB calls, hot-reloaded login limits and a seeded fault plan
on the posting path.
Changed
- Breaking
id_effect_logger:LogRecordis#[non_exhaustive]; build it with
LogRecord::newand thewith_*methods. - Breaking
id_effect_opentelemetry:OtelConfiggains thesamplerfield. - Breaking
id_effect_sql: network and TLS failures areSqlError::ConnectionFailed/
TlsFailedinstead ofQueryFailed; PostgreSQL errors carry(SQLSTATE …); MySQL duplicate
keys and SQLite unique violations report23505. - Breaking
id_effect_sql: the backends no longer enable sqlx'stimefeature (reverting
0.9); MySQL date-time parameters bind as text literals. - Breaking
id_effect_sql:SqlClientis implemented forBox<T>andArc<T>; an
impl SqlClient for Box<YourClient>of your own now conflicts with it. - Breaking
id_effect_events: the published API differs from the labs crate (effects,
append_expected,SqlEventJournaloverSqlClient, the journal'spositioncolumn). - Breaking
id_effect_tool:cli::NewArgsgainsjobsandconsole. id_effect_platform: the reqwest layer has a 10 s connect / 30 s total timeout and retries
idempotent requests (GET/PUT/DELETE) on connect errors,5xxand429, up to 3
attempts, honouringRetry-After.id_effect_logger:provide_composite_loggerandprovide_redaction_policyare process-wide
(they were per thread); minimum level, annotations and spans stay per thread.id_effect_sql:#[from_row(schema)]on a derived type lists every invalid column in
SqlError::InvalidRow; derived schemas report every invalid field fromdecode_unknown_all.id_effect_sql: PostgreSQL date-times use the crate's own binary codecs; values jiff cannot
hold (±infinity, years beyond ±9999) areSqlError::ValueOutOfRange, never a panic.id_effect_node:PgAdvisoryLease::from_pool_configapplies the pool config's TLS settings
(it used only the URL); QUIC uses rustls on aws-lc-rs.id_effect_toolservicetemplate:/readyturns 503 on SIGTERM and in-flight requests
drain; request spans come fromRequestIdLayer::span_hookinstead of a root-fiber hook.
Deprecated
- Nothing new. The 0.9 deprecations (
id_effect_sql::SqlParam,SqlRow::new,
SqlRow::text_cells;id_effect_axum::json::decode_json_schema,JsonSchemaError,
SchemaJsonErrorBody) stay one more release to keep 0.10's breaking changes to the new
batteries; they are removed in 0.11 (MIGRATION-0.9 and the book now say so).
Removed
labs/id_effect_jobs(with its apalis, obix,joband rdkafka dependencies) and
labs/id_effect_graph; see MIGRATION-0.10.md for the replacements.id_effect_events: thees-entityfeature (EsEntityEventStore,EsEntityPgBackend, its DDL
helpers and providers,dispatch_command_es_entity),SqlJournalBackend/
TestSqlJournalBackend, and theid_effect_graph, es-entity, sqlx and tokio dependencies.id_effect_sql: thetimedependency and sea-query'swith-time.
Fixed
id_effect_node:ClusterSingleton::resignclears the local pid after a lost lease;
PgAdvisoryLease::try_acquireno longer "re-acquires" after its lock session died.id_effect_proc_macro:#[derive(Schema)]is hygienic (a field namedobjor__issuesno
longer shadows the decoder's locals) and reports an unsupported field type once, not three times.id_effect_toolservicetemplate:/readydid not see the server's readiness, so it never
turned 503 on SIGTERM, and nothing was drained.id_effect_sql:InstrumentedSqlClientforwardsbegin_withandquery_stream(wrapping a
PgSqlClientlost server-side streaming and custom transaction options).id_effect_node: quinn's default features pulled in rustls'ringprovider next to aws-lc-rs,
so TLS configs built from the default panicked whenquicwas enabled with sqlx TLS.id_effect_messaging:Inbox::handler/MemoryInbox::handlerno longer borrow the inbox.id_effect_axum:trace_requestno longer holds its span entered across.await.id_effect_console: the dashboard sends the CSRF token, so its log-level and job actions work
behindSecurityLayer::recommended.id_effect_test: a container start that loses a port-publish race ("address already in use",
"port is already allocated") is retried with fresh ports instead of failing the test.
Security
id_effect_logger: the redaction policy and composite backend applied only on the thread that
installed them, so lines logged on server worker threads reachedtracingunredacted; both
are process-wide now.id_effect_config: config parse errors no longer quote the offending source line or values
(a malformed line holding a secret reachedConfigError, logs and
ProfileWatch::last_error()).id_effect_authaccounts: verification and reset tokens are stored only as SHA-256 digests,
compared in constant time and single-use;forgot_passwordanswers identically for unknown
addresses; a reset revokes the account's sessions. Account mails bypass the job queue, so a
usable token never sits in the jobs table.id_effect_console: every route sits behind the caller's guard (including the 404 fallback);
job actions requireapplication/json, so a cross-site form cannot trigger them; the page
sendsSecurityLayer's CSRF token.id_effect_axum:SecurityLayer::csrf_exempt_bearer()skips CSRF only for requests with
Authorization: Bearerand noCookieheader; cookie, Basic and other schemes are still
checked.id_effect_mail: SMTP / SendGrid credentials print as<redacted>inDebug.deny.toml: 0BSD allowed (quoted_printable, via lettre).
Scorecard
Measured by blind re-score (docs/scorecard/REPORT-0.10.0.md, METHOD 2.1):
| Index | 0.9.0 | 0.10.0 | Target |
|---|---|---|---|
| Enterprise (E) | 67.2 (#3) | 75.5 (#2) | 73.8 (#2) |
| Runtime (RT) | 70.8 (#2) | 78.4 (#1) | 76.8 (#1) |
| Rows < 40 | 21 | 12 | ≤ 12 |
| Rows ≥ 80 | 38 | 47 | 44 |
| Rows where id_effect leads | 19 | 24 | — |
17 rows up, none down. Messaging & jobs 45.4 → 87.6, Observability 70.4 → 92.6; biggest rows
msg.jobs 13 → 100, msg.email 1 → 75, obs.console / msg.brokers / dx.repl 25 → 75,
data.events 39 → 88. Every 0.10-targeted row met or beat its target; dx.lint moved to 0.11
by the D1 checkpoint. DX KPIs (docs/benchmarks/dx-kpis-0.10.json): K1 19.90 s, K2 0.97 s, K3
632 lines including a background job, K4 17/20, K5 93.3 %, K6 22.52 s. Bench gates: no budget
gate fails against 0.9.0, with fiber-registry off or on.
Release artifacts
Each crate ships with its .crate file and a CycloneDX SBOM (.cdx.json).
Full Changelog: v0.9.0...v0.10.0