updateGitMirror: use double checked locking#4074
Conversation
Currently `updateGitMirror` acquire the mirror lock before checking for the existence of the revision. But `git rev-parse` should be safe to run concurrently, so before locking it's worth checking if the commit is present, which should be safe to do without the lock. When running builds with large number of jobs, and large server with many agents on them, it's not rare that several agents are looking for the same revision, hence reducing contention on that path is worth it.
|
Hiya @byroot, just wanted to acknowledge and thank you for your contribution. I look forward to reviewing it early next week! |
|
Thank you. On a side note, I'm experimenting with implementing our own checkout, to see if we can optimize this further. I'm not sure why locking is even needed (aside from the clone lock) as IIRC git has its own internal locking when needed. So I'm going to experiment with no locking at all, and I'll report back if it's successful. |
| } | ||
| }() | ||
| if isMainRepository { | ||
| // Check again after we get a lock, in case the other process has already updated |
There was a problem hiding this comment.
I found this comment suspect so I dug a bit in the history, look like the agent used to check before the lock a few years ago: https://github.com/byroot/agent/blame/2a581a13e7f63e69651f2bbe689e44db6d33e074~1/internal/job/executor.go#L951
It seems it was removed in #3789, but it's not clear to me why.
|
As it turns out, the reason we removed this "snapshot an existing commit without a lock" is because it exposes us to the following race condition:
Your use-case where a large number of jobs are snapshotting the same commit is valid though. Maybe there's something in the region of a commit-aware lock, such that jobs requesting the same commit can safely snapshot concurrently (no risk of corruption from gc), while jobs seeking a different commit can wait for a lock (status quo). |
Interesting. I'll make sure to disable auto-gc. It makes for unpredictable performance anyway.
Yes, that was my backup idea :) |
|
Another avenue (which can also be complementary) is to use flock's RWLock ability. If the concern is that a if !mirror.has_commit?(SHA)
flock.lock_exclusive(SHA) do
if !mirror.has_commit?(SHA) # Check again in case another process did fetch it for us
flock.lock_exclusive do
mirror.fetch(SHA)
end
end
end
end
# Ensure we're not cloning during a concurrent `fetch`
# but still allow concurrent clones.
flock.lock_shared do
do_local_clone
end |
Yeah, this could totally work and feels like a complete solution. Curious to hear how you see this as merely complementary. |
Well, I said can :) My hunch (we may be totally wrong) is that even fetching a commit that is already present still increase contention on the mirror, hence it could be beneficial to park the agents that wish to fetch a commit that is already being fetched (or about to) on a secondary lock. |
Assuming you meant "snapshotting a commit …", what contention do you see arising on the mirror? The only one I can think of is if somehow git would report a commit as "present", but partially GC'ed and thus requiring a fetch, but that feels kind of bad if true. I tried to game out the exclusive/shared lock idea below but with the shared lock annotated with a commit sha (I don't think your sketch above does this, but I could've misread). I think it will allow concurrent snapshots of a present commit with no risk of corruption from a concurrent fetch/gc. What do you think?
None of the commits currently exist in the mirror. There are two locks:
Sequence:
|
By snapshotting you mean running
By contention I just mean that the For full context, we previously had the However we noticed a lot of traffic to github (very costly because EC2), so we've now re-enabled the mirror updating, which did bring the github traffic down, but that's where we noticed the performance becoming much less stable.
I think you can achieve better, with just a single lock that is taken as Let me replicate your scenario:
None of the commits currently exist in the mirror. There is a single locks:
Sequence:
|
|
Btw @byroot you might be interested in the git caching work we are doing in https://github.com/buildkite/content-cache. |
Ah yes. I overindexed on the "--mirror of a --mirror" process in But when clean-checkout is disabled, the local clone indeed interacts directly with the shared mirror, increasing the chances of contention.
Ah I see it now - if all the commits requested by the jobs are present on the shared mirror and there is no fetching in flight ( I hope to spend a bit more time on this next week. Will keep you posted! |
|
Just catching up here and figuring out the intent behind the I think the right model is one repo-wide RW lock, not a per-commit lock. The resource being protected is the mirror, and operations such as repack or GC can rewrite packs containing many commits. Jobs requesting any already-present commit take On a cache miss, the job can drop The change currently proposed checks for the commit and calls Keen on @DrJosh9000's thoughts too, I imagine there was a ton of additional context behind the original change. |
Yes, we established that in #4074 (comment) I haven't updated the PR accordingly, but as we both mentioned, a way to reduce contention a bit is to use a shared lock during "snapshoting". I can try to implement that is there is interest, but given the discussion is still ongoing that maybe isn't worth it yet. |
It's a detail, but in theory it should be able to take
|
|
So, after trying my custom checkout more extensively, and more importantly instrumenting individual What's really causing the So it seems we're experiencing some IO bottleneck.... I still think it'd be nice to keep the lock less contended, but that alone wouldn't solve our issue 😢 |
Currently
updateGitMirroracquire the mirror lock before checking for the existence of the revision.But
git rev-parseshould be safe to run concurrently, so before locking it's worth checking if the commit is present, which should be safe to do without the lock.When running builds with large number of jobs, and large server with many agents on them, it's not rare that several agents are looking for the same revision, hence reducing contention on that path is worth it.
Ideally, while we're blocked on the lock, we could occasionally check if the commit has been fetched, but it's a more invasive change.