Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

run tests in parallel if we can open enough files #450

Closed
wants to merge 1 commit into from
Closed

run tests in parallel if we can open enough files #450

wants to merge 1 commit into from

Conversation

mvdan
Copy link
Contributor

@mvdan mvdan commented Sep 20, 2021

(see commit message)

A Linux box tends to default to a soft limit of 1024 open files.
This means that simply marking all tests as parallel
will quickly result in "socket: too many open files" errors.

Unfortunately, running all tests sequentially is also too slow,
especially since most tests sleep for at least a second or two.

As a middle ground, run tests in parallel if the limit is high enough.
See the added code and inline documentation.

When running "go test" with a small open file limit,
the tests will simply run sequentially.
Otherwise, they will run in parallel given GOMAXPROCS.

With a high enough "ulimit -Sn" on my Linux laptop,
this brings "go test" down from ~220s to ~36s.
Comment on lines +11 to +38
func TestMain(m *testing.M) {
// For all tests to work reliably in parallel,
// we require being able to open many files at a time.
// Many environments have lower limits,
// such as Linux's "ulimit -n" soft limit defaulting to 1024.
// On a machine with 16 CPU threads, 8k seems to be enough.
const wantFileLimit = 8 << 10
files := make([]*os.File, 0, wantFileLimit)
for i := 0; i < wantFileLimit; i++ {
file, err := os.Open("pubsub_test.go")
if err != nil {
if i == 0 {
panic(err) // the file doesn't exist?
}
fmt.Fprintf(os.Stderr, "Skipping parallel runs of tests; open file limit %d is below %d.\n",
i, wantFileLimit)
fmt.Fprintf(os.Stderr, "On Linux, consider running: ulimit -Sn %d\n",
wantFileLimit*2)
canRunInParallel = false
break
}
files = append(files, file)
}
for _, file := range files {
file.Close()
}
os.Exit(m.Run())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is UNSPEAKABLY ugly.

The correct way to do this is to have an init function in the test, which tries to set the file descriptor limit to whatever it needs and sets the canRunInParallel variable accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. We could do this kind of check for every test, but that feels very repetitive, and also far too specific. Are we going to measure exactly how many FDs each tests consumes?

The limits also don't apply to each test in isolation. It's roughly "number of FDs used at a time per test on average" multiplied by GOMAXPROCS. So I think a global approximation is good enough overall. Right now CI uses 2k, and I locally found 8k to be plenty with 16 CPU threads, so that seems fine to me, at least.

I also thought about doing the equivalent of ulimit -Sn directly, but note that that's not portable, so it would require code behind // +build linux that would not work on other systems. It also seems a bit weird for tests to lift the soft limit on their own; it feels like something the user should be in control of, like what the CI config does.

os.Exit(m.Run())
}

var canRunInParallel = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should default to false.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checking code always runs, does it make a difference? :)

@vyzo
Copy link
Collaborator

vyzo commented Sep 20, 2021 via email

@mvdan
Copy link
Contributor Author

mvdan commented Sep 21, 2021

Just use a test init function to set the variable

Sorry, but I'm still not understanding that approach. I think a global one is reasonable, as I tried to explain in #450 (comment).

there ought to be some library abstracting that.

I would agree, but it doesn't seem to exist. I'm more than happy to use one if you point me towards it. Otherwise, just making it work on Linux doesn't feel particularly better than what this PR has right now.

look, this is a gross hack.

All that said, I'll close the PR if you don't feel like this is worth your time. I honestly thought making the tests not take four minutes would be worthwhile for everyone. Any solution we come up with will be relatively hacky in one way or another, because of varying environments and defaults.

@vyzo
Copy link
Collaborator

vyzo commented Sep 23, 2021 via email

@mvdan mvdan closed this Sep 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants