gate: a missing money plane is inert, an unreachable one fails closed
Gate's own comment named the distinction — "nobody bills in this deployment" versus "the biller is one socket away" — and the code never made it. gatePeer treated a missing socket as an error, so a deployment with no commerce at all 503'd every priced act, which is the behaviour "billing not configured" was never supposed to have. The socket is what answers it. No socket means this deployment does not run commerce, so the gate is inert, exactly as before the split. A socket that exists and does not answer is a real fault and still fails closed — which is the direction that matters, because allowing there is what once turned every priced act free, silently. Both directions are pinned: TestGate_NoMoneyPlaneIsInert and TestGate_UnreachableBillerFailsClosed, the second against a real socket that accepts and hangs up. PeerPresent requires a socket, not merely a name on disk. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -159,6 +159,22 @@ func Peer(app string) (*zip.Conn, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// PeerPresent reports whether an app is serving its plane socket here.
|
||||
//
|
||||
// It is the discriminator between the two shapes a failed call can have, and
|
||||
// they need OPPOSITE handling: no socket at all means this deployment does not
|
||||
// run that app — the legitimate split-deploy or no-money-plane case, where a
|
||||
// caller falls back or stays inert — while a socket that exists and does not
|
||||
// answer is a real fault, which must surface rather than be papered over.
|
||||
//
|
||||
// Collapsing them is how a gate either 503s a deployment that never billed, or
|
||||
// hands out free work in one that does.
|
||||
func PeerPresent(app string) bool {
|
||||
bindRuntimeDir()
|
||||
fi, err := os.Stat(zip.SocketPath(app))
|
||||
return err == nil && fi.Mode()&os.ModeSocket != 0
|
||||
}
|
||||
|
||||
// Ask is the whole client half: dial the app, invoke the op, close.
|
||||
//
|
||||
// The org a call acts for rides the CALLER — forwarded from the gateway's
|
||||
|
||||
@@ -36,6 +36,18 @@ const (
|
||||
// cap is 402 spend_cap_exceeded, and anything else is unknown — which the
|
||||
// fail-closed caller turns into 503 rather than free work.
|
||||
func (rm *ResourceMeter) gatePeer(ctx context.Context, org, project string, projectValidated bool, costCents int64) error {
|
||||
if !PeerPresent(peerCommerce) {
|
||||
// No local ledger AND no commerce serving one: this deployment has no money
|
||||
// plane at all, which is a legitimate shape and the behaviour "billing not
|
||||
// configured" always had. Refusing here would 503 every priced act in a
|
||||
// deployment that never intended to bill.
|
||||
//
|
||||
// It is NOT the free-work hole: where commerce DOES run, its socket exists,
|
||||
// so this branch is not reached and an unreachable biller still fails closed
|
||||
// below. The distinction is "nobody bills here" versus "the biller is one
|
||||
// socket away", and the socket is what answers it.
|
||||
return nil
|
||||
}
|
||||
in := plane.AuthorizeIn{
|
||||
Subject: org,
|
||||
Amount: plane.Amount(money.FromUSD(costCents)),
|
||||
@@ -74,6 +86,9 @@ func (rm *ResourceMeter) gatePeer(ctx context.Context, org, project string, proj
|
||||
// logged for reconciliation rather than swallowed — an unbilled create is a number
|
||||
// somebody has to find later, so it says so now.
|
||||
func (rm *ResourceMeter) meterPeer(org, kind string, u metering.Usage) {
|
||||
if !PeerPresent(peerCommerce) {
|
||||
return // nothing bills in this deployment; there is no debit to lose
|
||||
}
|
||||
in := plane.RecordIn{
|
||||
Subject: org,
|
||||
Amount: plane.Amount(money.FromUSD(u.AmountCents)),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hanzoai/cloud/apps/metering"
|
||||
luxlog "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// The gate's two failure shapes need OPPOSITE answers, and getting them the
|
||||
// wrong way round is how a fleet either 503s a deployment that never billed or
|
||||
// hands out free work in one that does. Both directions are pinned here.
|
||||
|
||||
func unconfiguredMeter(t *testing.T) *ResourceMeter {
|
||||
t.Helper()
|
||||
m, err := metering.New(metering.Config{}) // empty BaseURL ⇒ !Enabled()
|
||||
if err != nil {
|
||||
t.Fatalf("metering.New: %v", err)
|
||||
}
|
||||
return &ResourceMeter{m: m, provider: "test", log: luxlog.NewNoOpLogger()}
|
||||
}
|
||||
|
||||
// NO SOCKET: this deployment has no money plane. The gate is inert, which is
|
||||
// what "billing not configured" always meant — refusing would break a
|
||||
// deployment that never intended to bill.
|
||||
func TestGate_NoMoneyPlaneIsInert(t *testing.T) {
|
||||
t.Setenv("ZIP_RUNTIME_DIR", t.TempDir())
|
||||
if err := unconfiguredMeter(t).Gate(context.Background(), "acme", "", false, "fn", 500); err != nil {
|
||||
t.Fatalf("a deployment with no money plane refused a priced act: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SOCKET PRESENT, NOTHING ANSWERING: commerce runs here and is not reachable.
|
||||
// That is a fault, and the gate must FAIL CLOSED — this is the exact shape that
|
||||
// once turned every priced act free, silently.
|
||||
func TestGate_UnreachableBillerFailsClosed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("ZIP_RUNTIME_DIR", dir)
|
||||
|
||||
// A real socket that accepts and says nothing: present, but no answer.
|
||||
ln, err := net.Listen("unix", filepath.Join(dir, "commerce.sock"))
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = c.Close() // accept, then hang up without answering
|
||||
}
|
||||
}()
|
||||
|
||||
if !PeerPresent("commerce") {
|
||||
t.Fatal("a live socket was not seen as present")
|
||||
}
|
||||
err = unconfiguredMeter(t).Gate(context.Background(), "acme", "", false, "fn", 500)
|
||||
if err == nil {
|
||||
t.Fatal("an unreachable biller ALLOWED a priced act — this is the free-work hole")
|
||||
}
|
||||
}
|
||||
|
||||
// A plain file where the socket should be is not a peer. Presence must mean a
|
||||
// socket, not merely a name on disk.
|
||||
func TestPeerPresent_OnlyASocketCounts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("ZIP_RUNTIME_DIR", dir)
|
||||
if err := os.WriteFile(filepath.Join(dir, "commerce.sock"), []byte("not a socket"), 0o600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if PeerPresent("commerce") {
|
||||
t.Fatal("a regular file was read as a live peer")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user