kino 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,437 @@
1
+ //! Current-thread Tokio runtimes for HTTP I/O.
2
+ //!
3
+ //! One accept thread owns the listener and assigns accepted connections to
4
+ //! the least-loaded shard. Each shard then owns that connection for its
5
+ //! lifetime, avoiding the shared Tokio worker pool on hot HTTP paths.
6
+
7
+ use std::net::SocketAddr;
8
+ use std::sync::atomic::{AtomicUsize, Ordering};
9
+ use std::sync::Arc;
10
+ use std::thread::JoinHandle;
11
+
12
+ use crate::listen::Listener;
13
+ use crate::log::{self, Level};
14
+ use crate::registry::{ServerInner, STATE_DRAINING};
15
+ use crate::server::{serve_conn, AsyncListener, Conn};
16
+
17
+ /// A connection in transit from the acceptor to its shard. Tokio streams
18
+ /// are bound to the runtime that registered them, so the handoff carries
19
+ /// the std stream and the shard re-registers it on arrival.
20
+ enum StdConn {
21
+ Tcp(std::net::TcpStream),
22
+ Unix(std::os::unix::net::UnixStream),
23
+ }
24
+
25
+ impl StdConn {
26
+ /// Register with the calling (shard) runtime.
27
+ fn into_tokio(self) -> std::io::Result<Conn> {
28
+ Ok(match self {
29
+ StdConn::Tcp(stream) => Conn::Tcp(tokio::net::TcpStream::from_std(stream)?),
30
+ StdConn::Unix(stream) => Conn::Unix(tokio::net::UnixStream::from_std(stream)?),
31
+ })
32
+ }
33
+ }
34
+
35
+ /// Detach an accepted stream from the acceptor's runtime for the handoff.
36
+ fn into_std(conn: Conn) -> std::io::Result<StdConn> {
37
+ Ok(match conn {
38
+ Conn::Tcp(stream) => StdConn::Tcp(stream.into_std()?),
39
+ Conn::Unix(stream) => StdConn::Unix(stream.into_std()?),
40
+ })
41
+ }
42
+
43
+ /// One accepted connection en route to a shard: the detached stream, the
44
+ /// addresses hyper reports, and the slot it holds against max_connections.
45
+ struct Accepted {
46
+ conn: StdConn,
47
+ remote_addr: SocketAddr,
48
+ local_addr: SocketAddr,
49
+ permit: tokio::sync::OwnedSemaphorePermit,
50
+ }
51
+
52
+ /// Shard count: an explicit `io_threads` wins; the default is half the
53
+ /// available CPUs. Framing requests is cheap next to running the app, so
54
+ /// the I/O plane gets the smaller share and Ruby workers keep the rest.
55
+ pub(crate) fn thread_count(io_threads: usize) -> usize {
56
+ if io_threads > 0 {
57
+ return io_threads;
58
+ }
59
+ default_thread_count(std::thread::available_parallelism().map_or(1, usize::from))
60
+ }
61
+
62
+ fn default_thread_count(cpus: usize) -> usize {
63
+ cpus.div_ceil(2)
64
+ }
65
+
66
+ /// Boot the shard threads, then the acceptor. Any thread that fails to
67
+ /// come up fails the whole boot: the already started threads are drained
68
+ /// (their senders drop) and joined before the error reaches Ruby.
69
+ pub(crate) fn spawn(
70
+ listener: Listener,
71
+ acceptor: Option<tokio_rustls::TlsAcceptor>,
72
+ server: Arc<ServerInner>,
73
+ max_connections: usize,
74
+ accept_shutdown_rx: tokio::sync::watch::Receiver<bool>,
75
+ runtime_shutdown_rx: tokio::sync::watch::Receiver<bool>,
76
+ shard_count: usize,
77
+ ) -> std::io::Result<Vec<JoinHandle<()>>> {
78
+ let shard_count = shard_count.max(1);
79
+ let mut handles = Vec::with_capacity(shard_count + 1);
80
+ let mut shard_txs = Vec::with_capacity(shard_count);
81
+ let mut loads = Vec::with_capacity(shard_count);
82
+
83
+ for i in 0..shard_count {
84
+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
85
+ let load = Arc::new(AtomicUsize::new(0));
86
+ let spawned = spawn_shard(
87
+ i,
88
+ rx,
89
+ acceptor.clone(),
90
+ server.clone(),
91
+ load.clone(),
92
+ runtime_shutdown_rx.clone(),
93
+ );
94
+ match spawned {
95
+ Ok(handle) => {
96
+ shard_txs.push(tx);
97
+ loads.push(load);
98
+ handles.push(handle);
99
+ }
100
+ Err(error) => {
101
+ drop(tx);
102
+ drop(shard_txs);
103
+ join_all(handles);
104
+ return Err(error);
105
+ }
106
+ }
107
+ }
108
+
109
+ match spawn_acceptor(
110
+ listener,
111
+ server,
112
+ max_connections,
113
+ accept_shutdown_rx,
114
+ shard_txs,
115
+ loads,
116
+ ) {
117
+ Ok(handle) => handles.push(handle),
118
+ Err(error) => {
119
+ join_all(handles);
120
+ return Err(error);
121
+ }
122
+ }
123
+ Ok(handles)
124
+ }
125
+
126
+ fn join_all(handles: Vec<JoinHandle<()>>) {
127
+ for handle in handles {
128
+ let _ = handle.join();
129
+ }
130
+ }
131
+
132
+ fn current_thread_runtime() -> std::io::Result<tokio::runtime::Runtime> {
133
+ tokio::runtime::Builder::new_current_thread()
134
+ .enable_all()
135
+ .build()
136
+ }
137
+
138
+ /// Wait for a just spawned I/O thread to report its runtime up, so a
139
+ /// startup failure becomes the boot error Ruby sees instead of a silently
140
+ /// dead thread.
141
+ fn await_ready(
142
+ handle: JoinHandle<()>,
143
+ ready_rx: std::sync::mpsc::Receiver<std::io::Result<()>>,
144
+ what: &str,
145
+ ) -> std::io::Result<JoinHandle<()>> {
146
+ match ready_rx.recv() {
147
+ Ok(Ok(())) => Ok(handle),
148
+ Ok(Err(error)) => {
149
+ let _ = handle.join();
150
+ Err(error)
151
+ }
152
+ Err(_) => Err(std::io::Error::other(format!(
153
+ "{what} thread exited during startup"
154
+ ))),
155
+ }
156
+ }
157
+
158
+ fn spawn_shard(
159
+ index: usize,
160
+ rx: tokio::sync::mpsc::UnboundedReceiver<Accepted>,
161
+ acceptor: Option<tokio_rustls::TlsAcceptor>,
162
+ server: Arc<ServerInner>,
163
+ load: Arc<AtomicUsize>,
164
+ mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
165
+ ) -> std::io::Result<JoinHandle<()>> {
166
+ let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
167
+ let handle = std::thread::Builder::new()
168
+ .name(format!("kino-io-{index}"))
169
+ .spawn(move || {
170
+ let runtime = match current_thread_runtime() {
171
+ Ok(runtime) => runtime,
172
+ Err(error) => {
173
+ let _ = ready_tx.send(Err(error));
174
+ return;
175
+ }
176
+ };
177
+ let _ = ready_tx.send(Ok(()));
178
+ runtime.block_on(shard_loop(rx, acceptor, server, load, &mut shutdown_rx));
179
+ })?;
180
+ await_ready(handle, ready_rx, "shard")
181
+ }
182
+
183
+ fn spawn_acceptor(
184
+ listener: Listener,
185
+ server: Arc<ServerInner>,
186
+ max_connections: usize,
187
+ shutdown_rx: tokio::sync::watch::Receiver<bool>,
188
+ shard_txs: Vec<tokio::sync::mpsc::UnboundedSender<Accepted>>,
189
+ loads: Vec<Arc<AtomicUsize>>,
190
+ ) -> std::io::Result<JoinHandle<()>> {
191
+ let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
192
+ let handle = std::thread::Builder::new()
193
+ .name("kino-accept".to_string())
194
+ .spawn(move || {
195
+ let runtime = match current_thread_runtime() {
196
+ Ok(runtime) => runtime,
197
+ Err(error) => {
198
+ let _ = ready_tx.send(Err(error));
199
+ return;
200
+ }
201
+ };
202
+ runtime.block_on(async move {
203
+ // Registration must happen on this runtime; a failure is
204
+ // routed through the same ready channel as a build error.
205
+ let listener = match AsyncListener::from_std(listener) {
206
+ Ok(listener) => listener,
207
+ Err(error) => {
208
+ let _ = ready_tx.send(Err(error));
209
+ return;
210
+ }
211
+ };
212
+ accept_loop(
213
+ listener,
214
+ server,
215
+ max_connections,
216
+ shutdown_rx,
217
+ shard_txs,
218
+ loads,
219
+ ready_tx,
220
+ )
221
+ .await;
222
+ });
223
+ })?;
224
+ await_ready(handle, ready_rx, "accept")
225
+ }
226
+
227
+ /// Serve handed-over connections until the acceptor is gone, then let the
228
+ /// remaining ones finish. The final teardown signal cuts either phase
229
+ /// short: dropping the runtime cancels connection tasks at their next
230
+ /// await point.
231
+ async fn shard_loop(
232
+ mut rx: tokio::sync::mpsc::UnboundedReceiver<Accepted>,
233
+ acceptor: Option<tokio_rustls::TlsAcceptor>,
234
+ server: Arc<ServerInner>,
235
+ load: Arc<AtomicUsize>,
236
+ shutdown_rx: &mut tokio::sync::watch::Receiver<bool>,
237
+ ) {
238
+ let mut connections = tokio::task::JoinSet::new();
239
+ loop {
240
+ tokio::select! {
241
+ _ = shutdown_rx.changed() => return,
242
+ accepted = rx.recv() => {
243
+ let Some(accepted) = accepted else { break };
244
+ let acceptor = acceptor.clone();
245
+ let server = server.clone();
246
+ let guard = LoadGuard(load.clone());
247
+ connections.spawn(async move {
248
+ let _guard = guard;
249
+ serve_accepted(accepted, acceptor, server).await;
250
+ });
251
+ }
252
+ // Reap closed connections so the set stays small.
253
+ Some(_) = connections.join_next(), if !connections.is_empty() => {}
254
+ }
255
+ }
256
+ // The acceptor is gone: drain. Every join is one connection closing.
257
+ while !connections.is_empty() {
258
+ tokio::select! {
259
+ _ = shutdown_rx.changed() => return,
260
+ _ = connections.join_next() => {}
261
+ }
262
+ }
263
+ }
264
+
265
+ /// Keeps the shard's connection count honest whichever way the task ends:
266
+ /// return, panic, or cancellation at teardown. A plain decrement after the
267
+ /// await would never run on the last two.
268
+ struct LoadGuard(Arc<AtomicUsize>);
269
+
270
+ impl Drop for LoadGuard {
271
+ fn drop(&mut self) {
272
+ self.0.fetch_sub(1, Ordering::Relaxed);
273
+ }
274
+ }
275
+
276
+ /// The shard's half of the handoff: re-register the stream on this
277
+ /// runtime, then run the shared connection pipeline (TLS handshake,
278
+ /// protocol layer) exactly as the default runtime would.
279
+ async fn serve_accepted(
280
+ accepted: Accepted,
281
+ acceptor: Option<tokio_rustls::TlsAcceptor>,
282
+ server: Arc<ServerInner>,
283
+ ) {
284
+ // Held for the connection's lifetime; dropping it frees a slot.
285
+ let _permit = accepted.permit;
286
+ let conn = match accepted.conn.into_tokio() {
287
+ Ok(conn) => conn,
288
+ Err(_) => {
289
+ log::emit(
290
+ Level::Warn,
291
+ "tokio",
292
+ "failed to register a stream on an I/O shard",
293
+ );
294
+ return;
295
+ }
296
+ };
297
+ serve_conn(
298
+ conn,
299
+ acceptor,
300
+ server,
301
+ accepted.remote_addr,
302
+ accepted.local_addr,
303
+ )
304
+ .await;
305
+ }
306
+
307
+ /// The sharded accept loop. Same backpressure as the default loop: the
308
+ /// permit is acquired before accept, so past max_connections the excess
309
+ /// waits in the kernel backlog instead of being accepted and dropped.
310
+ /// A shard whose channel is gone is marked dead and routed around; with
311
+ /// no shard left the loop stops accepting and flips the server to
312
+ /// draining, so the control plane stops reporting ready.
313
+ async fn accept_loop(
314
+ listener: AsyncListener,
315
+ server: Arc<ServerInner>,
316
+ max_connections: usize,
317
+ mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
318
+ shard_txs: Vec<tokio::sync::mpsc::UnboundedSender<Accepted>>,
319
+ loads: Vec<Arc<AtomicUsize>>,
320
+ ready_tx: std::sync::mpsc::SyncSender<std::io::Result<()>>,
321
+ ) {
322
+ let conn_limit = Arc::new(tokio::sync::Semaphore::new(max_connections));
323
+ let mut live = vec![true; shard_txs.len()];
324
+ let _ = ready_tx.send(Ok(()));
325
+ 'accept: loop {
326
+ let permit = tokio::select! {
327
+ _ = shutdown_rx.changed() => break,
328
+ permit = conn_limit.clone().acquire_owned() => match permit {
329
+ Ok(permit) => permit,
330
+ Err(_) => break,
331
+ },
332
+ };
333
+ let (conn, remote_addr, local_addr) = tokio::select! {
334
+ _ = shutdown_rx.changed() => break,
335
+ accepted = listener.accept() => match accepted {
336
+ Ok(accepted) => accepted,
337
+ Err(_) => continue,
338
+ },
339
+ };
340
+ let conn = match into_std(conn) {
341
+ Ok(conn) => conn,
342
+ Err(_) => {
343
+ log::emit(
344
+ Level::Warn,
345
+ "tokio",
346
+ "failed to detach an accepted stream; connection dropped",
347
+ );
348
+ continue;
349
+ }
350
+ };
351
+ let mut accepted = Accepted {
352
+ conn,
353
+ remote_addr,
354
+ local_addr,
355
+ permit,
356
+ };
357
+ loop {
358
+ let Some(index) = least_loaded(&loads, &live) else {
359
+ log::emit(
360
+ Level::Error,
361
+ "tokio",
362
+ "all I/O shards are down; not accepting connections",
363
+ );
364
+ server.state.store(STATE_DRAINING, Ordering::Relaxed);
365
+ break 'accept;
366
+ };
367
+ loads[index].fetch_add(1, Ordering::Relaxed);
368
+ match shard_txs[index].send(accepted) {
369
+ Ok(()) => break,
370
+ Err(returned) => {
371
+ loads[index].fetch_sub(1, Ordering::Relaxed);
372
+ live[index] = false;
373
+ log::emit(Level::Warn, "tokio", "I/O shard is down; routing around it");
374
+ accepted = returned.0;
375
+ }
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ /// The live shard with the fewest open connections.
382
+ fn least_loaded(loads: &[Arc<AtomicUsize>], live: &[bool]) -> Option<usize> {
383
+ loads
384
+ .iter()
385
+ .enumerate()
386
+ .filter(|(index, _)| live[*index])
387
+ .min_by_key(|(_, load)| load.load(Ordering::Relaxed))
388
+ .map(|(index, _)| index)
389
+ }
390
+
391
+ #[cfg(test)]
392
+ mod tests {
393
+ use super::{default_thread_count, least_loaded, thread_count, Arc, AtomicUsize};
394
+
395
+ fn loads(counts: &[usize]) -> Vec<Arc<AtomicUsize>> {
396
+ counts
397
+ .iter()
398
+ .map(|&n| Arc::new(AtomicUsize::new(n)))
399
+ .collect()
400
+ }
401
+
402
+ #[test]
403
+ fn explicit_io_threads_win() {
404
+ assert_eq!(thread_count(3), 3);
405
+ }
406
+
407
+ #[test]
408
+ fn least_loaded_picks_the_emptiest_live_shard() {
409
+ assert_eq!(
410
+ least_loaded(&loads(&[3, 0, 1]), &[true, true, true]),
411
+ Some(1)
412
+ );
413
+ }
414
+
415
+ #[test]
416
+ fn least_loaded_routes_around_dead_shards() {
417
+ // The dead shard's count is frozen at 0; it must not win anyway.
418
+ assert_eq!(
419
+ least_loaded(&loads(&[3, 0, 1]), &[true, false, true]),
420
+ Some(2)
421
+ );
422
+ }
423
+
424
+ #[test]
425
+ fn least_loaded_reports_when_no_shard_is_left() {
426
+ assert_eq!(least_loaded(&loads(&[0, 0]), &[false, false]), None);
427
+ }
428
+
429
+ #[test]
430
+ fn default_is_half_the_cpus() {
431
+ assert_eq!(default_thread_count(1), 1);
432
+ assert_eq!(default_thread_count(2), 1);
433
+ assert_eq!(default_thread_count(3), 2);
434
+ assert_eq!(default_thread_count(12), 6);
435
+ assert_eq!(default_thread_count(128), 64);
436
+ }
437
+ }
data/ext/kino/src/lib.rs CHANGED
@@ -4,9 +4,14 @@
4
4
  #[global_allocator]
5
5
  static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
6
6
 
7
+ mod access_log;
7
8
  mod control;
9
+ mod cpus;
8
10
  mod env_strings;
9
11
  mod gvl;
12
+ mod io_shards;
13
+ mod listen;
14
+ mod log;
10
15
  mod logsink;
11
16
  mod mono;
12
17
  mod pin;
@@ -62,8 +67,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
62
67
  function!(server::interrupt_all_workers, 1),
63
68
  )?;
64
69
  native.define_singleton_method("shutdown_runtime", function!(server::shutdown_runtime, 2))?;
65
- native.define_singleton_method("log_error", function!(server::log_error, 1))?;
70
+ native.define_singleton_method("log_line", function!(log::log_line, 3))?;
66
71
  native.define_singleton_method("sleep_chunk", function!(timer::sleep_chunk, 1))?;
72
+ native.define_singleton_method(
73
+ "available_parallelism",
74
+ function!(cpus::available_parallelism, 0),
75
+ )?;
67
76
  native.define_singleton_method("log_device_open", function!(logsink::device_open, 1))?;
68
77
  native.define_singleton_method("log_device_write", function!(logsink::device_write, 2))?;
69
78
  native.define_singleton_method("log_device_close", function!(logsink::device_close, 1))?;
@@ -87,6 +96,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
87
96
  request.define_method("write_chunk", method!(Request::write_chunk, 1))?;
88
97
  request.define_method("finish", method!(Request::finish, 0))?;
89
98
  request.define_method("abort", method!(Request::abort, 0))?;
99
+ request.define_method("timing", method!(Request::set_timing, 2))?;
90
100
 
91
101
  // Force-resolve the TypedData class cache on the main ractor: magnus
92
102
  // resolves it lazily on first wrap, and a racy first resolution from two
@@ -0,0 +1,137 @@
1
+ //! Listening sockets for the main server and the control plane: a TCP
2
+ //! `host:port`, or a `unix://path` domain socket (the usual shape behind
3
+ //! nginx). Binding is synchronous so an address conflict surfaces at boot.
4
+
5
+ use std::io;
6
+ use std::os::unix::net::UnixListener;
7
+ use std::path::{Path, PathBuf};
8
+
9
+ /// The bind scheme that selects a unix domain socket; anything else is a
10
+ /// TCP host.
11
+ pub const UNIX_SCHEME: &str = "unix://";
12
+
13
+ /// The socket path of a `unix://` bind, or None for a TCP host.
14
+ pub fn unix_path(bind: &str) -> Option<&Path> {
15
+ bind.strip_prefix(UNIX_SCHEME).map(Path::new)
16
+ }
17
+
18
+ /// A bound, non-blocking listener of either kind.
19
+ pub enum Listener {
20
+ Tcp(std::net::TcpListener),
21
+ Unix(UnixListener, PathBuf),
22
+ }
23
+
24
+ impl Listener {
25
+ /// Bind `bind:port` (TCP; a hostname resolves to its addresses and the
26
+ /// first that binds wins) or `unix://path`.
27
+ pub fn bind(bind: &str, port: u16) -> io::Result<Listener> {
28
+ match unix_path(bind) {
29
+ Some(path) => Ok(Listener::Unix(bind_unix(path)?, path.to_path_buf())),
30
+ None => {
31
+ let listener = std::net::TcpListener::bind((bind, port))?;
32
+ listener.set_nonblocking(true)?;
33
+ Ok(Listener::Tcp(listener))
34
+ }
35
+ }
36
+ }
37
+
38
+ /// The bound TCP port; 0 for a unix socket, which has none.
39
+ pub fn port(&self) -> io::Result<u16> {
40
+ match self {
41
+ Listener::Tcp(listener) => Ok(listener.local_addr()?.port()),
42
+ Listener::Unix(..) => Ok(0),
43
+ }
44
+ }
45
+ }
46
+
47
+ /// Bind a unix domain socket at `path`. A path that already exists is
48
+ /// either a live listener (refuse: never steal it) or a stale file left
49
+ /// behind by a crashed process (unlink and reclaim). A connect probe tells
50
+ /// them apart: a successful connect means someone is accepting right now.
51
+ pub fn bind_unix(path: &Path) -> io::Result<UnixListener> {
52
+ match std::os::unix::net::UnixStream::connect(path) {
53
+ Ok(_) => return Err(io::Error::new(io::ErrorKind::AddrInUse, "socket is in use")),
54
+ Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => {
55
+ match std::fs::remove_file(path) {
56
+ Ok(()) => {}
57
+ Err(e) if e.kind() == io::ErrorKind::NotFound => {}
58
+ Err(e) => return Err(e),
59
+ }
60
+ }
61
+ Err(e) if e.kind() == io::ErrorKind::NotFound => {}
62
+ Err(e) => return Err(e),
63
+ }
64
+ let listener = UnixListener::bind(path)?;
65
+ listener.set_nonblocking(true)?;
66
+ Ok(listener)
67
+ }
68
+
69
+ /// Remove a socket file at shutdown; one that is already gone is fine.
70
+ pub fn cleanup_unix(path: &Path) {
71
+ let _ = std::fs::remove_file(path);
72
+ }
73
+
74
+ #[cfg(test)]
75
+ mod tests {
76
+ use super::{bind_unix, unix_path, Listener};
77
+ use std::path::PathBuf;
78
+
79
+ /// A socket path unique to this process and test. macOS caps sun_path
80
+ /// at 104 bytes, so the name stays short.
81
+ fn socket_path(name: &str) -> PathBuf {
82
+ let path = std::env::temp_dir().join(format!("kino-{}-{name}.sock", std::process::id()));
83
+ let _ = std::fs::remove_file(&path);
84
+ path
85
+ }
86
+
87
+ #[test]
88
+ fn unix_path_recognises_only_the_unix_scheme() {
89
+ assert_eq!(
90
+ unix_path("unix:///run/kino.sock").unwrap().to_str(),
91
+ Some("/run/kino.sock")
92
+ );
93
+ assert!(unix_path("127.0.0.1").is_none());
94
+ assert!(unix_path("unix.example.com").is_none());
95
+ }
96
+
97
+ #[test]
98
+ fn binds_a_unix_socket_and_reports_no_port() {
99
+ let path = socket_path("bind");
100
+ let listener = Listener::bind(&format!("unix://{}", path.display()), 9292).unwrap();
101
+ assert!(matches!(listener, Listener::Unix(..)));
102
+ assert_eq!(listener.port().unwrap(), 0);
103
+ assert!(std::fs::metadata(&path).is_ok());
104
+ drop(listener);
105
+ let _ = std::fs::remove_file(&path);
106
+ }
107
+
108
+ #[test]
109
+ fn reclaims_a_stale_socket_file() {
110
+ let path = socket_path("stale");
111
+ // Dropping a listener closes the socket but leaves its file behind,
112
+ // exactly what a crashed process leaves.
113
+ drop(bind_unix(&path).unwrap());
114
+ assert!(std::fs::metadata(&path).is_ok());
115
+ let reclaimed = bind_unix(&path).unwrap();
116
+ drop(reclaimed);
117
+ let _ = std::fs::remove_file(&path);
118
+ }
119
+
120
+ #[test]
121
+ fn refuses_a_socket_someone_is_listening_on() {
122
+ let path = socket_path("live");
123
+ let live = bind_unix(&path).unwrap();
124
+ let err = bind_unix(&path).unwrap_err();
125
+ assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse);
126
+ assert!(err.to_string().contains("in use"));
127
+ drop(live);
128
+ let _ = std::fs::remove_file(&path);
129
+ }
130
+
131
+ #[test]
132
+ fn binds_tcp_on_an_ephemeral_port() {
133
+ let listener = Listener::bind("127.0.0.1", 0).unwrap();
134
+ assert!(matches!(listener, Listener::Tcp(_)));
135
+ assert_ne!(listener.port().unwrap(), 0);
136
+ }
137
+ }