vxx2 / vlib / net / http / server_tls_idle.v
105 lines · 99 sloc · 3.54 KB · 6e2d911004257b2b6384e3a12ad0c050275f4ed6
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6import net
7import sync
8
9@[heap]
10struct TlsIdleConnTracker {
11 mu &sync.Mutex = sync.new_mutex()
12mut:
13 handles []int
14 closing bool
15 // closed records handles that close_idle force-closed on Windows. The woken
16 // worker consults was_force_closed and relinquishes socket ownership (so its
17 // conn.shutdown frees the TLS resources without closing the fd again), keeping
18 // the fd closed exactly once. Empty on non-Windows (close_idle only shuts the
19 // fd down there, leaving the worker the sole closer).
20 closed []int
21}
22
23fn (mut t TlsIdleConnTracker) mark_idle(handle int) bool {
24 t.mu.lock()
25 defer {
26 t.mu.unlock()
27 }
28 if t.closing {
29 return false
30 }
31 // Dedup: the same handle can be marked twice (e.g. a worker marks it for the
32 // handshake window and again per keep-alive request, or the OS recycles the
33 // fd value). A duplicate would make close_idle shut down / close the same fd
34 // twice within its own loop, so only track each handle once.
35 if t.handles.index(handle) < 0 {
36 t.handles << handle
37 }
38 return true
39}
40
41fn (mut t TlsIdleConnTracker) unmark_idle(handle int) {
42 t.mu.lock()
43 defer {
44 t.mu.unlock()
45 }
46 idx := t.handles.index(handle)
47 if idx >= 0 {
48 t.handles.delete(idx)
49 }
50}
51
52// was_force_closed reports whether close_idle has already closed `handle`
53// (Windows force-close to wake a worker blocked in select). The woken worker
54// must then relinquish socket ownership before its own conn.shutdown, which
55// then frees the TLS resources without closing the fd again: a second close
56// would race process-wide SOCKET reuse — the value can be reused by ANY socket
57// the process opens, not just this server's accept loop — and could close an
58// unrelated socket. Always false on non-Windows, where close_idle only shuts
59// the fd down and the worker remains the sole closer.
60fn (mut t TlsIdleConnTracker) was_force_closed(handle int) bool {
61 $if windows {
62 t.mu.lock()
63 defer {
64 t.mu.unlock()
65 }
66 return t.closed.index(handle) >= 0
67 } $else {
68 return false
69 }
70}
71
72fn (mut t TlsIdleConnTracker) close_idle() {
73 t.mu.lock()
74 defer {
75 t.mu.unlock()
76 }
77 t.closing = true
78 handles := t.handles.clone()
79 t.handles.clear()
80 $if windows {
81 // On Windows, net.shutdown(SD_BOTH) does not unblock a worker blocked in
82 // select() — only closesocket() (net.close, below) does — so we must
83 // force-close idle handles to wake their workers. Record ownership of
84 // those closes here, under the lock, so a worker that wakes (or exits for
85 // another reason) sees it via was_force_closed and relinquishes socket
86 // ownership instead of closing the fd again. Without that the fd would be
87 // closed twice, and between
88 // the two closes the SOCKET value can be reused by any socket the process
89 // opens, so the second close could hit an unrelated socket.
90 t.closed << handles
91 }
92 // Hold the lock across the loop. On non-Windows, was_force_closed is always
93 // false, so the worker remains the sole closer (via conn.shutdown); holding
94 // the lock blocks its unmark_idle until our net.shutdown below has run, so
95 // the worker cannot close the fd — and let the OS recycle the value — before
96 // we shut it down. (Releasing the lock here would reopen exactly that race.)
97 // On Windows the woken worker skips its close regardless, so the held lock
98 // only briefly delays it and is otherwise harmless.
99 for handle in handles {
100 net.shutdown(handle)
101 $if windows {
102 net.close(handle) or {}
103 }
104 }
105}
106