async: remove pseudo-async http client

This commit is contained in:
Pierre Tachoire
2024-11-15 14:57:44 +01:00
parent 6809bb5393
commit 7fed1f3015
6 changed files with 3 additions and 2203 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const io = std.io;
const assert = std.debug.assert;
const tcp = @import("tcp.zig");
pub const Stream = struct {
alloc: std.mem.Allocator,
conn: *tcp.Conn,
handle: posix.socket_t,
pub fn close(self: Stream) void {
posix.close(self.handle);
self.alloc.destroy(self.conn);
}
pub const ReadError = posix.ReadError;
pub const WriteError = posix.WriteError;
pub const Reader = io.Reader(Stream, ReadError, read);
pub const Writer = io.Writer(Stream, WriteError, write);
pub fn reader(self: Stream) Reader {
return .{ .context = self };
}
pub fn writer(self: Stream) Writer {
return .{ .context = self };
}
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
else => return error.Unexpected,
};
}
pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize {
return posix.readv(s.handle, iovecs);
}
/// Returns the number of bytes read. If the number read is smaller than
/// `buffer.len`, it means the stream reached the end. Reaching the end of
/// a stream is not an error condition.
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
return readAtLeast(s, buffer, buffer.len);
}
/// Returns the number of bytes read, calling the underlying read function
/// the minimal number of times until the buffer has at least `len` bytes
/// filled. If the number read is less than `len` it means the stream
/// reached the end. Reaching the end of the stream is not an error
/// condition.
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try s.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
/// file system thread instead of non-blocking. It needs to be reworked to properly
/// use non-blocking I/O.
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
error.AccessDenied => error.AccessDenied,
error.WouldBlock => error.WouldBlock,
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
error.MessageTooBig => error.FileTooBig,
error.BrokenPipe => error.BrokenPipe,
else => return error.Unexpected,
};
}
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writev`.
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
if (iovecs.len == 0) return 0;
const first_buffer = iovecs[0].base[0..iovecs[0].len];
return try self.write(first_buffer);
}
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writevAll`.
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].len) {
amt -= iovecs[i].len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].base += amt;
iovecs[i].len -= amt;
}
}
};

View File

@@ -1,112 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const net = std.net;
const Stream = @import("stream.zig").Stream;
const Loop = @import("jsruntime").Loop;
const NetworkImpl = Loop.Network(Conn.Command);
// Conn is a TCP connection using jsruntime Loop async I/O.
// connect, send and receive are blocking, but use async I/O in the background.
// Client doesn't own the socket used for the connection, the caller is
// responsible for closing it.
pub const Conn = struct {
const Command = struct {
impl: NetworkImpl,
done: bool = false,
err: ?anyerror = null,
ln: usize = 0,
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
self.err = err;
self.ln = ln;
self.done = true;
}
fn wait(self: *Command) !usize {
while (!self.done) try self.impl.tick();
if (self.err) |err| return err;
return self.ln;
}
pub fn onConnect(self: *Command, err: ?anyerror) void {
self.ok(err, 0);
}
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
};
loop: *Loop,
pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.connect(&cmd, socket, address);
_ = try cmd.wait();
}
pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.send(&cmd, socket, buffer);
return try cmd.wait();
}
pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.receive(&cmd, socket, buffer);
return try cmd.wait();
}
};
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
// TODO async resolve
const list = try net.getAddressList(alloc, name, port);
defer list.deinit();
if (list.addrs.len == 0) return error.UnknownHostName;
for (list.addrs) |addr| {
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
error.ConnectionRefused => {
continue;
},
else => return err,
};
}
return std.posix.ConnectError.ConnectionRefused;
}
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
errdefer std.posix.close(sockfd);
var conn = try alloc.create(Conn);
conn.* = Conn{ .loop = loop };
try conn.connect(sockfd, addr);
return Stream{
.alloc = alloc,
.conn = conn,
.handle = sockfd,
};
}

View File

@@ -1,189 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const http = std.http;
const Client = @import("Client.zig");
const Request = @import("Client.zig").Request;
pub const Loop = @import("jsruntime").Loop;
const url = "https://w3.org";
test "blocking mode fetch API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
const res = try client.fetch(.{
.location = .{ .uri = try std.Uri.parse(url) },
});
try std.testing.expect(res.status == .ok);
}
test "blocking mode open/send/wait API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var buf: [2014]u8 = undefined;
var req = try client.open(.GET, try std.Uri.parse(url), .{
.server_header_buffer = &buf,
});
defer req.deinit();
try req.send();
try req.finish();
try req.wait();
try std.testing.expect(req.response.status == .ok);
}
// Example how to write an async http client using the modified standard client.
const AsyncClient = struct {
cli: Client,
const YieldImpl = Loop.Yield(AsyncRequest);
const AsyncRequest = struct {
const State = enum { new, open, send, finish, wait, done };
cli: *Client,
uri: std.Uri,
req: ?Request = undefined,
state: State = .new,
impl: YieldImpl,
err: ?anyerror = null,
buf: [2014]u8 = undefined,
pub fn deinit(self: *AsyncRequest) void {
if (self.req) |*r| r.deinit();
}
pub fn fetch(self: *AsyncRequest) void {
self.state = .new;
return self.impl.yield(self);
}
fn onerr(self: *AsyncRequest, err: anyerror) void {
self.state = .done;
self.err = err;
}
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
if (err) |e| return self.onerr(e);
switch (self.state) {
.new => {
self.state = .open;
self.req = self.cli.open(.GET, self.uri, .{
.server_header_buffer = &self.buf,
}) catch |e| return self.onerr(e);
},
.open => {
self.state = .send;
self.req.?.send() catch |e| return self.onerr(e);
},
.send => {
self.state = .finish;
self.req.?.finish() catch |e| return self.onerr(e);
},
.finish => {
self.state = .wait;
self.req.?.wait() catch |e| return self.onerr(e);
},
.wait => {
self.state = .done;
return;
},
.done => return,
}
return self.impl.yield(self);
}
pub fn wait(self: *AsyncRequest) !void {
while (self.state != .done) try self.impl.tick();
if (self.err) |err| return err;
}
};
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
return .{
.cli = .{
.allocator = alloc,
.loop = loop,
},
};
}
pub fn deinit(self: *AsyncClient) void {
self.cli.deinit();
}
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
return .{
.impl = YieldImpl.init(self.cli.loop),
.cli = &self.cli,
.uri = uri,
};
}
};
test "non blocking client" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client = AsyncClient.init(alloc, &loop);
defer client.deinit();
var reqs: [3]AsyncClient.AsyncRequest = undefined;
for (0..reqs.len) |i| {
reqs[i] = try client.createRequest(try std.Uri.parse(url));
reqs[i].fetch();
}
for (0..reqs.len) |i| {
try reqs[i].wait();
reqs[i].deinit();
}
}

View File

@@ -29,7 +29,7 @@ const html_test = @import("html_test.zig").html;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
const Client = @import("async/Client.zig");
const Client = @import("http/async/main.zig").Client;
var doc: *parser.DocumentHTML = undefined;
@@ -41,7 +41,7 @@ fn execJS(
try js_env.start();
defer js_env.stop();
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
try js_env.setUserContext(UserContext{

View File

@@ -298,7 +298,7 @@ test {
const msgTest = @import("msg.zig");
std.testing.refAllDecls(msgTest);
const asyncTest = @import("async/test.zig");
const asyncTest = @import("http/async/std/http.zig");
std.testing.refAllDecls(asyncTest);
const dumpTest = @import("browser/dump.zig");