Skip to content

Make index iterator thread safe - #2108

Merged
vmg merged 20 commits into
developmentfrom
rb/threadsafe-index-iterator
Apr 18, 2014
Merged

Make index iterator thread safe#2108
vmg merged 20 commits into
developmentfrom
rb/threadsafe-index-iterator

Conversation

@arrbee

@arrbee arrbee commented Feb 9, 2014

Copy link
Copy Markdown
Member

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:

  1. Make the index iterator use a snapshot of the index content
  2. Protect the index entries vector during mutations with a mutex
  3. While there are outstanding index iterators, defer freeing index entries until the iterators are freed
  4. Make index iterator sort order independent of the native order of the index by resorting the snapshot

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:

  • allow index iterator to re-sort the entries (and get rid of re-sorting cruft in tree to index diff)
  • add threaded tests of concurrent diffs using the index
  • add threaded tests of diff while another thread adds and deletes from the index (making sure deferred deletes come into play)
  • fix thread-safe life cycle management for git attribute file and ignore file objects
  • check for memory leak based on all these changes

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 rwlock work will be required or a better index data structure should be used.

@jspahrsummers

Copy link
Copy Markdown
Contributor

😍

@arrbee arrbee mentioned this pull request Feb 10, 2014
@arrbee

arrbee commented Feb 11, 2014

Copy link
Copy Markdown
Member Author

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.

@GrahamDennis

Copy link
Copy Markdown
Contributor

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.

@arrbee

arrbee commented Feb 13, 2014

Copy link
Copy Markdown
Member Author

@GrahamDennis That is a reasonable question and we should probably write it up in the docs directory or something because I think it has previously only be described in responses to some issues.

When libgit2 is compiled with THREADSAFE on, the intent is to enable a SQLite-like threading model where the library can be used in a threaded environment so long as each git_repository is only accessed by a single thread. We have some reasonable confidence that this is an okay usage pattern (with some minor caveats about making sure that git_threads_init is called before any concurrency actually starts).

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 git_repository object. The only area where I have much confidence in this is for simple usage of the ODB - i.e. using git_object_lookup and the like to do simple object access.

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 git_repository and even where there has been some work done, it is usually still in a multiple-readers, single-writer type of model (i.e. so you would still have to explicitly serialize any write actions). The "one thread per git_repository" is really the only approved threading model at this point.

@scunz

scunz commented Feb 13, 2014

Copy link
Copy Markdown
Contributor

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 :)

@GrahamDennis

Copy link
Copy Markdown
Contributor

@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 git_index and provide a git_index_copy API? Or, requiring users to obtain two git_index objects derived from different git_repository objects?

@jspahrsummers

Copy link
Copy Markdown
Contributor

The "one thread per git_repository" is really the only approved threading model at this point.

@arrbee Just to clarify, does this actually have to be one thread, or do you just mean that all use of a git_repository should be serialized? In other words, does libgit2 use TLS or some mechanism which has an explicit association with threads?

@arrbee

arrbee commented Feb 21, 2014

Copy link
Copy Markdown
Member Author

@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 git_repository access, not have a dedicated thread.

@arrbee arrbee mentioned this pull request Feb 21, 2014
@arrbee

arrbee commented Mar 10, 2014

Copy link
Copy Markdown
Member Author

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.

@arrbee

arrbee commented Mar 14, 2014

Copy link
Copy Markdown
Member Author

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...

@arrbee

arrbee commented Mar 15, 2014

Copy link
Copy Markdown
Member Author

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.

@vmg

vmg commented Apr 4, 2014

Copy link
Copy Markdown
Member

@arrbee: this just got stale. If you could rebase it, I'd like to merge it right away.

@arrbee

arrbee commented Apr 7, 2014

Copy link
Copy Markdown
Member Author

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.

@arrbee

arrbee commented Apr 10, 2014

Copy link
Copy Markdown
Member Author

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.

@arrbee

arrbee commented Apr 11, 2014

Copy link
Copy Markdown
Member Author

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...

@arrbee

arrbee commented Apr 14, 2014

Copy link
Copy Markdown
Member Author

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 git_vector_insert_sorted (which I know you dislike) to keep the index sorted even across inserts so we can maintain "findability" at all times. Because we must search the index before every insertion anyhow, I don't think this is slower than inserting at the end and re-sorting the list every time. If anything, I suspect it will provide a minor performance improvement.

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 git_mutex items are not a scarce resource.

After these changes, I need to check again for memory leaks to make sure I haven't introduced any new ones...

@arrbee

arrbee commented Apr 14, 2014

Copy link
Copy Markdown
Member Author

Okay - a little rebase there to meld this with some of the merged ignore bug fixes.

@vmg

vmg commented Apr 15, 2014

Copy link
Copy Markdown
Member

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?

@arrbee

arrbee commented Apr 15, 2014

Copy link
Copy Markdown
Member Author

What I'm not starting to like is the size of this PR

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.

@arrbee

arrbee commented Apr 15, 2014

Copy link
Copy Markdown
Member Author

@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.

@arrbee

arrbee commented Apr 16, 2014

Copy link
Copy Markdown
Member Author

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.

arrbee added 2 commits April 17, 2014 14:43
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.
arrbee added 18 commits April 17, 2014 14:43
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.
@arrbee

arrbee commented Apr 17, 2014

Copy link
Copy Markdown
Member Author

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 development (which were my own fixes for some attribute stuff that I backported).

@arrbee

arrbee commented Apr 17, 2014

Copy link
Copy Markdown
Member Author

There isn't anything else that I intend to do with this PR.

vmg pushed a commit that referenced this pull request Apr 18, 2014
@vmg
vmg merged commit 28fd720 into development Apr 18, 2014
@vmg

vmg commented Apr 18, 2014

Copy link
Copy Markdown
Member

This is a huge improvement. Particularly love the threaded tests. :)))))))

Comment thread CMakeLists.txt

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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: posix

What do you get?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$ 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()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

❤️ Do you see other warnings? I generally try to keep things compiling warning-free, but obviously there are a lot of compiler combinations...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No warnings at all.

@jspahrsummers

Copy link
Copy Markdown
Contributor

@arrbee
arrbee deleted the rb/threadsafe-index-iterator branch April 18, 2014 16:19
phatblat pushed a commit to phatblat/libgit2 that referenced this pull request Sep 13, 2014
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.

6 participants