Skip to content

fix: await object init outcome before reporting add/update success - #90

Open
ygd58 wants to merge 1 commit into
genlayerlabs:mainfrom
ygd58:fix/await-object-init-before-reporting-success
Open

fix: await object init outcome before reporting add/update success#90
ygd58 wants to merge 1 commit into
genlayerlabs:mainfrom
ygd58:fix/await-object-init-before-reporting-success

Conversation

@ygd58

@ygd58 ygd58 commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Fixes #80update_object_config (and add_object) reported success even when the object handler's init/1 asynchronously rejected the config.

Root cause

do_add_object treats ObjectSupervisor.start_object's {:ok, pid} as proof the config was accepted. But that only means the process spawnedObjectServer.init/1 defers the handler's real init/1 call via send(self(), :init_object), which runs afterward, asynchronously. A rejecting (or slow) init/1 hadn't even run yet when start_object returned.

Net effect: do_update_object_config's remove+re-add reported "updated", the overlay/snapshot recorded the new config, but the async init then failed and the object landed in :error state (handler_state nil) — silently dead while everything else said it was fine. Same bug for add_object (issue: "an added object whose init rejects currently reports :added").

Fix

Added ObjectServer.await_init/3 — a handshake callers use to learn the real init outcome instead of trusting start_object's {:ok, pid}:

  • New init_waiters/init_error fields on the ObjectServer struct.
  • await_init/3 does a GenServer.call(:await_init): replies immediately if init already resolved (:ok or {:error, init_error}), otherwise queues the caller.
  • New resolve_init/2 helper, wired into every place both native-mode (handler.init/1) and process-mode (backend start) init can resolve — releases all queued callers with the right answer.
  • do_add_object now calls await_init right after start_object succeeds. On failure it stops the half-initialized object, rolls back the topology edges, and returns {:error, {:object_init_failed, reason}} — the same failure shape as any other do_add_object failure branch, so do_update_object_config's existing rollback-to-old-spec logic handles it without any changes to that function. This single change point fixes both add_object and update_object_config's rollback, since both go through do_add_object.
  • Added client-facing messages in swarm_controller.ex's format_error/1 for the new error shapes instead of falling through to generic "Internal error".

Testing

  • test/genswarms/objects/object_server_await_init_test.exs (unit, starts an ObjectServer directly): successful init, rejected init, a slow init (asserts await_init actually blocks — elapsed-time check, not just a return value), a caller asking after resolution (via init_error), and 5 concurrent callers all released with the same outcome.
  • test/genswarms/swarm_manager_object_init_reject_test.exs (integration, via SwarmManager): reproduces the issue's exact repro — a handler rejecting config[:bad] == true. PATCH is now correctly refused and the object stays alive on its old config; POST add_object is correctly refused and nothing is left registered. Also covers the non-rejecting path for both, to guard against false-positive rejections.

I could not run these through mix test — this sandbox's network policy has no route to hex.pm to fetch deps (phoenix, bandit, jason, telemetry, ... per mix.exs). Instead:

  • Syntax-checked all changed/new files via Code.string_to_quoted.
  • Actually exercised the real, modified ObjectServer source standalone via Code.require_file (Registry started manually, one-line :telemetry stub since that's the only external dep the exercised paths touch) — all 5 await_init unit scenarios pass against the fix, and I confirmed they fail against the pre-fix code (await_init/2 is undefined).
  • Couldn't bootstrap the full SwarmManager integration test the same way (too many interlinked GenServers/Registries to hand-wire safely), so that file is syntax-checked only pending CI.

Flagging the coverage gap explicitly rather than overclaiming local test results — CI will be the first full run of the integration test.

…enlayerlabs#80)

update_object_config's docs promise "a rejected init/1 rolls back to the
old config" — that only held for SYNCHRONOUS rejections (the config_schema
immutable-key gate). A handler whose init/1 returns {:error, _} was not
caught:

- do_update_object_config's remove+re-add calls do_add_object, which calls
  ObjectSupervisor.start_object.
- start_object (and ObjectServer's own GenServer init/1) returns {:ok, pid}
  as soon as the PROCESS spawns. The handler's real init/1 runs afterward,
  asynchronously, via a deferred `send(self(), :init_object)` — so a slow
  or rejecting init/1 hadn't even run yet when start_object returned.
- do_add_object treated {:ok, pid} as "config accepted" and reported
  success. The async init then failed, the object landed in :error state
  (handler_state nil), but the PATCH/POST already reported "updated"/
  "added" and the overlay/snapshot recorded the new config — the object
  was silently dead while everything else said it was fine.

Same bug for add_object (issue: "an added object whose init rejects
currently reports :added").

Fix: added ObjectServer.await_init/3, a handshake callers can use to learn
the REAL init outcome instead of trusting start_object's {:ok, pid}.

- New init_waiters/init_error fields on the ObjectServer struct.
- await_init/3 does a GenServer.call(:await_init); if init already
  resolved it replies immediately (:ok or {:error, init_error}), otherwise
  it queues the caller and a new resolve_init/2 helper — wired into every
  place both native-mode (handler.init/1) and process-mode (backend start)
  init can resolve, success or failure — releases all queued callers with
  the right answer once it does.
- do_add_object now calls await_init right after start_object succeeds.
  On {:error, reason} it stops the half-initialized object, rolls back the
  topology edges, and returns {:error, {:object_init_failed, reason}} —
  same failure shape as any other add_object failure branch, so it's
  already handled by do_update_object_config's existing rollback-to-old-
  spec logic without touching that function. Fixes BOTH add_object and
  update_object_config's rollback from a single change point, since both
  go through do_add_object.
- Added client-facing messages in swarm_controller.ex's format_error/1 for
  the new error shapes (object_init_failed, config_rejected,
  config_rejected_and_rollback_failed) instead of falling through to the
  generic "Internal error".

Testing:

Added tests/genswarms/objects/object_server_await_init_test.exs (unit,
starts an ObjectServer directly) covering: successful init, rejected
init, a slow init (asserts await_init actually blocks — elapsed time
check, not just a return value), a caller asking AFTER resolution (via
init_error, not just the waiter queue), and 5 concurrent callers all
released with the same correct outcome.

Added tests/genswarms_swarm_manager_object_init_reject_test.exs
(integration, via SwarmManager) reproducing the issue's exact repro: an
object handler whose init/1 rejects config[:bad] == true — PATCH is now
correctly refused and the object stays alive on its old config; POST
add_object is correctly refused and nothing is left registered. Also
covers the non-rejecting path for both (no false-positive rejections).

Couldn't run these through `mix test` — this sandbox's network policy has
no route to hex.pm to fetch deps (phoenix, bandit, jason, telemetry, ...
per mix.exs). Instead: syntax-checked all changed/new files via
Code.string_to_quoted, then actually exercised the real, modified
ObjectServer source standalone via Code.require_file (with a Registry
started manually and a one-line :telemetry stub, since that's the only
external dep the exercised code paths touch) — all 5 await_init unit
scenarios pass against the fix and I confirmed they fail against
pre-fix code (await_init/2 is undefined). Couldn't run the SwarmManager
integration test the same way (too many interlinked GenServers/Registries
to hand-bootstrap safely), so that file is syntax-checked only pending
CI. Flagging this explicitly rather than overclaiming local coverage.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@ygd58, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09882704-d6db-4218-b856-f2b468091b09

📥 Commits

Reviewing files that changed from the base of the PR and between 9295e25 and b5a31b0.

📒 Files selected for processing (5)
  • lib/genswarms/objects/object_server.ex
  • lib/genswarms/swarm_manager.ex
  • lib/genswarms_web/controllers/swarm_controller.ex
  • test/genswarms/objects/object_server_await_init_test.exs
  • test/genswarms/swarm_manager_object_init_reject_test.exs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update_object_config reports 'updated' when the handler's init/1 rejects the patch (async init not awaited)

1 participant