Files
zeekayandHanzo Dev 679ae61163 feat(transport): address the engine and the SDK, so both can use a socket
The engine took a PORT and the SDK an Options.HostPort, so neither end
could use a unix socket for traffic that never leaves the host. The
field name was not the only obstacle: luxfi/zap bound net.Listen("tcp",
":port") and reached a peer with net.DialTimeout("tcp", addr), so a path
was refused at the dialer with "missing port in address" and could not
be bound at all. That is fixed upstream (luxfi/zap Address + Network);
this pins the commit until it is tagged.

EmbedConfig.Address, Embedded.Address, ServeGated(addr) and
client.Options.Address each take ONE address, because a port is a detail
of one transport rather than a thing every listener has. A path binds or
dials a unix socket; anything else is host:port.

TestDialRequiresHostPort became TestDialRequiresAddress: Dial needs an
address, and asserts a socket path is refused for being absent rather
than for being portless. TestE2E_UnixSocket runs a real workflow over a
socket end to end, and TestE2E_UnixSocketRebind proves the socket is
unlinked on stop so a restart binds cleanly.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 16:27:19 -07:00

148 lines
3.9 KiB
Go

package workflow
// Selector is the multi-way choice primitive. Build one with
// NewSelector(ctx), attach cases with AddFuture / AddReceive /
// AddDefault, then block on Select(ctx). Exactly one case fires per
// Select call.
//
// Mirrors go.temporal.io/sdk/workflow.Selector exactly.
type Selector interface {
AddFuture(f Future, fn func(f Future)) Selector
AddReceive(ch ReceiveChannel, fn func(ch ReceiveChannel, more bool)) Selector
AddDefault(fn func()) Selector
Select(ctx Context)
}
// NewSelector returns a Selector that blocks on Select until one of
// the attached cases fires. The ctx is captured only for the env
// handle; Select takes its own ctx so callers can pair a Selector
// with a cancel-scoped ctx for one iteration of a loop.
func NewSelector(ctx Context) Selector {
return &selector{
ctx: ctx,
}
}
type selectorCase struct {
kind int // 0 = future, 1 = receive, 2 = default
future Future
futCB func(Future)
ch ReceiveChannel
chCB func(ReceiveChannel, bool)
defCB func()
}
const (
caseFuture = 0
caseReceive = 1
caseDefault = 2
)
type selector struct {
ctx Context
cases []selectorCase
def *selectorCase
}
func (s *selector) AddFuture(f Future, fn func(f Future)) Selector {
s.cases = append(s.cases, selectorCase{kind: caseFuture, future: f, futCB: fn})
return s
}
func (s *selector) AddReceive(ch ReceiveChannel, fn func(ch ReceiveChannel, more bool)) Selector {
s.cases = append(s.cases, selectorCase{kind: caseReceive, ch: ch, chCB: fn})
return s
}
func (s *selector) AddDefault(fn func()) Selector {
s.def = &selectorCase{kind: caseDefault, defCB: fn}
return s
}
// Select blocks until one case fires, then invokes its callback.
// Inside the callback the caller may still call f.Get(ctx, &v) for
// the ready future or ch.Receive(ctx, &v) for the ready channel,
// exactly as users expect.
//
// When a default is attached, Select returns immediately with the
// default callback if no other case is ready.
func (s *selector) Select(ctx Context) {
if len(s.cases) == 0 {
if s.def != nil {
s.def.defCB()
}
return
}
// Build the env SelectCase list. Default is handled separately.
envCases := make([]SelectCase, 0, len(s.cases))
for _, c := range s.cases {
switch c.kind {
case caseFuture:
envCases = append(envCases, SelectCase{Future: c.future})
case caseReceive:
envCases = append(envCases, SelectCase{Channel: c.ch})
}
}
// Fast path: if a default is attached, poll once and only
// fall through to the env's blocking Select if no case is
// ready. This is what Temporal does too.
if s.def != nil {
if idx := readyIndex(envCases); idx >= 0 {
s.fire(s.cases[idx])
return
}
s.def.defCB()
return
}
idx := ctx.env().Select(envCases)
if idx < 0 || idx >= len(s.cases) {
return
}
s.fire(s.cases[idx])
}
// fire invokes the matched callback. For a receive case, it
// reports whether the channel is still open (the `more` argument
// that chan-close semantics require).
func (s *selector) fire(c selectorCase) {
switch c.kind {
case caseFuture:
if c.futCB != nil {
c.futCB(c.future)
}
case caseReceive:
more := true
// Phase 1 heuristic: if the channel is an unbuffered impl,
// it knows its closed state. For external channels we pass
// `more=true` and rely on the user calling Receive in the
// callback to discover the close.
if ch, ok := c.ch.(*chanImpl); ok {
ch.mu.Lock()
// Mirror the Receive rule: "drained and closed" means no more.
drainedClosed := ch.closed && len(ch.buf) == 0 && len(ch.sendWaiters) == 0
ch.mu.Unlock()
more = !drainedClosed
}
if c.chCB != nil {
c.chCB(c.ch, more)
}
}
}
// readyIndex scans cases for one that is immediately ready. Returns
// the first ready index or -1. Used for the AddDefault fast path.
func readyIndex(cases []SelectCase) int {
for i, c := range cases {
if c.Future != nil && c.Future.IsReady() {
return i
}
if ch, ok := c.Channel.(*chanImpl); ok && ch.hasValue() {
return i
}
}
return -1
}