Skip to content

Async::Service::Controller#stop overrides @graceful_stop default with literal true #10

Description

@akitoshiga

Hi!
While migrating our Rails API from Puma to Falcon, we ran into --graceful-stop being silently ignored.
After spelunking through the gem stack, the root cause turned out to be a small detail in Async::Service::Controller#stop.
Writing it up here in case it's useful — and thanks for all the great work on the async ecosystem, it's been a delight to dig into.

Summary

Async::Service::Controller#stop shadows its parent's graceful = @graceful_stop default with graceful = true.
As a result, controller.stop calls without arguments (made internally by Container::Controller#run on Interrupt) forward true to Group#stop, which falls back to DEFAULT_GRACEFUL_TIMEOUT = 10s regardless of:

  • the graceful_stop: constructor option
  • Falcon's --graceful-stop CLI flag (which is wired into the constructor)
  • the ASYNC_CONTAINER_GRACEFUL_TIMEOUT env var (added in async-container v0.27.0)

Versions: async-service 0.22.0, async-container 0.34.5, falcon 0.55.3.

Reproduction

# probe.ru
run ->(_) { sleep 25; [200, {}, ['ok']] }
bundle exec falcon serve --bind http://0.0.0.0:4567 --count 1 --forked \
  --graceful-stop 30 --config probe.ru &
PID=$!; sleep 1
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" http://localhost:4567/ &
sleep 0.5; kill -TERM $PID; wait

Expected: 200 ~25s. Observed: 000 ~10s. Falcon's log shows Stopping container... timeout=10.0.

Root cause

# async-service-0.22.0/lib/async/service/controller.rb
class Controller < Async::Container::Controller
  def stop(graceful = true)   # ← shadows parent's `graceful = @graceful_stop`
    @services.each { |service| ... }
    super                      # forwards `true` upstream
  end
end

Container::Controller#run calls self.stop (no args) on Interrupt, so graceful resolves to the literal true.
That true propagates to Group#stop, which uses DEFAULT_GRACEFUL_TIMEOUT = 10.0 whenever graceful == true.

Proposed fix

-  def stop(graceful = true)
+  def stop(graceful = @graceful_stop)

super (no parens) forwards the new value. Existing stop(false) callers in Container::Controller#run (Terminate / ensure) are unaffected.

Verified locally as a monkey-patch with the repro above:

--graceful-stop Without fix With fix
1 killed at 10s killed at 1s
30 killed at 10s completes at 25s

Note on default behavior

This shifts the default for users not passing graceful_stop: / --graceful-stop from 10s (current accidental fallback) to Falcon CLI's actual default of 1.0.
If 10s should remain the effective default, the CLI default in falcon/command/serve.rb would also need to be raised.

Context

socketry/falcon#71 (closed) referenced async-container v0.27.0's env-var as the resolution; that fix is correct, it just does not propagate through Service::Controller#stop.

Happy to send a PR with a regression test if this analysis looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions