Skip to content

Commit dff4286

Browse files
authored
Merge pull request #280 from Lightbug-HQ/fix/connection-close-parallel-assets
Revert connection close changes
2 parents 5877606 + 8a7914c commit dff4286

7 files changed

Lines changed: 50 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Once you have a Mojo project set up locally,
6060

6161
```toml
6262
[dependencies]
63-
lightbug_http = ">=0.26.1,<0.26.2"
63+
lightbug_http = ">=0.26.1.1,<0.26.2"
6464
```
6565

6666
3. Run `pixi install` at the root of your project, where `pixi.toml` is located

lightbug_http/connection.mojo

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from lightbug_http.c.socket_error import (
1010
RecvfromError,
1111
SendError,
1212
SendtoError,
13+
SetsockoptError,
1314
ShutdownEINVALError,
1415
)
1516
from lightbug_http.c.socket_error import SocketError as CSocketError
@@ -360,18 +361,33 @@ struct TCPConnection[network: NetworkType = NetworkType.tcp4]:
360361
return self.socket.receive(buf)
361362

362363
fn write(self, buf: Span[Byte]) raises SendError -> UInt:
363-
"""Write data to the TCP connection.
364+
"""Write all data to the TCP connection, handling partial sends.
364365
365366
Args:
366367
buf: Buffer containing data to write.
367368
368369
Returns:
369-
Number of bytes written.
370+
Total number of bytes written.
370371
371372
Raises:
372373
SendError: If write fails.
373374
"""
374-
return self.socket.send(buf)
375+
var total_sent: UInt = 0
376+
while total_sent < UInt(len(buf)):
377+
var sent = self.socket.send(buf[Int(total_sent):])
378+
total_sent += sent
379+
return total_sent
380+
381+
fn set_recv_timeout(self, seconds: Int) raises SetsockoptError:
382+
"""Set the receive timeout on this connection's socket.
383+
384+
Args:
385+
seconds: Timeout in seconds. 0 to disable.
386+
387+
Raises:
388+
SetsockoptError: If setting the socket option fails.
389+
"""
390+
self.socket.set_timeout(seconds)
375391

376392
fn close(mut self) raises FatalCloseError:
377393
"""Close the TCP connection.

lightbug_http/server.mojo

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ fn handle_connection[
295295
if read_err.isa[EOF]() or read_err.isa[SocketClosedError]():
296296
provision.state = ConnectionState.closed()
297297
break
298+
# On keep-alive connections, treat timeout (EAGAIN) as clean close
299+
# so the server can accept new connections.
300+
if provision.keepalive_count > 0:
301+
provision.state = ConnectionState.closed()
302+
break
298303
raise read_err^
299304

300305
if bytes_read == 0:
@@ -437,8 +442,8 @@ fn handle_connection[
437442
if (provision.keepalive_count + 1) >= config.max_keepalive_requests:
438443
provision.should_close = True
439444

440-
if provision.should_close:
441-
response.set_connection_close()
445+
# Always send Connection: close for now as the server is single-threaded
446+
response.set_connection_close()
442447

443448
provision.response = response^
444449
provision.state = ConnectionState.responding()
@@ -583,7 +588,6 @@ struct Server(Movable):
583588
# Connection handling failed - just close the connection
584589
pass
585590
finally:
586-
# Always clean up the connection and return provision to pool
587591
try:
588592
conn^.teardown()
589593
except:

lightbug_http/socket.mojo

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from sys.ffi import c_uint
22
from sys.info import CompilationTarget
33

4+
from lightbug_http.c.aliases import c_void
5+
46
from lightbug_http.address import (
57
Addr,
68
NetworkType,
@@ -17,6 +19,7 @@ from lightbug_http.c.socket import (
1719
ShutdownOption,
1820
SocketOption,
1921
SocketType,
22+
_setsockopt,
2023
accept,
2124
bind,
2225
close,
@@ -808,13 +811,25 @@ struct Socket[
808811
"""Return the timeout value for the socket."""
809812
return self.get_socket_option(SocketOption.SO_RCVTIMEO)
810813

811-
fn set_timeout(self, var duration: Int) raises SetsockoptError:
812-
"""Set the timeout value for the socket.
814+
fn set_timeout(self, seconds: Int) raises SetsockoptError:
815+
"""Set the receive timeout for the socket.
813816
814817
Args:
815-
duration: Seconds - The timeout duration in seconds.
818+
seconds: The timeout duration in seconds.
819+
820+
Raises:
821+
SetsockoptError: If setting the socket option fails.
816822
"""
817-
self.set_socket_option(SocketOption.SO_RCVTIMEO, duration)
823+
# SO_RCVTIMEO requires a timeval struct: {tv_sec: Int64, tv_usec: Int64}
824+
# (16 bytes on both macOS and Linux 64-bit).
825+
var timeval = InlineArray[Int64, 2](seconds, 0)
826+
_ = _setsockopt(
827+
self.fd.value,
828+
SOL_SOCKET,
829+
SocketOption.SO_RCVTIMEO.value,
830+
UnsafePointer(to=timeval).bitcast[c_void](),
831+
16,
832+
)
818833

819834

820835
comptime UDPSocket[address: Addr] = Socket[

pixi.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ build_and_publish = [{ task = "build" }, { task = "publish" }]
3434

3535
[package]
3636
name = "lightbug_http"
37-
version = "0.26.1.0"
37+
version = "0.26.1.1"
3838

3939
[package.build]
4040
backend = { name = "pixi-build-mojo", version = "*" }

recipes/recipe.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json
22

33
context:
4-
version: "0.26.1.0"
4+
version: "0.26.1.1"
55

66
package:
77
name: "lightbug_http"
8-
version: 0.26.1.0
8+
version: 0.26.1.1
99

1010
source:
1111
- path: ../lightbug_http

tests/integration/integration_client.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,10 @@
3434
assert response.status_code == 200
3535

3636
print("\n~~~ Testing parallel connections ~~~")
37-
# Browsers open 6+ parallel connections for assets.
38-
# A single-threaded server with keep-alive blocks on conn.read() waiting for
39-
# the next request, preventing other connections from being accepted.
40-
# This test verifies all parallel requests complete within a reasonable time.
4137

4238

4339
def fetch(path):
44-
return requests.get(f"http://127.0.0.1:8080{path}", headers={"connection": "close"})
40+
return requests.get(f"http://127.0.0.1:8080{path}")
4541

4642

4743
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:

0 commit comments

Comments
 (0)