+3
-3
backend/src/http.zig
+3
-3
backend/src/http.zig
···
180
180
break :blk 0;
181
181
};
182
182
183
-
const opt_text = if (opt == .string) opt.string else "";
183
+
// use json.fmt to properly escape quotes and special chars
184
184
try response.print(alloc,
185
-
\\{{"text":"{s}","count":{d}}}
186
-
, .{ opt_text, count });
185
+
\\{{"text":{f},"count":{d}}}
186
+
, .{ json.fmt(opt, .{}), count });
187
187
}
188
188
189
189
try response.print(alloc,
+40
-6
backend/src/main.zig
+40
-6
backend/src/main.zig
···
1
1
const std = @import("std");
2
2
const net = std.net;
3
+
const posix = std.posix;
3
4
const Thread = std.Thread;
4
5
const db = @import("db.zig");
5
6
const http_server = @import("http.zig");
6
7
const tap = @import("tap.zig");
7
8
9
+
// max concurrent http connections (prevents resource exhaustion)
10
+
const MAX_HTTP_WORKERS = 16;
11
+
12
+
// socket timeout in seconds
13
+
const SOCKET_TIMEOUT_SECS = 30;
14
+
8
15
pub fn main() !void {
9
16
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
10
17
defer _ = gpa.deinit();
11
18
const allocator = gpa.allocator();
12
19
13
20
// init sqlite - use DATA_PATH env or default to /data/pollz.db
14
-
const db_path = std.posix.getenv("DATA_PATH") orelse "/data/pollz.db";
21
+
const db_path = posix.getenv("DATA_PATH") orelse "/data/pollz.db";
15
22
try db.init(db_path);
16
23
defer db.close();
17
24
18
-
// tap handles backfill automatically - no need to call backfill.run()
19
-
20
25
// start tap consumer in background
21
26
const tap_thread = try Thread.spawn(.{}, tap.consumer, .{allocator});
22
27
defer tap_thread.join();
23
28
29
+
// init thread pool for http connections
30
+
var pool: Thread.Pool = undefined;
31
+
try pool.init(.{
32
+
.allocator = allocator,
33
+
.n_jobs = MAX_HTTP_WORKERS,
34
+
});
35
+
defer pool.deinit();
36
+
24
37
// start http server (bind to 0.0.0.0 for containerized deployments)
25
38
const address = try net.Address.parseIp("0.0.0.0", 3000);
26
39
var server = try address.listen(.{ .reuse_address = true });
27
40
defer server.deinit();
28
41
29
-
std.debug.print("pollz backend listening on http://127.0.0.1:3000\n", .{});
42
+
std.debug.print("pollz backend listening on http://127.0.0.1:3000 (max {} workers)\n", .{MAX_HTTP_WORKERS});
30
43
31
44
while (true) {
32
-
const conn = try server.accept();
33
-
_ = try Thread.spawn(.{}, http_server.handleConnection, .{conn});
45
+
const conn = server.accept() catch |err| {
46
+
std.debug.print("accept error: {}\n", .{err});
47
+
continue;
48
+
};
49
+
50
+
// set socket timeouts to prevent slow client attacks
51
+
setSocketTimeout(conn.stream.handle, SOCKET_TIMEOUT_SECS) catch |err| {
52
+
std.debug.print("failed to set socket timeout: {}\n", .{err});
53
+
};
54
+
55
+
pool.spawn(http_server.handleConnection, .{conn}) catch |err| {
56
+
std.debug.print("pool spawn error: {}\n", .{err});
57
+
conn.stream.close();
58
+
};
34
59
}
35
60
}
61
+
62
+
fn setSocketTimeout(fd: posix.fd_t, secs: u32) !void {
63
+
const timeout = std.mem.toBytes(posix.timeval{
64
+
.sec = @intCast(secs),
65
+
.usec = 0,
66
+
});
67
+
try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout);
68
+
try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout);
69
+
}
+1
-1
readme.md
+1
-1
readme.md
···
11
11
## stack
12
12
13
13
- [tap](https://github.com/bluesky-social/atproto/tree/main/packages/tap) - firehose sync
14
-
- [zig](https://ziglang.org) + [httpz](https://github.com/ikskuh/http.zig) - backend
14
+
- [zig](https://ziglang.org) + [zqlite](https://github.com/karlseguin/zqlite.zig) - backend
15
15
- [atcute](https://github.com/mary-ext/atcute) - atproto client
16
16
- [fly.io](https://fly.io) - backend hosting
17
17
- [cloudflare pages](https://pages.cloudflare.com) - frontend hosting