feat: fail CI when pubsub.Publish calls are found in db transactions (#17903)

Publishing inside a db transaction can lead to database connection
starvation/contention since it requires its own connection.

This ruleguard rule (one-shotted by Claude Sonnet 3.7 and finalized by
@Emyrk) will detect two of the following 3 instances:

```go
type Nested struct {
	ps pubsub.Pubsub
}

func TestFail(t *testing.T) {
	t.Parallel()

	db, ps := dbtestutil.NewDB(t)
	nested := &Nested{
		ps: ps,
	}

	// will catch this
	_ = db.InTx(func(_ database.Store) error {
		_, _ = fmt.Printf("")
		_ = ps.Publish("", []byte{})
		return nil
	}, nil)

	// will catch this
	_ = db.InTx(func(_ database.Store) error {
		_ = nested.ps.Publish("", []byte{})
		return nil
	}, nil)

	// will NOT catch this
	_ = db.InTx(func(_ database.Store) error {
		blah(ps)
		return nil
	}, nil)
}

func blah(ps pubsub.Pubsub) {
	ps.Publish("", []byte{})
}
```

The ruleguard doesn't recursively introspect function calls so only the
first two cases will be guarded against, but it's better than nothing.

<img width="1444" alt="image"
src="https://github.com/user-attachments/assets/8ffa0d88-16a0-41a9-9521-21211910dec9"
/>

---------

Signed-off-by: Danny Kopping <dannykopping@gmail.com>
Co-authored-by: Steven Masley <stevenmasley@gmail.com>
This commit is contained in:
Danny Kopping
2025-05-19 16:52:51 +02:00
committed by GitHub
parent 4412f194d4
commit 87dc2478a9

View File

@ -134,6 +134,42 @@ func databaseImport(m dsl.Matcher) {
Where(m.File().PkgPath.Matches("github.com/coder/coder/v2/codersdk"))
}
// publishInTransaction detects calls to Publish inside database transactions
// which can lead to connection starvation.
//
//nolint:unused,deadcode,varnamelen
func publishInTransaction(m dsl.Matcher) {
m.Import("github.com/coder/coder/v2/coderd/database/pubsub")
// Match direct calls to the Publish method of a pubsub instance inside InTx
m.Match(`
$_.InTx(func($x) error {
$*_
$_ = $ps.Publish($evt, $msg)
$*_
}, $*_)
`,
// Alternative with short variable declaration
`
$_.InTx(func($x) error {
$*_
$_ := $ps.Publish($evt, $msg)
$*_
}, $*_)
`,
// Without catching error return
`
$_.InTx(func($x) error {
$*_
$ps.Publish($evt, $msg)
$*_
}, $*_)
`).
Where(m["ps"].Type.Is("pubsub.Pubsub")).
At(m["ps"]).
Report("Avoid calling pubsub.Publish() inside database transactions as this may lead to connection deadlocks. Move the Publish() call outside the transaction.")
}
// doNotCallTFailNowInsideGoroutine enforces not calling t.FailNow or
// functions that may themselves call t.FailNow in goroutines outside
// the main test goroutine. See testing.go:834 for why.