79 lines
3 KiB
Go
79 lines
3 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"testing"
|
|
)
|
|
|
|
// spendlineorder_test.go pins ONE lock order, and it pins it in the only place where breaking it is
|
|
// invisible: the source.
|
|
//
|
|
// The order is stated at both of its ends (emitter.mu, spendLine): the emitter's mutex is taken BEFORE
|
|
// the store's write connection and never after one, because the mutex is held across store writes. The
|
|
// spend-line closure is the single piece of this package's code that executes while the write connection
|
|
// is held, so it is the single place the order can be violated — and the violation does not look like a
|
|
// deadlock: the store's op timeout breaks the cycle ten seconds later, and the failure surfaces as
|
|
// `context deadline exceeded` in whatever unrelated tests happen to be running (backlog row 387,
|
|
// measured at thirteen).
|
|
//
|
|
// A comment cannot hold that. What holds it is the closure having no `e` in scope — and what tells the
|
|
// next person they have just put it back is this test.
|
|
func TestTheSpendLineClosureCannotReachTheEmitter(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
file, err := parser.ParseFile(fset, "events.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse events.go: %v", err)
|
|
}
|
|
var fn *ast.FuncDecl
|
|
for _, d := range file.Decls {
|
|
f, ok := d.(*ast.FuncDecl)
|
|
if ok && f.Name.Name == "spendLine" && f.Recv != nil {
|
|
fn = f
|
|
break
|
|
}
|
|
}
|
|
if fn == nil {
|
|
t.Fatal("spendLine is not in events.go any more — this pin is looking at the wrong place and is asserting nothing")
|
|
}
|
|
// The receiver's own name, read from the declaration rather than assumed: renaming it must not turn
|
|
// this guard off.
|
|
if len(fn.Recv.List) != 1 || len(fn.Recv.List[0].Names) != 1 {
|
|
t.Fatal("spendLine's receiver has no name — a nameless receiver cannot be captured, but this pin can no longer tell")
|
|
}
|
|
recv := fn.Recv.List[0].Names[0].Name
|
|
|
|
// The closure handed to the store: the FuncLit under the `Line:` key.
|
|
var closure *ast.FuncLit
|
|
ast.Inspect(fn, func(n ast.Node) bool {
|
|
kv, ok := n.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if key, ok := kv.Key.(*ast.Ident); ok && key.Name == "Line" {
|
|
if lit, ok := kv.Value.(*ast.FuncLit); ok {
|
|
closure = lit
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
if closure == nil {
|
|
t.Fatal("spendLine no longer hands a function literal to store.SpendLine.Line — the shape this pin reads is gone, so it proves nothing")
|
|
}
|
|
|
|
var used []string
|
|
ast.Inspect(closure, func(n ast.Node) bool {
|
|
if id, ok := n.(*ast.Ident); ok && id.Name == recv {
|
|
used = append(used, fset.Position(id.Pos()).String())
|
|
}
|
|
return true
|
|
})
|
|
if len(used) > 0 {
|
|
t.Fatalf("the spend-line closure reaches the emitter (%q) at %v.\n"+
|
|
"That closure runs INSIDE the store's write transaction, and the emitter's mutex is held across store writes — "+
|
|
"so any method taken on it from here closes a lock cycle that the store's timeout breaks ten seconds later, "+
|
|
"in whatever tests happen to be running (backlog row 387). Capture what you need by value before the closure instead.",
|
|
recv, used)
|
|
}
|
|
}
|