carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1diff --git a/bin/tep b/bin/tep
2index a66fcb1..1c34e08 100755
3--- a/bin/tep
4+++ b/bin/tep
5@@ -898,6 +898,7 @@ def translate(input_path, lax: ENV["TEP_LAX"] == "1")
6 out << <<~RB
7 __port = #{config[:port] || 4567}
8 __workers = #{config[:workers] || 1}
9+ __sockpath = ""
10 __quiet = false
11 __scheduled = #{scheduled ? 'true' : 'false'}
12 __i = 0
13@@ -909,6 +910,18 @@ def translate(input_path, lax: ENV["TEP_LAX"] == "1")
14 elsif __a == "-w" && __i + 1 < ARGV.length
15 __workers = ARGV[__i + 1].to_i
16 __i += 2
17+ elsif __a == "--socket" && __i + 1 < ARGV.length
18+ # Serve over this unix-domain socket path instead of TCP (the
19+ # argv deployment platforms like CARLOS spawn instances with).
20+ __sockpath = ARGV[__i + 1]
21+ __i += 2
22+ elsif __a == "--db" && __i + 1 < ARGV.length
23+ # Platform-supplied DB path; tolerated so a platform can pass it
24+ # unconditionally. Apps that want it read it from ARGV themselves.
25+ __i += 2
26+ elsif __a == "serve"
27+ # Tolerated no-op: platform unit templates default to `<bin> serve`.
28+ __i += 1
29 elsif __a == "-q"
30 __quiet = true
31 __i += 1
32@@ -921,7 +934,11 @@ def translate(input_path, lax: ENV["TEP_LAX"] == "1")
33 __i += 1
34 end
35 end
36- Tep.run!(__port, __workers, __quiet, __scheduled)
37+ if __sockpath.length > 0
38+ Tep.run_unix!(__sockpath, __quiet)
39+ else
40+ Tep.run!(__port, __workers, __quiet, __scheduled)
41+ end
42 RB
43 out << ""
44
45diff --git a/tep.rb b/tep.rb
46index 92a8ae4..7fb112a 100644
47--- a/tep.rb
48+++ b/tep.rb
49@@ -903,6 +903,11 @@ module Tep
50 # accepts the call without the 4th arg only because it supports
51 # default-value params; without this, the 3-arg call silently
52 # miscompiled (matz/spinel arity-warning shape, tep#13).
53+ # Unix-socket twin of run! -- see Server#run_unix.
54+ def self.run_unix!(path, quiet)
55+ Server.new(APP).run_unix(path, quiet)
56+ end
57+
58 def self.run!(port, workers, quiet, scheduled = false)
59 if scheduled
60 Server::Scheduled.new(APP).run(port, workers, quiet)
61diff --git a/tep/app.rb b/tep/app.rb
62index 7668be8..500da09 100644
63--- a/tep/app.rb
64+++ b/tep/app.rb
65@@ -63,6 +63,13 @@ module Tep
66 # Late-seeded for the same reason as openai_backend.
67 attr_accessor :openai_events
68 attr_accessor :asset_bodies, :asset_mimes, :asset_etags
69+ # Serve every response with Connection: close (no keep-alive reuse).
70+ # Set by Server#run_unix: that mode is a single blocking worker, and
71+ # a reverse proxy's idle keep-alive connection would wedge it — the
72+ # worker blocks reading the idle socket while every other connection
73+ # queues behind it (observed live behind CARLOS's activator proxy:
74+ # intermittent hangs + dropped POSTs until Go's 90s idle timeout).
75+ attr_accessor :close_per_request
76 attr_accessor :sched_fibers, :sched_wake_at, :sched_current
77 attr_accessor :sched_io_fd, :sched_io_mode, :sched_io_ready
78 # Tep::Job background-worker idempotency flag. App-level so a
79@@ -113,6 +120,7 @@ module Tep
80 @asset_mimes = Tep.str_hash # by Tep::Assets._add lines
81 # the bin/tep translator emits)
82 @asset_etags = Tep.str_hash # path -> content-hash ETag (#152)
83+ @close_per_request = false
84 # FiberSlot array for the cooperative scheduler. Initialise
85 # with a noop-bodied slot to pin the array element type, then
86 # drop it. Each slot holds one Fiber + a timer entry in the
87diff --git a/tep/net.rb b/tep/net.rb
88index 45d22e9..7a20724 100644
89--- a/tep/net.rb
90+++ b/tep/net.rb
91@@ -24,6 +24,9 @@ module Sock
92 ffi_lib "crypto"
93
94 ffi_func :sphttp_listen, [:int, :int], :int
95+ # Unix-domain listener: bind+listen on a filesystem path (see
96+ # sphttp.c). -1 on failure; accept/read/write reuse the same fd paths.
97+ ffi_func :sphttp_listen_unix, [:str], :int
98 ffi_func :sphttp_accept, [:int], :int
99 # Non-blocking accept variant used by Tep::Server::Scheduled.
100 # Listen fd must be in non-blocking mode (sphttp_set_nonblock).
101diff --git a/tep/server.rb b/tep/server.rb
102index 1bec928..8e81dea 100644
103--- a/tep/server.rb
104+++ b/tep/server.rb
105@@ -96,6 +96,31 @@ module Tep
106 end
107 end
108
109+
110+ # Serve over a unix-domain socket path: the shape deployment
111+ # platforms hand an instance app (`--socket <path>`). One process,
112+ # one listener -- prefork/SO_REUSEPORT does not apply to AF_UNIX,
113+ # and socket-path apps are typically hibernated single instances.
114+ def run_unix(path, quiet)
115+ if !quiet
116+ puts "[tep " + VERSION + "] listening on unix:" + path
117+ end
118+ # One blocking worker serves this socket: an idle kept-alive proxy
119+ # connection would starve every other connection. Close per request;
120+ # re-dialing a unix socket is effectively free.
121+ Tep::APP.close_per_request = true
122+ Sock.sphttp_install_term_handlers
123+ sfd = Sock.sphttp_listen_unix(path)
124+ if sfd < 0
125+ puts "tep: bind failed on " + path
126+ exit(1)
127+ end
128+ worker_loop(sfd)
129+ if Sock.sphttp_shutdown_requested != 0
130+ Tep.on_shutdown
131+ end
132+ end
133+
134 def worker_loop(sfd)
135 loop do
136 client = Sock.sphttp_accept(sfd)
137@@ -151,7 +176,7 @@ module Tep
138 res = Response.new
139 @app.dispatch(req, res)
140
141- keep_alive = req.keep_alive? && !res.halted_close?
142+ keep_alive = req.keep_alive? && !res.halted_close? && !Tep::APP.close_per_request
143 write_response(client, req, res, keep_alive)
144 keep_going = keep_alive
145 end
146@@ -240,7 +265,12 @@ module Tep
147 if !res.headers.key?("Content-Type") && res.status != 204 && res.status != 304
148 res.headers["Content-Type"] = "text/html; charset=utf-8"
149 end
150- res.headers["Content-Length"] = res.body.length.to_s
151+ # bytesize, not length: Content-Length is an OCTET count, and a
152+ # body with multi-byte UTF-8 (an embedded asset, upstream API
153+ # text) otherwise declares fewer bytes than are written -- the
154+ # client truncates the tail (seen live: a bundled .js losing its
155+ # closing braces, killing the whole script).
156+ res.headers["Content-Length"] = res.body.bytesize.to_s
157 if keep_alive
158 res.headers["Connection"] = "keep-alive"
159 else
160diff --git a/tep/sphttp.c b/tep/sphttp.c
161index 6b7863e..7af1b04 100644
162--- a/tep/sphttp.c
163+++ b/tep/sphttp.c
164@@ -24,6 +24,7 @@
165 #include <sys/wait.h>
166 #include <sys/stat.h>
167 #include <netinet/in.h>
168+#include <sys/un.h>
169 #include <netinet/tcp.h>
170 #include <arpa/inet.h>
171 #include <signal.h>
172@@ -126,6 +127,26 @@ int sphttp_sleep_ms(int ms) {
173 * term-flag-aware pre-check + EINTR handling as the old body; the
174 * shutdown flag is now sp_net's (set via sp_net_install_term_handlers,
175 * which sphttp_install_term_handlers delegates to). */
176+
177+/* Unix-domain listener (deployment platforms that hand an app a socket
178+ * path -- e.g. CARLOS spawns instances as `<bin> --socket <path>` and
179+ * proxies HTTP over it). Unlinks a stale path, binds, listens. Single
180+ * fd, no SO_REUSEPORT (kernel load-balancing is a TCP concern; unix
181+ * instance apps run one process). Returns the listening fd or -1. */
182+int sphttp_listen_unix(const char *path) {
183+ struct sockaddr_un addr;
184+ if (strlen(path) >= sizeof(addr.sun_path)) return -1;
185+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
186+ if (fd < 0) return -1;
187+ memset(&addr, 0, sizeof(addr));
188+ addr.sun_family = AF_UNIX;
189+ strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
190+ unlink(path);
191+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return -1; }
192+ if (listen(fd, 128) < 0) { close(fd); unlink(path); return -1; }
193+ return fd;
194+}
195+
196 int sphttp_listen(int port, int reuseport) { return sp_net_listen(port, reuseport); }
197 int sphttp_accept(int sfd) { return sp_net_accept(sfd); }
198 int sphttp_accept_nb(int sfd) { return sp_net_accept_nb(sfd); }
199