Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Once you have a Mojo project set up locally,

```toml
[dependencies]
lightbug_http = ">=0.26.1,<0.26.2"
lightbug_http = ">=0.26.1.1,<0.26.2"
```

3. Run `pixi install` at the root of your project, where `pixi.toml` is located
Expand Down
22 changes: 19 additions & 3 deletions lightbug_http/connection.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ from lightbug_http.c.socket_error import (
RecvfromError,
SendError,
SendtoError,
SetsockoptError,
ShutdownEINVALError,
)
from lightbug_http.c.socket_error import SocketError as CSocketError
Expand Down Expand Up @@ -360,18 +361,33 @@ struct TCPConnection[network: NetworkType = NetworkType.tcp4]:
return self.socket.receive(buf)

fn write(self, buf: Span[Byte]) raises SendError -> UInt:
"""Write data to the TCP connection.
"""Write all data to the TCP connection, handling partial sends.

Args:
buf: Buffer containing data to write.

Returns:
Number of bytes written.
Total number of bytes written.

Raises:
SendError: If write fails.
"""
return self.socket.send(buf)
var total_sent: UInt = 0
while total_sent < UInt(len(buf)):
var sent = self.socket.send(buf[Int(total_sent):])
total_sent += sent
return total_sent

fn set_recv_timeout(self, seconds: Int) raises SetsockoptError:
"""Set the receive timeout on this connection's socket.

Args:
seconds: Timeout in seconds. 0 to disable.

Raises:
SetsockoptError: If setting the socket option fails.
"""
self.socket.set_timeout(seconds)

fn close(mut self) raises FatalCloseError:
"""Close the TCP connection.
Expand Down
10 changes: 7 additions & 3 deletions lightbug_http/server.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ fn handle_connection[
if read_err.isa[EOF]() or read_err.isa[SocketClosedError]():
provision.state = ConnectionState.closed()
break
# On keep-alive connections, treat timeout (EAGAIN) as clean close
# so the server can accept new connections.
if provision.keepalive_count > 0:
provision.state = ConnectionState.closed()
break
raise read_err^

if bytes_read == 0:
Expand Down Expand Up @@ -437,8 +442,8 @@ fn handle_connection[
if (provision.keepalive_count + 1) >= config.max_keepalive_requests:
provision.should_close = True

if provision.should_close:
response.set_connection_close()
# Always send Connection: close for now as the server is single-threaded
response.set_connection_close()

provision.response = response^
provision.state = ConnectionState.responding()
Expand Down Expand Up @@ -583,7 +588,6 @@ struct Server(Movable):
# Connection handling failed - just close the connection
pass
finally:
# Always clean up the connection and return provision to pool
try:
conn^.teardown()
except:
Expand Down
23 changes: 19 additions & 4 deletions lightbug_http/socket.mojo
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from sys.ffi import c_uint
from sys.info import CompilationTarget

from lightbug_http.c.aliases import c_void

from lightbug_http.address import (
Addr,
NetworkType,
Expand All @@ -17,6 +19,7 @@ from lightbug_http.c.socket import (
ShutdownOption,
SocketOption,
SocketType,
_setsockopt,
accept,
bind,
close,
Expand Down Expand Up @@ -808,13 +811,25 @@ struct Socket[
"""Return the timeout value for the socket."""
return self.get_socket_option(SocketOption.SO_RCVTIMEO)

fn set_timeout(self, var duration: Int) raises SetsockoptError:
"""Set the timeout value for the socket.
fn set_timeout(self, seconds: Int) raises SetsockoptError:
"""Set the receive timeout for the socket.

Args:
duration: Seconds - The timeout duration in seconds.
seconds: The timeout duration in seconds.

Raises:
SetsockoptError: If setting the socket option fails.
"""
self.set_socket_option(SocketOption.SO_RCVTIMEO, duration)
# SO_RCVTIMEO requires a timeval struct: {tv_sec: Int64, tv_usec: Int64}
# (16 bytes on both macOS and Linux 64-bit).
var timeval = InlineArray[Int64, 2](seconds, 0)
_ = _setsockopt(
self.fd.value,
SOL_SOCKET,
SocketOption.SO_RCVTIMEO.value,
UnsafePointer(to=timeval).bitcast[c_void](),
16,
)


comptime UDPSocket[address: Addr] = Socket[
Expand Down
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ build_and_publish = [{ task = "build" }, { task = "publish" }]

[package]
name = "lightbug_http"
version = "0.26.1.0"
version = "0.26.1.1"

[package.build]
backend = { name = "pixi-build-mojo", version = "*" }
Expand Down
4 changes: 2 additions & 2 deletions recipes/recipe.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json

context:
version: "0.26.1.0"
version: "0.26.1.1"

package:
name: "lightbug_http"
version: 0.26.1.0
version: 0.26.1.1

source:
- path: ../lightbug_http
Expand Down
6 changes: 1 addition & 5 deletions tests/integration/integration_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,10 @@
assert response.status_code == 200

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


def fetch(path):
return requests.get(f"http://127.0.0.1:8080{path}", headers={"connection": "close"})
return requests.get(f"http://127.0.0.1:8080{path}")


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