brandur.org

River binaries will get 10 kB lighter

With v0.43, Go binaries linking River will get 10 kB lighter. This is good, except they never should’ve had the extra weight in the first place.

River has a test-oriented package called testsignal that’s used in main code paths to help check for conditions that’d otherwise require tricky timing workarounds involving sleeps. It’s invoked in non-test code, but no-ops outside of tests:

// notifies when the service deletes a batch
DeletedBatch testsignal.TestSignal[struct{}]

// send the test signal
s.TestSignals.DeletedBatch.Signal(struct{}{})

testsignal has few dependencies, but we used a helper from another River package (riversharedtest) for test helpers which was pulling in a slew of test dependencies like Goleak, Testify, and even YAML through Testify.

This meant that River’s non-test code was also picking up all these test dependencies transitively through testsignal and including them in any River program, adding ~10 kB in binary overhead (from go tool nm -size):

github.com/stretchr/testify  18 symbols / 1168 bytes
github.com/davecgh/go-spew   11 symbols / 1312 bytes
go.uber.org/goleak            8 symbols /  896 bytes
gopkg.in/yaml                22 symbols / 4320 bytes
riversharedtest              13 symbols / 2248 bytes

Obviously a silly mistake to make, but one that’s hard to spot and which Go doesn’t protect you from, leaving latent binary bloat until someone notices it months later (in this case a contributor – thanks @e-yavuz-1!).

Thankfully, there’s a linter to help prevent the problem. Depguard (part of golangci-lint) lets you add rules that target a configured set of files and allow or deny a configured set of packages. I added a couple new ones for River:

# The next two blocks have the same intent: don't allow testsignal,
# which is used in non-test code, to have non-stdlib dependencies.
# Previously, we ran into a problem where it was accidentally importing
# riversharedtest, which was importing Goleak, Testify, YAML (through
# Testify), etc. which added 10 kB overhead to all binaries built with
# River. testsignal does use testutil, so the second block makes sure
# that testutil has no stdlib dependencies so that testsignal doesn't
# pick one up transitively.
testsignal-no-test-deps:
  files:
    - "**/testsignal/*.go"
    - "!$test"
  allow:
    - $gostd
    - "github.com/riverqueue/river/rivershared/util/testutil$"
testutil-no-test-deps:
  files:
    - "**/util/testutil/*.go"
    - "!$test"
  allow:
    - $gostd

Did I make a mistake? Please consider sending a pull request.