| 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. |
| 4 | module http |
| 5 | |
| 6 | import net |
| 7 | import sync |
| 8 | |
| 9 | @[heap] |
| 10 | struct TlsIdleConnTracker { |
| 11 | mu &sync.Mutex = sync.new_mutex() |
| 12 | mut: |
| 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 | |
| 23 | fn (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 | |
| 41 | fn (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. |
| 60 | fn (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 | |
| 72 | fn (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 | |