Streaming read support for the loose ODB backend - #4450
Conversation
d2dcb41 to
ad0ac75
Compare
pks-t
left a comment
There was a problem hiding this comment.
Cool, it was quite an enjoyable read. Thanks for that ;)
Personally, I've got no problem with breaking the API. In theory, somebody could have his own ODB backend which already supports the streaming API, but I kind of doubt that.
| if (decompressed > head_len) { | ||
| stream->start_len = decompressed - head_len; | ||
| memcpy(stream->start, head + head_len, decompressed - head_len); | ||
| } |
There was a problem hiding this comment.
So we're initially decompressing up to 64 bytes as that is the maximum header length for any object in the stream. As we might be decompressing too much data, though, we have to retain the first leading bytes of the object itself (64 - header_len) in a buffer. Did I get that right?
| */ | ||
| if (stream->start_len && len) { | ||
| size_t chunk = MIN(stream->start_len, len); | ||
| memcpy(buffer, stream->start, chunk); |
There was a problem hiding this comment.
This logic doesn't look right. If len < stream->start_len, we have to copy multiple chunks from stream->start on successive calls. But you keep on copying the beginning of stream->start, and don't ever advance into the start buffer. So the caller will repeatedly see the first bytes of stream->start without seeing the rest of it.
There was a problem hiding this comment.
I didn't see this being addressed. Do I miss something here?
Edit: nvm, getting fixed later. The fixup landed in d315374 (odb: loose object streaming for packlike loose objects, 2017-12-17), though. I guess this was not by intention.
| total += chunk; | ||
| } | ||
|
|
||
| return total; |
There was a problem hiding this comment.
I don't see any logic how we can handle and indicate an EOF to the caller. Is that being handled by the git_odb_stream?
There was a problem hiding this comment.
We behave like read(2) by returning 0 at EOF.
| backend = (loose_backend *)_backend; | ||
| *stream_out = NULL; | ||
| *len_out = 0; | ||
| *type_out = 0; |
There was a problem hiding this comment.
Shouldn't that be GIT_OBJ_BAD instead? 0 indicates GIT_OBJ__EXT1.
There was a problem hiding this comment.
Seems reasonable; fixed.
| hash_ctx = git__malloc(sizeof(git_hash_ctx)); | ||
| GITERR_CHECK_ALLOC(hash_ctx); | ||
|
|
||
| if (locate_object(&object_path, backend, oid) < 0) { |
There was a problem hiding this comment.
Maybe put that check before the allocations?
| zstream->z.next_in = (Bytef *)zstream->in; | ||
| zstream->z.avail_in = (uInt)zstream->in_len; | ||
|
|
||
| if ((size_t)zstream->z.avail_in != zstream->in_len) { |
There was a problem hiding this comment.
Is this handling the case where z.avail_in = (uInt) zstream->in_len overflows? If so, a small comment might be worthwhile.
There was a problem hiding this comment.
Updated to add a comment and an improved test.
|
|
||
| return 0; | ||
| } | ||
|
|
|
|
||
| if (out_len) | ||
| *out_len = used; | ||
|
|
| error = read_header_loose_standard(out, obj, (size_t)obj_len); | ||
|
|
||
| if (!git_object_typeisloose(hdr.type)) { | ||
| if (!error && !git_object_typeisloose(out->type)) { |
There was a problem hiding this comment.
A goto in case of error would be a bit nicer here, I guess. Special-casing error handling just because it is the last statement only asks for small error later on when more code is being inserted after it.
| if (!is_zlib_compressed_data((unsigned char *)obj.ptr, obj.size)) | ||
| error = read_loose_packlike(out, &obj); | ||
| else | ||
| error = read_loose_standard(out, &obj); |
There was a problem hiding this comment.
Huh. Why should we treat an object as packlike though when obj.size < 2? Shouldn't we error out in that case instead? Something like:
if (is_zlib_compressed_data(&is_compressed, obj.ptr. obj.size) < 0)
return -1;
if (!is_compressed)
packlike();
else
standard();
d92533d to
72e661b
Compare
|
I think that I've taken care of your issues, @pks-t |
72e661b to
ba5bb53
Compare
pks-t
left a comment
There was a problem hiding this comment.
I went through the complete series again. It's only stylistic things now I can comment on, otherwise it looks good to me.
| cl_assert_equal_oid(&expected, &oid); | ||
| } | ||
|
|
||
| void test_odb_largefiles__streamread(void) |
There was a problem hiding this comment.
I don't think we've come to an agreement on this, no. I'll rearrange these, but we should hash this out at the summit. I'm definitely not the only person who prefers to add a failing test first, so the question is if we can get a workflow that accommodates all our preferences.
There was a problem hiding this comment.
I definitly agree with you. Adding failing tests first is the best way how we can demonstrate that the next commit fixes the issue. See my pull request #4500, which tackles this exact problem
| #include "git2/types.h" | ||
|
|
||
| /* maximum possible header length */ | ||
| #define HEADER_LEN 64 |
There was a problem hiding this comment.
Small nit: maybe call it MAX_HEADER_LEN?
| /* inflate the initial part of the compressed buffer in order to | ||
| * parse the header; read the largest header possible, then push the | ||
| * remainder into the body buffer. | ||
| */ |
There was a problem hiding this comment.
This comment doesn't follow kernel-style anymore. See Linus explaining it nicely: http://lkml.iu.edu/hypermail/linux/kernel/1607.1/00627.html
There was a problem hiding this comment.
lololol, i trust that your
ing with the "nicely".
| loose_readstream *stream = (loose_readstream *)_stream; | ||
|
|
||
| git_futils_mmap_free(&stream->map); | ||
| git_zstream_free(&stream->zstream); |
| total += chunk; | ||
| } | ||
|
|
||
| return total; |
| */ | ||
| if (stream->start_len && len) { | ||
| size_t chunk = MIN(stream->start_len, len); | ||
| memcpy(buffer, stream->start, chunk); |
There was a problem hiding this comment.
I didn't see this being addressed. Do I miss something here?
Edit: nvm, getting fixed later. The fixup landed in d315374 (odb: loose object streaming for packlike loose objects, 2017-12-17), though. I guess this was not by intention.
| memcpy(buffer, stream->start, chunk); | ||
| if (start_remain && buffer_len) { | ||
| size_t chunk = min(start_remain, buffer_len); | ||
| memcpy(buffer, stream->start + stream->start_read, chunk); |
There was a problem hiding this comment.
Oops. Stumbled upon that the second time now :P
| * inflate the initial part of the compressed buffer in order to parse the | ||
| * header; read the largest header possible, then store it in the `start` | ||
| * field of the stream object. | ||
| */ |
There was a problem hiding this comment.
Small nit: indentation of this comment is a bit off here
|
|
||
| void test_odb_loose__streaming_reads(void) | ||
| { | ||
| size_t blocksizes[] = { 1, 2, 4, 16, 99, 1024, 123456789, 0 }; |
There was a problem hiding this comment.
Ugh, I was really confused by the 0 blocksize here. Didn't see that it was used as a terminator, so I kept on asking myself for several minutes how we can actually fully read through the stream in that case. Maybe remove that blocksize and iterate by array size instead of by sentinel?
There was a problem hiding this comment.
Yeah, I think you're right; I fixed this.
There are two streaming functions; one for reading, one for writing. Disambiguate function names between `stream` and `writestream` to make allowances for a read stream.
The streaming read functionality should provide the length and the type of the object, like the normal read functionality does.
Provide a streaming loose object reader.
Since some test situations may have generous disk space, but limited RAM (eg hosted build agents), test that we can stream a large file into a loose object, and then stream it out of the loose object storage.
A "packlike" loose object was a briefly lived loose object format where the type and size were encoded in uncompressed space at the beginning of the file, followed by the compressed object contents. Handle these in a streaming manner as well.
Refactor packlike loose object reads to use `git_zstream` for simplification.
Introduce `get_output_chunk` that will inflate/deflate all the available input buffer into the output buffer. `get_output` will call `get_output_chunk` in a loop, while other consumers can use it to inflate only a piece of the data.
Make `read_header` use the common zstream implementation. Remove the now unnecessary zlib wrapper in odb_loose.
Support `read_header` for "packlike loose objects", which were a temporarily and uncommonly used format loose object format that encodes the header before the zlib deflate data. This will never actually be seen in the wild, but add support for it for completeness and (more importantly) because our corpus of test data has objects in this format, so it's easier to support it than to try to special case it.
Test that we can read_header on large blobs. This should succeed on all platforms since we read only a few bytes into memory to be able to parse the header.
When checking to see if a file has zlib deflate content, make sure that we actually have read at least two bytes before examining the array.
Only run the large file tests on 64 bit platforms. Even though we support streaming reads on objects, and do not need to fit them in memory, we use `size_t` in various places to reflect the size of an object.
`MAX_HEADER_LEN` is a more descriptive constant name.
ba5bb53 to
09df354
Compare
|
Thanks again, updated with your comments. |
|
Now if only GitHub had an interface to easily view the interdiff between two versions of the PR... don't really feel like going through all of it again right now :( |
Change the streaming reader API a bit, to provide the type and length of the object when initializing the stream. This would be a breaking API change - except, of course, that nobody is actually using the streaming reader API since none of the backends support it. So I feel pretty good breaking this API that nobody could be using.
Add streaming reader support to the loose object ODB backend. I cleaned up a bit while I was in there to add some more tests for things like read_header which had no explicit tests (and in fact failed on some of the loose ODB test corpus), teach read_header how to cope with "packlike loose objects" (which were a weird loose object format that was attempted for a bit and will never actually be seen in the wild) and drop the odb_loose internal zstream abstraction layer that ultimately became git_zstream.
Note that this depends on #4443.