From 430f9a61605871530c73c6c62c6beff06aa18c7f Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Mon, 24 Aug 2026 14:54:04 -0700 Subject: [PATCH] docs: say where async waits with no worker solid-objects-js#22 reported that its README never said a process claims nothing until the roles start. An external prober built a two-process harness from that page and read the unclaimed messages as stranded. The gem states the rule in Worker requirements, but the async section, which is where a reader actually is when the question occurs to them, only said execution is left to the worker fleet. The async section now says the generator and the migrations start no role, so an application that serves web requests alone leaves the message ready until solid_objects start runs the roles, and points at the feature-by-role table. The runtime section of the operations guide says the same, with the boundary that makes it make sense: a direct call or an explicit sync needs no running role. test/integration/background_pickup_test.rb keeps it honest. The message reads ready and the actor state stays empty until a worker runs, then reads completed. Running a worker before the first assertion turns it red, so the assertion detects pickup rather than asserting a constant. The enqueue does create the instance row, because it allocates the mailbox sequence, so the test asserts on empty state rather than on a missing row. --- CHANGELOG.md | 10 ++++++ README.md | 6 ++++ docs/operations.md | 7 ++++ test/integration/background_pickup_test.rb | 37 ++++++++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 test/integration/background_pickup_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c21e7..92b679d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +- State where `async` waits when no worker runs. The `async` section of the + README and the runtime section of `docs/operations.md` now say that the + generator and the migrations start no role, so an application that serves + web requests alone leaves the message ready until + `bundle exec solid_objects start` runs the roles. The message is durable + and waits; it is not lost. `test/integration/background_pickup_test.rb` + pins it: the message reads `ready` and the actor state stays empty until a + worker runs. This matches solid-objects-js#22, which reported the same gap + for `runtime.run(signal)` in the Node package. + - Add `examples/at_least_once` and `bundle exec rake at_least_once`, an executable proof that the at-least-once clause fires and that the documented remedy absorbs it. One actor turn stages an effect that writes diff --git a/README.md b/README.md index 6748081..69050f1 100644 --- a/README.md +++ b/README.md @@ -662,6 +662,12 @@ message = order.async( ).submit ``` +`async` needs a running actor worker. Installing the engine and migrating the +schema starts no role, so a process that only serves web requests leaves the +message ready. Nothing is lost. The message waits until +`bundle exec solid_objects start` runs the roles. See +[Worker requirements](#worker-requirements) for the feature-by-role table. + Use `available_at:` to spread bulk work or delay one message: ```ruby diff --git a/docs/operations.md b/docs/operations.md index 30d7e90..f1dd8dc 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -31,6 +31,13 @@ Start all configured roles: bundle exec solid_objects start ``` +The generator and the migrations prepare the database and start nothing. A +process claims ready messages only after this command starts its roles, so an +application that serves web requests alone leaves every `async` message ready. +The message is durable and waits for the first process that runs the roles. A +direct call or an explicit `sync` needs no running role, because the caller's +own path executes it. + The command loads the host application's `app/actors` directories before starting any runtime role, even when Rails eager loading is disabled. Actors in the conventional directory do not need initializer references. The targeted diff --git a/test/integration/background_pickup_test.rb b/test/integration/background_pickup_test.rb new file mode 100644 index 0000000..1668244 --- /dev/null +++ b/test/integration/background_pickup_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "database_test_helper" + +class BackgroundPickupTest < ActiveSupport::TestCase + class MailboxActor < SolidObjects::Actor + actor_type "background-pickup-mailbox" + + attribute :delivered, default: 0 + + def receive + self.delivered += 1 + end + end + + test "leaves an async message ready until a worker runs the roles" do + message = MailboxActor.ref("inbox").async.receive + + assert_equal "ready", message.status + assert_empty SolidObjects::Instance.find_by!( + actor_type: "background-pickup-mailbox", actor_id: "inbox" + ).state + + worker = SolidObjects::Worker.new + begin + worker.run_until_idle + ensure + worker.stop + end + + assert_equal "completed", message.status + assert_equal( + { "delivered" => 1 }, + SolidObjects::Instance.find_by!(actor_type: "background-pickup-mailbox", actor_id: "inbox").state + ) + end +end