Skip to content

Commit c0c9008

Browse files
rustyconoverclaude
andcommitted
feat: add TCP transport (--tcp) to worker
Bump vgi-rpc-go to v0.10.0 (adds Server.RunTcp) and expose the raw Arrow-IPC TCP transport through the worker: - Worker.RunTcp(host, port, idleTimeout) serves via Server.RunTcp and prints the TCP:<host>:<port> launcher discovery line with the actual bound port (so ephemeral port==0 binds are discoverable). - New transportTCP server class (concurrent connections, no cleanup hook — same as unix). - Example worker accepts --tcp [HOST:]PORT (host defaults to 127.0.0.1), mutually exclusive with --http/--unix. Registered in filterKnownFlags as a value-consuming flag so the space-separated form parses. Raw TCP framing carries no auth/encryption (loopback/trusted networks only); HTTP remains the transport for untrusted networks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 03ed259 commit c0c9008

4 files changed

Lines changed: 61 additions & 6 deletions

File tree

cmd/vgi-example-worker/main.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log"
1010
"math"
1111
"os"
12+
"strconv"
1213
"strings"
1314
"time"
1415

@@ -26,7 +27,8 @@ import (
2627
func main() {
2728
httpMode := flag.Bool("http", false, "Run as HTTP server instead of stdio")
2829
unixPath := flag.String("unix", "", "Bind to this AF_UNIX socket path (launcher transport); mutually exclusive with --http")
29-
idleTimeout := flag.Float64("idle-timeout", 300, "Self-shutdown after N seconds idle when serving --unix (0 = never)")
30+
tcpAddr := flag.String("tcp", "", "Bind a raw TCP socket ([HOST:]PORT, host defaults to 127.0.0.1, port 0 auto-selects); mutually exclusive with --http/--unix")
31+
idleTimeout := flag.Float64("idle-timeout", 300, "Self-shutdown after N seconds idle when serving --unix/--tcp (0 = never)")
3032
// --describe / --no-describe: accepted for launcher compatibility (the VGI
3133
// extension passes it through). Description pages aren't served over the
3234
// socket/stdio transports, so it is currently a no-op here.
@@ -38,6 +40,7 @@ func main() {
3840
// rather than failing to start. Flags named here consume a value token.
3941
flag.CommandLine.Parse(filterKnownFlags(os.Args[1:], map[string]bool{
4042
"unix": true,
43+
"tcp": true,
4144
"idle-timeout": true,
4245
"log-level": true,
4346
"log-format": true,
@@ -52,8 +55,14 @@ func main() {
5255
// (no-op otherwise); the harness kills pooled/long-lived workers with SIGTERM.
5356
covflush.Start()
5457

55-
if *unixPath != "" && *httpMode {
56-
log.Fatal("--unix and --http are mutually exclusive")
58+
nTransports := 0
59+
for _, on := range []bool{*unixPath != "", *tcpAddr != "", *httpMode} {
60+
if on {
61+
nTransports++
62+
}
63+
}
64+
if nTransports > 1 {
65+
log.Fatal("--unix, --tcp, and --http are mutually exclusive")
5766
}
5867

5968
// Pick the FunctionStorage backend from VGI_WORKER_SHARED_STORAGE.
@@ -936,6 +945,14 @@ func main() {
936945
if err := w.RunUnix(*unixPath, time.Duration(*idleTimeout*float64(time.Second))); err != nil {
937946
log.Fatal(err)
938947
}
948+
case *tcpAddr != "":
949+
host, port, err := parseTCPAddr(*tcpAddr)
950+
if err != nil {
951+
log.Fatal(err)
952+
}
953+
if err := w.RunTcp(host, port, time.Duration(*idleTimeout*float64(time.Second))); err != nil {
954+
log.Fatal(err)
955+
}
939956
case *httpMode:
940957
authFn, jwtCleanup := resolveAuthenticate()
941958
if jwtCleanup != nil {
@@ -955,6 +972,24 @@ func main() {
955972
}
956973
}
957974

975+
// parseTCPAddr parses a "[HOST:]PORT" --tcp bind spec. A bare PORT (no colon)
976+
// binds 127.0.0.1; an empty host (leading ":") also defaults to loopback.
977+
func parseTCPAddr(spec string) (string, int, error) {
978+
host := "127.0.0.1"
979+
portStr := spec
980+
if i := strings.LastIndex(spec, ":"); i >= 0 {
981+
if spec[:i] != "" {
982+
host = spec[:i]
983+
}
984+
portStr = spec[i+1:]
985+
}
986+
port, err := strconv.Atoi(portStr)
987+
if err != nil {
988+
return "", 0, fmt.Errorf("--tcp expects [HOST:]PORT, got %q", spec)
989+
}
990+
return host, port, nil
991+
}
992+
958993
// filterKnownFlags drops command-line tokens for flags this binary doesn't
959994
// define, so launcher-injected argv-differentiation flags (e.g. --threaded,
960995
// --quiet, --debug) don't abort flag parsing. Flags named in valueFlags consume

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ go 1.25.0
1010
replace github.com/apache/arrow-go/v18 => github.com/Query-farm/arrow-go/v18 v18.0.0-20260220022719-2d45cbd918a4
1111

1212
require (
13-
github.com/Query-farm/vgi-rpc-go v0.9.4
13+
github.com/Query-farm/vgi-rpc-go v0.10.0
1414
github.com/Query-farm/vgi-rpc-go/vgirpc/jwtauth v0.9.3
1515
github.com/apache/arrow-go/v18 v18.5.2
1616
github.com/duckdb/duckdb-go/v2 v2.10502.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt
44
github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8=
55
github.com/Query-farm/arrow-go/v18 v18.0.0-20260220022719-2d45cbd918a4 h1:hH37wAlNZG3LEfHxwnlE9aMtFqZndL26cHOEP0nhWJ8=
66
github.com/Query-farm/arrow-go/v18 v18.0.0-20260220022719-2d45cbd918a4/go.mod h1:IJTMBTlHe7cDOhRh0ioGuEKBl5iTR6xPfl5BN4AgirU=
7-
github.com/Query-farm/vgi-rpc-go v0.9.4 h1:Da+0bNrQkTH2rHXfDZ3i52UldUov4xJx+fWe26n/1Lk=
8-
github.com/Query-farm/vgi-rpc-go v0.9.4/go.mod h1:XbQBjp31eFKIZaBqmg0q9r2Mxp/LTPeAG97w1qy5gmI=
7+
github.com/Query-farm/vgi-rpc-go v0.10.0 h1:ddteS2eF/F8Y7ZeyOfRpwjYosVv3MTTyad9U57RuaTs=
8+
github.com/Query-farm/vgi-rpc-go v0.10.0/go.mod h1:XbQBjp31eFKIZaBqmg0q9r2Mxp/LTPeAG97w1qy5gmI=
99
github.com/Query-farm/vgi-rpc-go/vgirpc/jwtauth v0.9.3 h1:9bmMKdhik5PFir5FBQO0yw8dnWBBeDfVpMY5dmQQAGk=
1010
github.com/Query-farm/vgi-rpc-go/vgirpc/jwtauth v0.9.3/go.mod h1:orAtn+oBHDFucdCpnVfvwtUwQSTjWkNrJ+lV3FsP4q4=
1111
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=

vgi/worker.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@ const (
859859
transportStdio serverTransport = iota // single serial stream — deferred cleanup
860860
transportHTTP // independent requests — no deferred cleanup (TTL sweep)
861861
transportUnix // concurrent connections — no cleanup hook (TTL sweep)
862+
transportTCP // concurrent connections — no cleanup hook (TTL sweep)
862863
)
863864

864865
func (w *Worker) buildServer(transport serverTransport) *vgirpc.Server {
@@ -903,6 +904,8 @@ func (w *Worker) buildServer(transport serverTransport) *vgirpc.Server {
903904
s.SetDispatchHook(&storageCleanupHook{worker: w, isHTTP: true})
904905
case transportUnix:
905906
// no cleanup hook
907+
case transportTCP:
908+
// no cleanup hook (concurrent connections, like unix)
906909
}
907910

908911
// Register bind (unary)
@@ -959,6 +962,23 @@ func (w *Worker) RunUnix(path string, idleTimeout time.Duration) error {
959962
})
960963
}
961964

965+
// RunTcp runs the worker serving RPC over a raw TCP socket — the launcher
966+
// transport's AF_INET sibling. It binds host:port (host "" defaults to
967+
// 127.0.0.1; port 0 picks a free port), prints the readiness marker
968+
// "TCP:<host>:<port>" with the actual bound port to stdout once listening
969+
// (then writes nothing further to stdout), and self-shuts-down after
970+
// idleTimeout with no active connections (<=0 disables the timeout).
971+
//
972+
// Raw TCP framing carries no auth/encryption — bind loopback / a trusted
973+
// network only; use RunHttp for untrusted networks.
974+
func (w *Worker) RunTcp(host string, port int, idleTimeout time.Duration) error {
975+
s := w.buildServer(transportTCP)
976+
return s.RunTcp(host, port, idleTimeout, func(boundHost string, boundPort int) {
977+
fmt.Printf("TCP:%s:%d\n", boundHost, boundPort)
978+
os.Stdout.Sync()
979+
})
980+
}
981+
962982
// RunHttp runs the worker serving RPC over HTTP. It listens on the given
963983
// address (e.g. "127.0.0.1:0" for a random port) and prints "PORT:<n>" to
964984
// stdout so callers can discover the assigned port.

0 commit comments

Comments
 (0)