Skip to content

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

@akitoshiga

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions