Skip to content

Commit b0bd84e

Browse files
committed
server+limits: add support for graceful shutdown
1 parent 87a1e58 commit b0bd84e

File tree

3 files changed

+84
-52
lines changed

3 files changed

+84
-52
lines changed

web-server-doc/web-server/scribblings/safety-limits.scrbl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
[#:max-form-data-field-length max-form-data-field-length nonnegative-length/c
4646
(code:line (* 8 1024) (code:comment #,(elem "8 KiB")))]
4747
[#:response-timeout response-timeout timeout/c 60]
48-
[#:response-send-timeout response-send-timeout timeout/c 60])
48+
[#:response-send-timeout response-send-timeout timeout/c 60]
49+
[#:shutdown-grace-period shutdown-grace-period (or/c #f timeout/c) #f])
4950
safety-limits?]
5051
@defthing[nonnegative-length/c flat-contract?
5152
#:value (or/c exact-nonnegative-integer? +inf.0)]
@@ -144,7 +145,14 @@
144145
If your application uses streaming responses or long polling,
145146
either adjust this value or make sure that your request handler sends
146147
data periodically, such as a no-op, to avoid hitting this limit.
147-
}]
148+
}
149+
@item{The @racket[shutdown-grace-period] argument controls how long,
150+
during shutdown, the server will wait for in-flight requests to
151+
finish before stopping. If @racket[#f], in-flight requests are killed
152+
immediately. Otherwise, the server stops accepting new connections
153+
and waits until either all in-flight requests complete, or the grace
154+
period passes, at which point it shuts down its custodian.}
155+
]
148156

149157

150158
@elemtag["safety-limits-porting"]{@bold{Compatibility note:}}
@@ -202,7 +210,8 @@
202210
[#:max-form-data-fields max-form-data-fields nonnegative-length/c +inf.0]
203211
[#:max-form-data-field-length max-form-data-field-length nonnegative-length/c +inf.0]
204212
[#:response-timeout response-timeout timeout/c +inf.0]
205-
[#:response-send-timeout response-send-timeout timeout/c +inf.0])
213+
[#:response-send-timeout response-send-timeout timeout/c +inf.0]
214+
[#:shutdown-grace-period shutdown-grace-period (or/c #f timeout/c) #f])
206215
safety-limits?]{
207216
Like @racket[make-safety-limits], but with default values that avoid
208217
imposing any limits that aren't explicitly specified,

web-server-lib/web-server/private/dispatch-server-with-connect-unit.rkt

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
(thread
3838
(lambda ()
3939
(define listener
40-
(with-handlers ([exn? (λ (e)
41-
(async-channel-put* confirmation-channel e)
42-
(raise e))])
40+
(with-handlers ([exn?
41+
(lambda (e)
42+
(async-channel-put* confirmation-channel e)
43+
(raise e))])
4344
(tcp-listen config:port config:max-waiting #t config:listen-ip)))
4445
(define-values (_local-addr local-port _remote-addr _remote-port)
4546
(tcp-addresses listener #t))
4647
(async-channel-put* confirmation-channel local-port)
47-
4848
(dynamic-wind
4949
void
5050
(lambda ()
@@ -60,48 +60,70 @@
6060
;; not synchronizable.
6161
(define listener-evt (if (evt? listener) listener (handle-evt always-evt (λ (_) listener))))
6262
(define max-concurrent (safety-limits-max-concurrent config:safety-limits))
63-
(let loop ([in-progress 0])
64-
(loop
65-
(with-handlers ([exn:fail:network? (λ (e)
66-
((error-display-handler)
67-
(format "Connection error: ~a" (exn-message e))
68-
e)
69-
in-progress)])
70-
(do-sync
71-
(handle-evt
72-
(thread-receive-evt)
73-
(lambda (_)
74-
(let drain-loop ([in-progress in-progress])
75-
(if (thread-try-receive)
76-
(drain-loop (sub1 in-progress))
77-
in-progress))))
78-
(handle-evt
79-
(if (< in-progress max-concurrent) listener-evt never-evt)
80-
(lambda (l)
81-
(define custodian (make-custodian))
82-
(parameterize ([current-custodian custodian])
83-
(parameterize-break #f
84-
(define-values (in out)
85-
(do-accept l))
86-
(define handler-thd
87-
(thread
88-
(lambda ()
89-
(call-with-parameterization
90-
paramz
91-
(lambda ()
92-
(when can-break? (break-enabled #t))
93-
(parameterize ([current-custodian (make-custodian custodian)])
94-
(handler in out)))))))
95-
(thread
96-
(lambda ()
97-
(thread-wait handler-thd)
98-
(thread-send listener-thd 'done)
99-
(custodian-shutdown-all custodian)))
100-
(add1 in-progress))))))))))
63+
(let loop ([in-progress 0]
64+
[stopped? #f])
65+
(define accepting?
66+
(and (not stopped?)
67+
(in-progress . < . max-concurrent)))
68+
(define-values (in-progress* stopped?*)
69+
(with-handlers ([exn:fail:network?
70+
(lambda (e)
71+
((error-display-handler)
72+
(format "Connection error: ~a" (exn-message e))
73+
e)
74+
(values in-progress stopped?))])
75+
(do-sync
76+
(handle-evt
77+
(thread-receive-evt)
78+
(lambda (_)
79+
(match (thread-receive)
80+
['done (values (sub1 in-progress) stopped?)]
81+
['stop (values in-progress #t)])))
82+
(handle-evt
83+
(if accepting? listener-evt never-evt)
84+
(lambda (l)
85+
(define custodian (make-custodian))
86+
(parameterize ([current-custodian custodian])
87+
(parameterize-break #f
88+
(define-values (in out)
89+
(do-accept l))
90+
(define handler-thd
91+
(thread
92+
(lambda ()
93+
(call-with-parameterization
94+
paramz
95+
(lambda ()
96+
(when can-break? (break-enabled #t))
97+
(parameterize ([current-custodian (make-custodian custodian)])
98+
(handler in out)))))))
99+
(thread
100+
(lambda ()
101+
(thread-wait handler-thd)
102+
(thread-send listener-thd 'done)
103+
(custodian-shutdown-all custodian)))
104+
(values (add1 in-progress) stopped?))))))))
105+
(unless (and stopped?* (zero? in-progress*))
106+
(loop in-progress* stopped?*))))
101107
(lambda ()
102108
(tcp-close listener))))))
103-
(lambda ()
104-
(custodian-shutdown-all the-server-custodian))))
109+
;; When there is a grace period, calling stop the first time causes the server to stop accepting
110+
;; new connections and waits for in-progress connections to finish. Calling it a second time
111+
;; immediately kills the server. This can come in handy when implementing dev tooling where stop
112+
;; can be called after a break to begin shutdown, and it can be called again after another break
113+
;; to kill the server (eg. if the developer doesn't want to wait for requests in flight at that
114+
;; particular moment).
115+
(let ([stopping? #f])
116+
(lambda ()
117+
(cond
118+
[(and (not stopping?)
119+
(safety-limits-shutdown-grace-period config:safety-limits))
120+
=> (lambda (timeout)
121+
(set! stopping? #t)
122+
(thread-send listener-thd 'stop)
123+
(sync/timeout timeout listener-thd)
124+
(custodian-shutdown-all the-server-custodian))]
125+
[else
126+
(custodian-shutdown-all the-server-custodian)])))))
105127

106128
;; serve-ports : input-port output-port -> void
107129
;; returns immediately, spawning a thread to handle

web-server-lib/web-server/safety-limits.rkt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#lang racket/base
22

3-
(require racket/contract
3+
(require (for-syntax racket/base
4+
racket/syntax)
5+
racket/contract
46
racket/match
5-
syntax/parse/define
6-
(for-syntax racket/base
7-
racket/syntax))
7+
syntax/parse/define)
88

99
;; Also, define-safety-limits/private-submodule generates
1010
;; a private submodule providing accessor functions and a match expander.
@@ -83,4 +83,5 @@
8383
max-form-data-parts nonnegative-length/c (+ max-form-data-fields max-form-data-files)
8484
max-form-data-header-length nonnegative-length/c (* 8 1024)
8585
response-timeout timeout/c 60
86-
response-send-timeout timeout/c 60)
86+
response-send-timeout timeout/c 60
87+
shutdown-grace-period (or/c #f timeout/c) #f)

0 commit comments

Comments
 (0)