I unintentionally broke the MongoDB Go driver ecosystem one March weekend in 2021. Dependency management systems are hard.
In late 2018, I moved all my Go libraries from github.com/xdg to
github.com/xdg-go to keep my Go work separate from other projects/languages.
This seemed painless, because GitHub automatically redirects repositories
after a rename. For example, in a project that was importing
github.com/xdg/stringprep, a dependency resolver would try to pull from that
repository, and GitHub would transparently redirect that to
github.com/xdg-go/stringprep.
Since then, Go modules caught on
as the new way to manage dependencies, replacing Glide, dep, and many
others.
With modules, Go libraries include a file, go.mod that defines the module
namespace and its dependencies and module versions are defined by tags in the
repository. The first line of go.mod looks something like this:
module github.com/xdg-go/stringprep
Now you see the problem. Before go.mod, it didn’t matter if code imported
xdg/stringprep or xdg-go/stringprep because the code
wasn’t aware of its own namespace. With a go.mod, however, a dependency
manager looking for xdg/stringprep would find a go.mod claiming
to be xdg-go/stringprep – NOT the same code that it’s looking
for.
Here’s where things went awry. Last weekend I dusted off some old
repositories from before their module days and added go.mod files.
Two of them, stringprep and scram, are used by the MongoDB Go
driver.
Ah, you say, but Go dependency management is smart — it freezes on a tag or
commit, so how could this change be a problem? Existing projects referencing
github.com/xdg/* will pull the old tag/commit from before when it wasn’t a
module.
In theory, this should work. The MongoDB driver’s go.mod referenced
specific, untagged commits:
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc
Any tools building up dependencies should reference these particular (non-module) versions.
But theory is no match for a horde of programmers and their dependency management tools.
It turns out that a lot of the community relies on go get -u foo to download
foo and updated versions of foo’s direct dependencies. Of course, this
defeats the whole point of freezing dependency versions, but that’s what
people apparently want. But the way that go get -u works, in theory that
shouldn’t update deep dependencies.
Theory, meet practice.
During the transition to modules, the tools also had to account for libraries
that weren’t modularized. For example, the MongoDB Go driver v1.0.0 had no
go.mod. So a project with a go.mod that relied on mongo-driver v1.0.0
would wind up with additional “indirect” dependencies to freeze deep
dependencies in go.mod like this:
module foo
require (
...
go.mongodb.org/mongo-driver v1.0.0
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
github.com/xdg/stringprep v1.0.0 // indirect
...
)
In other words, deep dependencies just became shallow – and thus targets of
go get -u foo. It doesn’t seem to matter that upgrading mongo-driver to a
modularized version (v1.3.0+) should make those indirect dependencies go away.
Before the go.mod can be recomputed without indirect dependencies, the
attempt to update the xdg/* libraries fails because of the conversion to
modules in the new namespace.
I got reports that other tools besides go get had similar problems from
recursively upgrading dependencies. Asking lots of people to fix their
toolchains and change their CI systems to stop auto-upgrading wasn’t a quick
or kind way to handle the problem. I needed to fix it.
Fixing forward doesn’t preclude backwards compatibility.
To fix the problem going forward, I submitted a pull-request for the MongoDB
Go driver to upgrade go.mod to use the modularized scram and stringprep
libraries. This merged Monday after the weekend I broke things, and the Go
driver team released v1.5.1 with the fix on Tuesday.
To fix the problem for people not upgrading the Go driver, I recreated the
xdg/scram and xdg/stringprep repos on GitHub, thus breaking the automatic
redirection, and pushed only commits through late 2018, effectively rewinding
time for those repository paths to before the breakage over the weekend.
Recreating a historical repository is straightforward with git. First, I created an empty repository on GitHub. Then I added it as a new remote to my local repository. Then it was just a matter of pushing only the commits I wanted. I created a branch at the right point in history and then pushed that as “master” to the repository copy.
$ git remote add legacyrepo git@github.com:xdg/stringprep.git
$ git checkout -b legacymaster 73f8eec
$ git push legacyrepo legacymaster:master
I thought that fixed things. But I was wrong.
What I didn’t realize at the time and discovered afterwards is that the Go
module proxy cached the bad tags on xdg/scram
and xdg/stringprep from when they were redirected to xdg-go. So even
fixing the GitHub redirect isn’t enough with newer versions of Go that
automatically use the proxy.
For example, this go get -u still broke:
go get -u -v github.com/golang-migrate/migrate/v4/cmd/migrate
...
go get: github.com/xdg/scram@v0.0.0-20180814205039-7eeb5667e42c updating to
github.com/xdg/scram@v1.0.2: parsing go.mod:
module declares its path as: github.com/xdg-go/scram
but was required as: github.com/xdg/scram
There is an environment variable to turn off the proxy, and this worked without error, confirming my diagnosis that the proxy was the problem:
GOPROXY=direct go get -u -v github.com/golang-migrate/migrate/v4/cmd/migrate
There seems to be no way to remove something from the proxy, so fixing the
problem required ’tricking’ the Go module proxy to ignore the bad tags. I
tagged xdg/scram and xdg/stringprep with new tags higher than the versions
that had been cached by the proxy. Then I ran go get on those tags to force
the proxy to cache the new versions.
That did the trick. When go get -u looks for a newer version, the proxy
informs them of the latest tags, which are cached from the new xdg repos
instead of the original xdg-go repo with the name conflict.
With these changes, I believe I’ve fixed the chaos I caused. I offer a sincere apology to anyone who was affected.
P.S. This was worse because I wasn’t getting issue notifications.
Several people opened GitHub issues over the weekend and on Monday, but I didn’t see any of them until someone highlighted my nickname directly. I’d turned off the GitHub setting to automatically watch repositories I’d been added to because I was getting notifications from every new repository added at work. But I think this affected repositories I created myself after the setting change, so I wasn’t getting notifications from them, either. I’ve fixed this and made sure I’m watching all repos I’ve created recently.