| 1 | diff --git a/bin/tep b/bin/tep |
| 2 | index 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 | |
| 45 | diff --git a/tep.rb b/tep.rb |
| 46 | index 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) |
| 61 | diff --git a/tep/app.rb b/tep/app.rb |
| 62 | index 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 |
| 87 | diff --git a/tep/net.rb b/tep/net.rb |
| 88 | index 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). |
| 101 | diff --git a/tep/server.rb b/tep/server.rb |
| 102 | index 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 |
| 160 | diff --git a/tep/sphttp.c b/tep/sphttp.c |
| 161 | index 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 | |