Make index iterator thread safe - #2108
Conversation
|
😍 |
|
Okay, so that last commit that adds some tests for running diffs concurrently in a threaded environment still has some outstanding issues. I immediately saw crashes when the various threads competed to update the attr file cache. I added a basic lock around modifying the cache and added refcounting on the individual attr files so they could be removed from the cache and outstanding threads would not access freed memory. I am still seeing sporadic crashes from ignore files for the crash and my initial attempt to resolve them with refcounts did not seem to work out right - the lifecycle for ignore file objects is slightly different than for git attribute files and that seems to have screwed me up. Additionally, I suspect that the reference counting I introduced has caused some memory leaks for certain cases with the attribute files. I'm putting that on the TODO list above, too. |
|
This is not entirely on-topic, but is there a discussion somewhere about the intended threading model for libgit2? It seems that the current idea is that individual objects can be accessed, and in this case, modified simultaneously with concurrent readers. For example, I wouldn't necessarily expect to be able to safely iterate over an index object while modifying it in another thread. In this situation, I'd expect to one of the threads would need to copy the index object. An alternative threading model that comes to mind is SQLite's where each connection object (or in the case of libgit2, a repository object) can only be accessed by a single thread at a time but a given database can have multiple connection objects. |
|
@GrahamDennis That is a reasonable question and we should probably write it up in the When libgit2 is compiled with There have been a couple smaller efforts to enable a more granular threading model where certain actions can be taken using multiple threads even with a shared This PR is an effort to expand the options for multithread access to a single repository to include some other actions that are still purely read-only, such as running two diffs concurrently. Libgit2 diff is a read-only operation so this should work, but due to some decisions in the design of index access and in the layer that caches git ignore and attribute data, this does not yet work correctly. There is no concerted effort to allow multiple threads to access a single |
|
Maybe it's worth to note that the thread-safety of the ODB was internally required for efficient creation of packfiles (unless my memory is totally misleading me now :) |
|
@arrbee OK that's the threading model I would have expected. For my benefit, is the issue here that searching the index for entries can mutate the index because it triggers a sort? Therefore 'read-only' operations can actually mutate the index? If so, why make the index support concurrent 'read-only' operations by increasing the cost of index iteration in all use cases (as all index iterators would need to create a snapshot), vs documenting the 'volatile' nature of |
@arrbee Just to clarify, does this actually have to be one thread, or do you just mean that all use of a |
|
@jspahrsummers Sorry for the delay in responding - as I think you now know, I was on vacation. While libgit2 does use TLS, it only uses it for storing the last error message that occurred from an API invoked on that thread. Provided that libgit2 error message checks are made on the same thread that actually invoked the API, then yes, you only need to serialize |
|
Just a quick note - I've continued to work on this, but it has proven quite tricky to actually provide true thread safety here without much more extensive changes to the index and attributes files. My current thought is to drop the actual thread-safety tests for index diffs from this PR (and not claim that diffs are thread safe) and merge the PR nonetheless - not as a fix for the index diff threading problems, but as a first step and one that reduces the window of contention over index contents during diffs. |
|
I did fix one memory leak from the additional refcounting the attributes / ignores code, but there is still another one that I'm chasing down... |
|
Okay, well, there is an issue - the memory leak fix for pop ignores in bd6265e causes a double free in the threaded tests, but if I take it out, a leak in the non threaded tests. A'debugging I will go. Other than that, I think this is shaping up. |
|
@arrbee: this just got stale. If you could rebase it, I'd like to merge it right away. |
|
I have a rebased version of this, but I'm struggling a bit to clear up memory leaks with ignore files. If I remove the leaks from the unthreaded code, then the threaded tests leak. If I remove the leaks from the threaded code, then I see a double-free in the unthreaded code. The ignores and attributes caching is overly messy but I'm trying hard not to make refactoring that a prerequisite for getting this wrapped up. |
|
I've been hacking on this off and on over the past couple of days and wrote a much simpler threaded test that causes the leak. This allowed me to find the race condition that causes the problem, but unfortunately, I don't see a great fix other than changing how the attribute/ignore file caching code works. The current logic is too complicated and mixes together too many situations (loading a file for the first time, finding a file is not present, updating an existing file, clearing a file that has been deleted). I'm about 75% of the way through reorganizing the code to make this all simpler and hopefully repair the race condition. |
|
Okay, so I pushed up a kind of big refactoring of the attribute cache. It compiles but the tests are still pretty broken - I think there is still some initialization order stuff that I've left borked. I'll fix it up tomorrow... |
|
Okay, so I was still seeing issues with the threaded tests that could lead to crashes. I worked around them by making two changes, neither of which I love, but they seem like pragmatic solutions. I'll be curious if you hate them @vmg too much... Specifically, I was seeing a problem where an index entry could end up inserted twice into the index (which would result in a double free). This appeared to be due to some race between finding the item in the index and inserting it. I switched to using The second fix is for a remaining race condition when a gitattribute file or gitignore file is considered out-of-date. Even though I rewrote the file caching layer to eliminate races, it was still possible for two threads to both fetch the file from the cache and both decide that it was out of date and needed to be reloaded. This could be fixed by moving the reloading logic into the code that checks the file out of the cache, but I didn't want to hold the cache lock during a file parse. Instead, I added a lock to the attribute file structure and just lock the one file while it is being reloaded. This is okay if After these changes, I need to check again for memory leaks to make sure I haven't introduced any new ones... |
|
Okay - a little rebase there to meld this with some of the merged ignore bug fixes. |
|
I don't see either change being particularly gross. I like them both. :) What I'm not starting to like is the size of this PR. I don't even know what changed since the last time I read through all of it. :'( Is there any chance we could merge this soon and maybe iron out the kinks later? |
Yes, this has grown quite a bit more than I expected. It now includes major changes to the attributes and ignore code that are separate from the index changes (although necessary to actually get the threading benefits). I think the only thing left is to check this further for memory leaks. I was planning to work on that this afternoon, but if you want to merge now and clean up leaks, etc., I'm okay with that. FWIW, just the last 4 commits in the PR are actually new code, although 987651d alone is pretty gigantic. |
|
@vmg If this PR has become too much to swallow, I'm willing to split it into multiple smaller PRs. There could be, conceptually, three PRs in this one: one to rework attribute/ignore file caching to be thread safe, one to rework the index to be more thread safe + provide thread safe snapshots + use them for diffs, and one with various code cleanups that are not directly related to the other two. |
|
Okay, I've spent the afternoon tracking down memory leaks and there are a couple of problems I've found with updating timestamp data so that a file won't always be reloaded and then with managing reference counts correctly when an existing file does need to reloaded. It won't be too big a fix, but I'd like to fix it up before we think about merging this. |
This is just laying some groundwork for internal index changes that I'm working on.
Again, laying groundwork for some index iterator changes, this contains a bunch of code refactorings for index internals that should make it easier down the line to add locking around index modifications. Also this removes the redundant prefix_position function and fixes some potential memory leaks.
The usefulness of these helpers came up for me while debugging some of the iterator changes that I was making, so since they have also been requested (albeit indirectly) I thought I'd include them.
This surrounds any function that mutates the entries vector with a mutex so it can be safely snapshotted.
This makes the index iterator honor the GIT_ITERATOR_IGNORE_CASE and GIT_ITERATOR_DONT_IGNORE_CASE flags without modifying the index data itself. To take advantage of this, I had to export a number of the internal index entry comparison functions. I also wrote some new tests to exercise the capability.
This adds a basic test of doing simultaneous diffs on multiple threads and adds basic locking for the attr file cache because that was the immediate problem that arose from these tests.
This makes the lock management on the index a little bit broader, having a number of routines hold the lock across looking up the item to be modified and actually making the modification. Still not true thread safety, but more pure index modifications are now safe which allows the simple cases (such as starting up a diff while index modifications are underway) safe enough to get the snapshot without hitting allocation problems. As part of this, I simplified the allocation of index entries to use a flex array and just put the path at the end of the index entry. This makes every entry self-contained and makes it a little easier to feel sure that pointers to strings aren't being accidentally copied and freed while other references are still being held.
Clear up some of the various "find" functions and the snapshot API naming to be things I like more.
Some ignore files were not being freed from the cache.
The iterator pushes and pops ignores incrementally onto a list as it traverses the directory structure so that it doesn't have to constantly recheck which ignore files apply. With the new ref counting, it wasn't decrementing the refcount on the ignores that it removed from the vector.
I introduced a leak into conflict cleanup by removing items from inside the git_vector_remove_matching call. This simplifies the code to just use one common way for the two conflict cleanup APIs. When an index has an active snapshot, removing an item can cause an error (inserting into the deferred deletion vector), so I made the git_index_conflict_cleanup API return an error code. I felt like this wasn't so bad since it is just like the other APIs. I fixed up a couple of comments while I was changing the header.
While I was looking at the conflict cleanup code, I looked over at the tree cache code, since we clear the tree cache for each entry that gets removed and there is some redundancy there. I made some small tweaks to avoid extra calls to strchr and strlen in a few circumstances.
This is a big refactoring of the attribute file cache to be a bit simpler which in turn makes it easier to enforce a lock around any updates to the cache so that it can be used in a threaded env. Tons of changes to the attributes and ignores code.
In the threading tests, I was still seeing a race condition where the same item could end up being inserted multiple times into the index. Preserving the sorted-ness of the index outside of the `index_insert` call fixes the issue.
I don't love this approach, but achieving thread-safety for attribute and ignore data while reloading files would require a larger rewrite in order to avoid this. If an attribute or ignore file is out of date, this holds a lock on the file while we are reloading the data so that another thread won't try to reload the data at the same time.
The checks to see if files were out of date in the attibute cache was wrong because the cache-breaker data wasn't getting stored correctly. Additionally, when the cache-breaker triggered, the old file data was being leaked.
|
Ok, so I'm getting a clean valgrind with the latest fixes here and I also rebased to deal with the conflicts that had been merged into |
|
There isn't anything else that I intend to do with this PR. |
|
This is a huge improvement. Particularly love the threaded tests. :))))))) |
There was a problem hiding this comment.
This is actually spewing a lot of warnings on Apple's g++/clang hybrid (Mavericks):
warning: unknown warning option '-Wno-unused-const-variable'; did you mean '-Wno-unused-variable'? [-Wunknown-warning-option]
There was a problem hiding this comment.
Hmm. I introduced this because the Apple gcc/clang on my box (also 10.9.2) was spewing warnings without it and now compiles warning free.
If I type gcc --version on my box, I see:
$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posixWhat do you get?
There was a problem hiding this comment.
$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.1.0
Thread model: posix
Yours is slightly newer, going to open a PR to rather do this:
+ CHECK_C_COMPILER_FLAG(-Wno-unused-const-variable UNUSED_CONST_VAR_WARNING)
+ IF(UNUSED_CONST_VAR_WARNING)
+ SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-const-variable")
+ ENDIF()
There was a problem hiding this comment.
❤️ Do you see other warnings? I generally try to keep things compiling warning-free, but obviously there are a lot of compiler combinations...
…ator Make index iterator thread safe

Making the index fully threadsafe is a large task, but making the index iterator take a snapshot of the index content and use that is a much more manageable project. The goals of this PR are:
I wrote a bunch of the code to test out these ideas and then refactored it into a few more coherent commits along with a certain amount of cleanup stuff and debugging helper stuff. There are still a few things left to do:
This PR does not make general index usage thread-safe. There are still a ton of places where the code sorts the index and then does a binary search without holding any kind of lock, so another thread could easily swoop in and mutate the index in the middle of the binary search. For that to work, either more
rwlockwork will be required or a better index data structure should be used.