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.
Hi!
While migrating our Rails API from Puma to Falcon, we ran into
--graceful-stopbeing 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#stopshadows its parent'sgraceful = @graceful_stopdefault withgraceful = true.As a result,
controller.stopcalls without arguments (made internally byContainer::Controller#runonInterrupt) forwardtruetoGroup#stop, which falls back toDEFAULT_GRACEFUL_TIMEOUT = 10sregardless of:graceful_stop:constructor option--graceful-stopCLI flag (which is wired into the constructor)ASYNC_CONTAINER_GRACEFUL_TIMEOUTenv var (added in async-container v0.27.0)Versions: async-service 0.22.0, async-container 0.34.5, falcon 0.55.3.
Reproduction
Expected:
200 ~25s. Observed:000 ~10s. Falcon's log showsStopping container... timeout=10.0.Root cause
Container::Controller#runcallsself.stop(no args) onInterrupt, sogracefulresolves to the literaltrue.That
truepropagates toGroup#stop, which usesDEFAULT_GRACEFUL_TIMEOUT = 10.0whenevergraceful == true.Proposed fix
super(no parens) forwards the new value. Existingstop(false)callers inContainer::Controller#run(Terminate/ensure) are unaffected.Verified locally as a monkey-patch with the repro above:
--graceful-stopNote on default behavior
This shifts the default for users not passing
graceful_stop:/--graceful-stopfrom 10s (current accidental fallback) to Falcon CLI's actual default of1.0.If 10s should remain the effective default, the CLI default in
falcon/command/serve.rbwould 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.