Repository navigation
Replace CAS with CS and CLS - #366
meroton-benjamin wants to merge 10 commits into
Conversation
a6c6de2 to
eb4b94a
Compare
| // that signals downstream storage layers that the chunk list has | ||
| // already been validated (or was freshly generated) and does not need | ||
| // expensive re-validation. | ||
| func NewContextWithChunkListValidationBypass(ctx context.Context) context.Context { |
There was a problem hiding this comment.
I think that we should get rid of this, and turn it into a function argument. For example, change BlobAccess.Put() to take a validated bool parameter.
There was a problem hiding this comment.
validated is now a property of chunklist.ChunkList
| // Optional: Storage backend to use when Reference objects refer to | ||
| // objects stored in another Content Addressable Storage. | ||
| BlobAccessConfiguration content_addressable_storage = 5; | ||
| ContentAddressableStorageConfiguration content_addressable_storage = 5; |
There was a problem hiding this comment.
Yeah, this is interesting. We'll need to figure out how to make ICAS work in the post chunking world. But are we sure that this is conceptually correct? Could a Chunk Store of type ReferenceExpanding be backed by another CAS? I can understand how it can be backed by another Chunk Store.
If we don't know how to solve this specific aspect, I'd be happy to phase out this specific feature. As in, keep ICAS support there, but remove the ability to reference another CAS.
There was a problem hiding this comment.
The other CAS may have other chunking parameters making it so that if we refer to chunks from a different chunk storage then we have to consider it to be a CAS in the context.
| @@ -0,0 +1,13 @@ | |||
| syntax = "proto3"; | |||
There was a problem hiding this comment.
chunklist.proto to match the directory name.
| } | ||
| if digest.SizeBytes < 0 { | ||
| return BadDigest, status.Error(codes.InvalidArgument, "Negative size digest provided") | ||
| } |
There was a problem hiding this comment.
Why is this specific change necessary? Function.NewDigest() also has a similar check, right?
There was a problem hiding this comment.
it was not
| serverCapabilities, err := getServerCapabilitiesWithCacheCapabilities(ctx, ba.capabilitiesClient, instanceName) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| cacheCapabilities := serverCapabilities.CacheCapabilities | ||
| // Only return fields that pertain to Chunk List Storage. | ||
| return &remoteexecution.ServerCapabilities{ | ||
| CacheCapabilities: &remoteexecution.CacheCapabilities{ | ||
| SplitBlobSupport: cacheCapabilities.SplitBlobSupport, | ||
| SpliceBlobSupport: cacheCapabilities.SpliceBlobSupport, | ||
| }, | ||
| }, nil |
There was a problem hiding this comment.
I thought we agreed that GetCapabilities() should always be invoked against the Chunk Store, not the Chunk List Store. In other words, this needs a panic(), just like we have for ICAS, ISCC, etc.
| } | ||
|
|
||
| // GetBytes returns the bytes of a digest from the CAS as a byteslice. | ||
| func GetBytes(ctx context.Context, cas ContentAddressableStorage, d digest.Digest) ([]byte, error) { |
There was a problem hiding this comment.
A function like this should respect an upper size limit.
|
|
||
| // ContentAddressableStorage is an interface which describes | ||
| // interactions with a Content Addressable Storage (CAS). | ||
| type ContentAddressableStorage interface { |
There was a problem hiding this comment.
I think we need to take a step back and ask ourselves what this interface actually abstracts. Right now it's only there to stitch a couple of things together: Chunk Store, Chunk List Store, and fetching of capabilities, and I'm not sure that's worth a lot.
Why can't we have implementations of MessageReader and StreamReader that are directly backed by:
- An instance of cdc.ParametersFetcher
- A Chunk Store BlobAccess
- A Chunk List Store BlobAccess
?
For uploading files/protos into the CAS, that's where we need to write a new abstraction. But that can potentially live in bb-remote-execution?
There was a problem hiding this comment.
I have removed the ContentAddressableStorage interface in favor of taking whatever is needed in the operations themselves
| SplitBlobSupport: cacheCapabilities.SplitBlobSupport, | ||
| SpliceBlobSupport: cacheCapabilities.SpliceBlobSupport, |
There was a problem hiding this comment.
So the entire idea behind these GetCapabilities() implementations is that they return/yield the kinds of properties that are variable. In this implementation we're going to require that implementations support chunking.
So with that in mind, I think that this function should only check that the CacheCapabilities returned by the server have SplitBlobSupport and SpliceBlobSupport set to true. It doesn't need to return those fields here. Then in cmd/bb_storage/main.go, we should set these fields to true (at the same place where we set SupportedCompressors).
Similarly, this code should never return a ServerCapabilities that doesn't have RepMaxCdcParams set.
| var casCapabilitiesProvider = capabilities.NewStaticProvider(&remoteexecution.ServerCapabilities{ | ||
| var csCapabilitiesProvider = capabilities.NewStaticProvider(&remoteexecution.ServerCapabilities{ | ||
| CacheCapabilities: &remoteexecution.CacheCapabilities{ | ||
| DigestFunctions: digest.SupportedDigestFunctions, |
There was a problem hiding this comment.
This should have SupportsSplitBlobs and such set.
| @@ -16,7 +15,6 @@ type BlobAccess interface { | |||
| capabilities.Provider | |||
|
|
|||
| Get(ctx context.Context, digest digest.Digest) buffer.Buffer | |||
There was a problem hiding this comment.
This may now return (buffer.Buffer, error)
There was a problem hiding this comment.
Returns (T, error)
|
|
||
| Get(ctx context.Context, digest digest.Digest) buffer.Buffer | ||
| GetFromComposite(ctx context.Context, parentDigest, childDigest digest.Digest, slicer slicing.BlobSlicer) buffer.Buffer | ||
| Put(ctx context.Context, digest digest.Digest, b buffer.Buffer) error |
There was a problem hiding this comment.
Consider making Buffer a generic type now.
- CAS:
buffer.Buffer[[]byte] - AC:
buffer.Buffer[*remoteexecution.ActionResult] - CLS:
buffer.Buffer[cdc.ChunkList]
?
type BufferMarshaler[T any] {
Marshal(digestFunction digest.Function, T any) ([]byte, error)
Unmarshal(digestFunction digest.Function, []byte) (T, error)
}There was a problem hiding this comment.
blobstore.BlobAccess is now generic
eb4b94a to
e8eae1a
Compare
|
State of latest commit: Review comments addressed and rebased on top of the lossymap implementation. This version does not use the lossymap but it could be an alternative to using the generic ttlcache.TTLCache. One question that remains before we do that is if we case about the eviction set eviction policy. It may be desireable to pick a different implementation for different caches, say an LRU implementation for the chunk cache, a FIFO implementation for the chunk list cache etc etc. |
| @@ -0,0 +1,147 @@ | |||
| package buffer | |||
There was a problem hiding this comment.
Do we think that it's fitting to put this in pkg/blobstore/buffer? Maybe the correct thing to do is to rename pkg/blobstore/chunklist to pkg/blobstore/chunk, and then move this in there as well?
There was a problem hiding this comment.
moved to pkg/blobstore/chunk
|
|
||
| mu sync.Mutex | ||
|
|
||
| hasData atomic.Bool |
There was a problem hiding this comment.
Out of curiosity, when would a buffer not have any data in decompressed form? Given that we now always perform checksum validation, we always end up decompressing objects anyway. This is probably not necessary?
There was a problem hiding this comment.
In the get path of a blob access we don't decompress to validate the chunks. So when fetching compressed data there is decompression on the storage side to validate the integrity of the blob but otherwise all in betweens just pass the compressed data back without inspecting it.
We could reduce that to zero decompressions on the get path by letting the ChunkCoder not perform integrity checks on decoding and joining the ChunkCoder with the XXH64SufficCoder for integrity checks (effectively taking the XXH64 of the compressed bytes).
| type Chunk struct { | ||
| zstdPool zstd.Pool | ||
|
|
||
| mu sync.Mutex |
There was a problem hiding this comment.
Slice assignment is not atomic so the mutex is needed for correctness. The risk is otherwise that a thread could read new slice header + old length though I'm uncertain if that could happen in practice in the codebase. In either case with the switch to atomic.Pointer[[]byte] makes it moot.
| data []byte | ||
|
|
||
| hasCompressedData atomic.Bool | ||
| compressedData []byte |
There was a problem hiding this comment.
Maybe it's a bit more elegant to use a atomic.Pointer[[]byte] here?
There was a problem hiding this comment.
I did atomic.Pointer as the function theoretically could fail and sync.Once can not take a context or return an error so a cancelation on the GetEncoder would consume the Once and have the value be error for ever.
| @@ -0,0 +1,9 @@ | |||
| package buffer | |||
There was a problem hiding this comment.
Given that nothing in pkg/blobstore/buffer uses this interface, could we remove this? Or maybe move it elsewhere?
| // chunklist.ChunkList into an efficient binary format. | ||
| // | ||
| // The binary schema uses a Struct-of-Arrays (SoA) layout to group sizes | ||
| // together which maximizes compressibility but does not itself compress |
There was a problem hiding this comment.
Even with the sizes and hashes split, I don't think that compressing these records is ever advantageous. The hashes are 32 bytes in size and are simply not compressible. The sizes are only 4 bytes. So in the best case we only save 4/(32+4) = 1/9 the space.
There was a problem hiding this comment.
Zstd encoder removed
| zstdMinimumBufferSize = 64 | ||
| ) | ||
|
|
||
| type zstdCoder struct { |
There was a problem hiding this comment.
So this Coder is only going to be used by non-chunkstore, right? So this would allow compressing AC entries, etc. Have we ever observed this to be necessary? AC entries are tiny.
There was a problem hiding this comment.
I mostly added it for symmetry with the chunk store but it might be true that we never need to do this.
| // Any errors generated above may be caused by | ||
| // data corruption on the Tree object. Force | ||
| // reading the Tree until completion, and prefer | ||
| // read errors over any errors generated above. |
There was a problem hiding this comment.
I believe that in the case of REv2 this still holds. It may well be the case that storage serves us a bad ChunkList, where the chunks when concatenated don't add up to the original digest. Implementations of StreamReader still need to check for this. They should reject handing out the final chunk in those cases.
| } | ||
|
|
||
| func (xxh64SuffixCoder) Encode(data []byte, parentDigest digest.Digest) ([]byte, error) { | ||
| hash := xxhash.Sum64(data) |
There was a problem hiding this comment.
I would consider adding parentDigest to the hash as well. That way we don't just validate that the object is intact. We also verify that it belongs to the digest under which it is stored.
There was a problem hiding this comment.
Clarification: we should likely include parentDigest.GetKey(digest.KeyWithoutInstance). So not the instance name.
| DigestFunctions: digest.SupportedDigestFunctions, | ||
| DigestFunctions: digest.SupportedDigestFunctions, | ||
| SplitBlobSupport: true, | ||
| SpliceBlobSupport: true, |
There was a problem hiding this comment.
Again, I think that setting these fields should be the responsibility of cmd/bb_storage/main.go. Inside of BlobAccess we should only set fields that are actually variable.
| func (ba *clsBlobAccess) Get(ctx context.Context, blobDigest digest.Digest) (chunklist.ChunkList, error) { | ||
| digestFunction := blobDigest.GetDigestFunction() | ||
|
|
||
| // TODO: Replace with streaming variant |
There was a problem hiding this comment.
Even though this is already stated as a TODO, I'm specifically adding a comment here so we don't forget. In my opinion we should only invoke the new API internally.
| for i, proto := range splitBlobsResponse.ChunkDigests { | ||
| d, err := digestFunction.NewDigestFromProto(proto) | ||
| if err != nil { | ||
| return chunklist.ChunkList{}, err | ||
| } | ||
| chunkList.Offsets[i] = offset | ||
| chunkList.Digests[i] = d | ||
| offset += uint64(d.GetSizeBytes()) | ||
| } |
There was a problem hiding this comment.
This should likely be the place where we also validate that the combined size of the chunk list matches the digest. Probably want to use https://pkg.go.dev/math/bits#Add64 here to ensure stuff doesn't overflow.
| } | ||
|
|
||
| r := resp.Responses[0] | ||
| if r.GetStatus().GetCode() != int32(codes.OK) { |
There was a problem hiding this comment.
Maybe cleaner to write something like this:
if err := status.ErrorProto(r.Status); err != nil {
return nil, err
}|
|
||
| switch r.Compressor { | ||
| case remoteexecution.Compressor_IDENTITY: | ||
| return buffer.NewChunk(ba.zstdPool, r.Data), nil |
There was a problem hiding this comment.
buffer.NewChunk() can't fail right now. Wouldn't it be better to do the checksum validation at this exact spot?
| if err != nil { | ||
| return nil, err | ||
| } | ||
| for i := 0; i < len(digests); i++ { |
There was a problem hiding this comment.
for i, digest := range digests {
}| // Limit the amount of data that is read to one byte beyond the | ||
| // advertised size, so that corrupt or malicious streams cannot | ||
| // trigger unbounded decompression. |
There was a problem hiding this comment.
This is not sufficient to ensure a client can't send a malicious request that causes the server to run out of memory. You could create a legitimate request that contains a 1 TB blob that compresses down to just a couple of bytes. Then this code will call io.ReadAll(1TB + 1).
There was a problem hiding this comment.
Good point, I just removed the PutBytes convenience function as PutReader already handles that case perfectly well and consumers can just wrap their bytes with bytes.NewReader themselves, they don't need a helper for it.
| if status.Code(err) != codes.NotFound { | ||
| return zero, util.StatusWrap(err, name) | ||
| } | ||
| err = replicator.ReplicateMultiple(ctx, digest.ToSingletonSet()) |
There was a problem hiding this comment.
Doesn't this mean that we actually load the same object twice from the second backend? The idea behind ReplicateSingle() was that this could be prevented.
There was a problem hiding this comment.
It does, but isn't this already the behavior internally for e.g. concurrency_limiting and queueing replicators?
There was a problem hiding this comment.
For some implementations that's indeed the case, but not for ones like local. And the nice thing is that in the absence of streaming there is even more of a reason to offer it. The reason concurrency_limiting and queueing couldn't do it was because they couldn't guarantee that the caller processes the buffer immediately/fast enough. However, if we don't have large objects, then this is never any issue.
| // capablitiesProvider is a copy of the capabilities.Provider interface | ||
| // which must be written inline for go mockgen to function in source | ||
| // mode. |
There was a problem hiding this comment.
It is indeed, really sad about that aspect of our gomocks but could not manage to find any other solution.
| // | ||
| // More details: https://github.com/bazelbuild/bazel/issues/11063 | ||
| func NewEmptyBlobInjectingBlobAccess(base BlobAccess) BlobAccess { | ||
| func NewEmptyBlobInjectingBlobAccess(base BlobAccess[*buffer.Chunk]) BlobAccess[*buffer.Chunk] { |
There was a problem hiding this comment.
I was thinking: do we even need a BlobAccess like this now?
With chunking in place, we already have logic at the high level to distinguish between small and large objects. Maybe it's better if the empty digest is also handled in those cases?
In other words: we forbid the creation of empty Chunk objects.
There was a problem hiding this comment.
I am not sure, what about things like completeness checking blob access, tree/directory objects or chunk lists? Couldn't they refer to empty blobs and thus we would have to handle it at their call sites as well to be spec compliant.
There was a problem hiding this comment.
They could. But the digests that they embed always refer to high-level CAS objects (read: files), not individual chunks. And EmptyBlobInjectingBlobAccess would only be capable of injecting empty chunks, which is something we should never see.
0858941 to
068b72c
Compare
| return util.StatusWrapf(err, "Failed to read configuration from %s", os.Args[1]) | ||
| } | ||
|
|
||
| if configuration.TraversalConcurrency <= 0 { |
There was a problem hiding this comment.
Is this option actually needed? I don't see it getting used anywhere.
| configuration.Source, | ||
| blobAccessCreator, | ||
| ) | ||
| zstdPool := bb_zstd.NewPoolFromConfiguration(nil) |
There was a problem hiding this comment.
Just out of curiosity, do you think we still need zstd pools? I think the only reason we added them was because we had to do streaming compression/decompression. Now that we only work with chunks, I suspect it's perfectly reasonable if we had a runtime.NumCPU() sized pool encoders/decoders, and placed a semaphore around it?
| } | ||
| chunkDigests = append(chunkDigests, chunkDigest) | ||
| } | ||
| if len(chunkDigests) > s.maximumChunkCount { |
There was a problem hiding this comment.
Q: Is there any particular reason we want to check this at this specific level? Wouldn't it be sufficient if we checked this in ChunkWhateverValidatingBlobAccess?
| var blobDigestProto *remoteexecution.Digest | ||
| var chunkDigests []digest.Digest | ||
| for i := 0; ; i++ { | ||
| in, err := stream.Recv() |
There was a problem hiding this comment.
I have the feeling that this code would be slightly more elegant if it was written like this:
m, err := stream.Recv()
if err != nil {
if err == io.EOF {
return status.Error(codes.InvalidArgument, "Did not receive initial message")
}
return err
}
instanceName = ...
digestFunction = ...
for {
chunkDigest = ...
m, err = stream.Recv()
if err != nil {
if err == io.EOF {
break
}
}
}| case 0: | ||
| // No chunks, just check that it's the empty blob. | ||
| if d.GetSizeBytes() != 0 { | ||
| return status.Error(codes.InvalidArgument, "Chunk mapping does not compose to blob") | ||
| } | ||
| return nil | ||
| case 1: | ||
| // Single chunk blob, check that the chunk digest matches the | ||
| // blob digest and that the chunk is present in storage. | ||
| if chunkDigests[0] != d { | ||
| return status.Error(codes.InvalidArgument, "Chunk mapping does not compose to blob") | ||
| } | ||
| params, err := s.cdcParametersFetcher.FetchCDCParameters(ctx, d.GetInstanceName()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| missing, err := cas.FindMissing(ctx, s.chunkStorage, s.chunkMappingStorage, params, chunkDigests[0].ToSingletonSet()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !missing.Empty() { | ||
| return status.Error(codes.NotFound, "At least one chunk is missing from storage.") | ||
| } | ||
| return nil |
There was a problem hiding this comment.
Even though it isn't necessarily equal to what is in REv2, what are your thoughts on simply rejecting calls like these? The phrasing that's in the spec is all written with FastCDC in mind, which requires this kind of nonsense. Given that we only want to support RepMaxCDC, I think we should do our best to bash clients into shape.
| } | ||
|
|
||
| // NewTTLCache instantiates a reusable TTLCache for any key-value pair. | ||
| func NewTTLCache[K comparable, V any](clock clock.Clock, evictionSet eviction.Set[K], maxItems int, cacheDuration time.Duration) *TTLCache[K, V] { |
There was a problem hiding this comment.
So I fully understand your concern that the hash map implementation we have in pkg/lossymap is a bit too convoluted for what you're trying to do here.
However, what are your thoughts on moving this into pkg/lossymap and letting it be an alternative implementation of lossymap.Map? As in, add a simple_map.go or something that provides the same contract.
| // case the Validated field should be true. | ||
| type Mapping struct { | ||
| Offsets []uint64 | ||
| Digests []digest.Digest |
There was a problem hiding this comment.
I think one of the reasons we wanted to add this type was to avoid the storage space usage of regular Digest messages.
Why don't we also implement this type so that it is backed by a []byte of concatenated hashes?
| } | ||
|
|
||
| // MappingFetcher retrieves a ChunkMapping for a digest. | ||
| type MappingFetcher interface { |
There was a problem hiding this comment.
This type has no relationship with anything above/below. Maybe best to move it into a separate file?
| i, exact := slices.BinarySearch(l.Offsets, off) | ||
| if exact { | ||
| return i, 0 | ||
| } | ||
|
|
||
| localOffset := int64(off - l.Offsets[i-1]) | ||
| if localOffset < l.Digests[i-1].GetSizeBytes() { | ||
| return i - 1, localOffset | ||
| } | ||
| return len(l.Offsets), 0 |
There was a problem hiding this comment.
My gut feeling tells me that something like this should have been sufficient:
i := sort.Search(len(l.Offsets), func(i int) bool {
return l.Offsets[i] > off
}) - 1
return i, off - l.Offsets[i]| // offset and the offset within that chunk. Offsets at or beyond the end | ||
| // of the blob yield an index equal to the number of chunks in the mapping. |
There was a problem hiding this comment.
I'm not a big fan of such a contract. Why can't we say:
This method MUST be called with an offset smaller than the size of the blob.
9fe4ad4 to
dc716df
Compare
| ) | ||
|
|
||
| // Chunk is a struct of raw binary data with its digest. | ||
| type Chunk struct { |
There was a problem hiding this comment.
Move this code into pkg/cas, make it private.
| // The internal RepMaxContentDefinedChunker may peek up to this many | ||
| // bytes. We therefore make sure that the underlying buffer is big | ||
| // enough to prevent bufio.ErrBufferFull errors. | ||
| bufferSizeBytes := 2*minChunkSizeBytes + horizonSizeBytes |
There was a problem hiding this comment.
go-cdc provides a method for this nowadays:
https://github.com/buildbarn/go-cdc/blob/main/content_defined_chunker.go#L37
| } | ||
|
|
||
| func (f *cachingMappingFetcher) FetchChunkMapping(ctx context.Context, d digest.Digest) (Mapping, error) { | ||
| if chunkMapping, err := f.cache.Get(d, f.clock.Now()); err == nil { |
There was a problem hiding this comment.
Pease move locking out of the lossy map into here.
| type Chunk struct { | ||
| data []byte | ||
|
|
||
| zstdPool zstd.Pool |
There was a problem hiding this comment.
It's unfortunate that we have the pool here. We should get rid of it.
| emptyHash string | ||
| } | ||
|
|
||
| func newBareFunction(enumValue remoteexecution.DigestFunction_Value, hasherFactory func(expectedSizeBytes int64) hash.Hash, hashBytesSize int) bareFunction { |
There was a problem hiding this comment.
If we're doing this, then you can also get rid of the hashSizeBytes argument. You can just do len(emptyHash)/2.
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return data, nil |
| @@ -30,17 +28,6 @@ var ( | |||
| []string{"storage_type", "operation", "grpc_code"}, | |||
| ) | |||
|
|
|||
| blobReplicatorOperationsBlobSizeBytes = prometheus.NewHistogramVec( | |||
There was a problem hiding this comment.
Maybe restore this, but simply use the digest.GetSizeBytes() ?
| @@ -3,13 +3,12 @@ package blobstore | |||
| import ( | |||
| FetchCDCParameters(ctx context.Context, instanceName digest.InstanceName) (*remoteexecution.RepMaxCdcParams, error) | ||
| } | ||
|
|
||
| type capabilitiesCDCParametersFetcher struct { |
| // NewCDCParametersFetcher creates a CDCParametersFetcher that obtains | ||
| // the CDC parameters by calling GetCapabilities() on the provided | ||
| // capabilities.Provider. | ||
| func NewCDCParametersFetcher(provider Provider) CDCParametersFetcher { |
This commit adds support for the SplitBlob and SpliceBlob methods from the Remote Execution v2 (REv2) api. SplitBlob and SpliceBlob can be used to facilitate uploads and downloads of large files but a naïve implementation like this has some major drawbacks as well. The blobs must exist in both their chunked and non chunked form, which may significantly increase storage requirements for large blobs. The protocol gives no guarantee that a large blob stored in the CAS exists in its chunked form which forces you to perform a fairly heavy Split call that loads the entire large blob in order to decomposition it into its chunks. This implementation mostly exists as a stepping stone for a different implementation where Buildbarn internally manages all blobs as chunked blobs.
This commit makes makes chunking a mandatory first class feature in Buildbarn. This removes the Content Addressable Storage (CAS) as a single blob access and it instead is a logical combination of two storage configurations that work in tandem. A Chunk Storage (CS) which is content addressed and contains chunks of blobs, and a Chunk Mapping Storage (CMS) which is addressed by a blob digest and contains a chunk.Mapping describing the chunks that make up the blob. All API calls are automatically translated to use Chunk Mappings created with RepMaxCDC. Effectively this means that large blobs no longer exists in the storage layer, individual chunks of the large blobs are in turn deduplicated in such a manner that the chunks are stored only once. The automatic translation makes certain that clients that are not CDC aware can still continue to use the storage backend without performing any changes. Clients which support RepMaxCDC also gets a significant reduction in the amount of blobs to transfer as they only need to transfer modified chunks rather than the entire blob.
dc716df to
0f14837
Compare
This commit builds on top of our split and splice blob support to make
it a mandatory first class feature in Buildbarn. With this commit the
Content Addressable Storage (CAS) is created from two Storage
configurations that work in tandem. A Chunk Storage (CS) which is
content addressed and contains chunks of blobs, and a Chunk List Storage
(CLS) which is addressed by a blob digest and contains a manifest
describing the chunks that make up the blob.
All api calls are automatically translated to use Chunk Lists created
with RepMaxCDC. Effectively this means that large blobs no longer exists
in the storage layer, individual chunks of the large blobs are in turn
deduplicated in such a manner that the chunks are stored only once.
The automatic translation makes certain that clients that are not cdc
aware can still continue to use the storage backend without performing
any changes. Clients which support RepMaxCDC also gets a significant
reduction in the amount of blobs to transfer as they only need to
transfer modified chunks rather than the entire blob.