Skip to content

feat: add EgressBatchStore#245

Merged
volmedo merged 22 commits intomainfrom
vic/feat/egress-batch-store
Oct 3, 2025
Merged

feat: add EgressBatchStore#245
volmedo merged 22 commits intomainfrom
vic/feat/egress-batch-store

Conversation

@volmedo
Copy link
Copy Markdown
Contributor

@volmedo volmedo commented Sep 18, 2025

Ref: #174

Implement an EgressBatchStore that allows appending space/content/retrieve receipts to a batch. When the batch is above the max, it is flushed automatically.

Things that are missing and will be implemented in follow up PRs:

  • sending the batch to the egress tracking service in a space/egress/track invocation
  • exposing an http.FileSystem and adding the corresponding http.FileServer endpoint

Once these are done, I'll hook it up in Alan's space/content/retrieve handler.

@volmedo volmedo self-assigned this Sep 18, 2025
@volmedo volmedo force-pushed the vic/feat/egress-batch-store branch 2 times, most recently from 5047631 to 479d257 Compare September 18, 2025 17:52
Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
}, nil
}

func (s *fsBatchStore) Append(ctx context.Context, rcpt receipt.Receipt[content.RetrieveOk, fdm.FailureModel]) error {
Copy link
Copy Markdown
Member

@alanshaw alanshaw Sep 19, 2025

Choose a reason for hiding this comment

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

I think we could generalize this as a "CAR logger" the parameter here could be an "Archiver" or any other function that returns bytes when called...

Or preferably, it could be a generic type and you could pass a "block encoder" to the constructor that will create a cid and bytes pair for the thing you're logging.

...and then you could have a callback for onFlush to implement the invocation to egress tracker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I prefer keeping things as stupidly simple as possible until there is a use case that justifies the extra complexity. What we need today is a component that handles batches of space/content/retrieve receipts. I'm not sure how likely it is that we will need to do a similar handling of some other bytes, but happy to generalize this when that need arises.

I like the idea of the callback though. Do you mind if I compare it with the current approach in #246 and implement there if it looks better?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think I prefer what you've done in #246

Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
batchFileSuffix = ".car"
)

var _ EgressBatchStore = (*fsBatchStore)(nil)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not convinced by the name - to me a store is something you put stuff in that you can also get stuff back out of. This is an append only log/logger - you cannot/do not want to retrieve items from it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actually, we will want to retrieve items from it when implementing the endpoint from which receipt batches will be fetched by the egress tracking service during consolidation, so I think it qualifies as a store. But happy to think of a different name if that argument still feels unconvincing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes ok I take your point. I was thinking in terms of retrieving individual receipts, there is an asymmetry here because you don't retrieve the receipts, you retrieve the batches.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah, gotcha. Something like ReceiptBatcher then? Sorry, I'm not super inspired today 😐

Copy link
Copy Markdown
Member

@frrist frrist Sep 23, 2025

Choose a reason for hiding this comment

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

How about "Journal" - imo that name implies both the append-only nature and the fact that we're preserving a record of what happened. At a high level, I think of this roughly as a filesystem journal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

SGTM - nice name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll do the rename once I merge the other two branches into this one if that's ok. I think that'd be easier than dealing with the merge conflicts.

Copy link
Copy Markdown
Member

@frrist frrist left a comment

Choose a reason for hiding this comment

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

Blocking due to:

  • uncertainty on the finalize method, gut says we don't want/need to call this. Might just want to call close?
  • the (re)opening of the current file for each append operation.

Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
@@ -0,0 +1,176 @@
package egressbatchstore
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

At a high-level I'd like us to look at separating the "sequential append" concerns from the "what format am I writing" concerns. This can of course come in a follow on.

As Alan has already suggested, we can have a "generic" journal that handles the "append" and "rotate" operations. Then plug something like a "CAR Writer" into it that handles the formatting of the data. For example, the JobQueue we use attempts to follow this kind of design.

This will make testing easier, and allow the component to be reused for other things when the time arises. I'd imagine having a robust/reusable journal implementation will come in handy down the road.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As I replied to Alan's suggestion, I'm not trying to implement a generic store. Our codebase is full of stores that are not used and interfaces with a single implementation. I like to apply the YAGNI mantra as much as possible.

Happy to work on a generic/reusable implementation when/if the need arises. I created #254 to capture this proposal.

But for now I'd rather put something together that solves the particular problem we have at hand in the minimum amount of time possible.

Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
s.mu.Lock()
defer s.mu.Unlock()

rwbs, err := blockstore.OpenReadWrite(s.curBatchPath, nil)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than opening and closing the current batch file for every append operations, lets instead maintain a reference to the current batch. Then we can just open and close the file during stat up/rollover once.

I suspect this will become a bottleneck for nodes when serving lots of content.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I erred on the side of safety. The idea is not to leave the file in an inconsistent state in case of failure. If the node fails it's unclear to me if the resulting state is recoverable. Since this process is directly related to node operators getting paid, I think it makes sense to trade some performance for stability in this case.

Of course, we can always revisit the implementation in the future if this indeed becomes an issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we move all the logic together that needs locking? e.g. Obtain a lock, open rwbs, put, finalize, check size/roll. unlock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can we move all the logic together that needs locking? e.g. Obtain a lock, open rwbs, put, finalize, check size/roll. unlock.

this doesn't apply anymore since the locks were moved up a layer to the EgressTrackingService

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah right. Is the intention here then to merge #246 into this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, #246 targets this branch, not main. Sorry, my intention was to make things easier to review but it ended up being confusing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am more than happy to review this as one giant PR/branch. Might be easier that way as we work things out fwiw.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm merging the other branches into this one, but I'd like to get the discussions in them solved first so that they don't fall through the cracks.

Comment thread pkg/store/egressbatchstore/fsstore.go Outdated
Comment on lines +71 to +72
s.mu.Lock()
defer s.mu.Unlock()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What exactly is being guarded with this lock? The blockstore used below should already have locking in place for Put operations yea?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One needs to read the blockstore's code to learn that it has its own locking indeed, there is no mention of concurrency safety in the docs. I don't like depending on the underlying implementation doing the right thing (and I actually think leaving the option to users of the library to implement synchronization as they see fit is a better design, but that's another topic), so I feel more comfortable making sure things are synchronized where they should.

In any case, the idea here was to make the append and a potential rotation atomic. We don't want a subsequent call to update the batch while we are rotating it/sending it in an invocation.

Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
Comment on lines +103 to +105
if err := rwbs.Finalize(); err != nil {
return fmt.Errorf("finalizing batch: %w", err)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we only need to call this when using a carv2 with an index: https://github.com/ipld/go-car/blob/v2.15.0/v2/internal/store/index.go#L90 - I assume that's not the case here? If so can probably drop this call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm actually trying to use CARv2. Among the features of CARv2, the docs mention:

Write CARv2 files via Read-Write blockstore API, with support for appending blocks to an existing CARv2 file, and resumption from a partially written CARv2 files.

which looks like a good fit for the egress batching store use case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this also means the CAR file(s) we batch will include an index in addition to the data we are serving to the egress tracker. I understand said index is required for using the RWBS API, but it means extra data to move around. Worth documenting this in the method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

your comment got me thinking. It's true that an indexed CARv2 is required to use blockstore.ReadWrite. However, at consolidation time all blocks in the CAR batch will be iterated sequentially, so the index is not useful in that case. Therefore, we could use a CARv2 for batching, but send a CARv1 to the egress tracker.

I also thought of storing rotated batches directly as CARv1, but I think having them as CARv2 will be useful when we implement receipt consolidation handling, as it might be necessary to read from the batch those receipts that had errors.

I created #259 to evaluate this alternative once we have a better idea about how consolidation errors will be handled.

Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
s.mu.Lock()
defer s.mu.Unlock()

rwbs, err := blockstore.OpenReadWrite(s.curBatchPath, nil)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skimming the car blockstore code leads me to believe we need to call Close on this when were done with it iff using CarV2 - should we include that here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's not necessary, Finalize does it. According to the docs:

Finalize finalizes this blockstore by writing the CARv2 header, along with flattened index for more efficient subsequent read. This is the equivalent to calling FinalizeReadOnly and Close. After this call, the blockstore can no longer be used.

and I confirmed the code does it indeed.

@frrist frrist added this to the v0.0.15 milestone Sep 30, 2025
@volmedo
Copy link
Copy Markdown
Contributor Author

volmedo commented Oct 1, 2025

Blocking due to:

  • uncertainty on the finalize method, gut says we don't want/need to call this. Might just want to call close?
  • the (re)opening of the current file for each append operation.

@frrist are these still a concern? PTAL at my replies and let me know what you think.

@volmedo
Copy link
Copy Markdown
Contributor Author

volmedo commented Oct 1, 2025

@alanshaw any concerns beyond the name of the store?

volmedo added a commit that referenced this pull request Oct 1, 2025
Ref: #174 

Depends on: #245 and #246 

Attach a ReceiptLogger to the `space/content/retrieve` handler to
collect retrieval receipts and add them to the EgressTrackingService to
be stored, batched and sent in `space/egress/track` invocations.
Ref: #174 

I separated the logic for storing receipt batching and the one sending
those batches to the egress tracking service. Now the store just stores
the batches, and notifies when they are ready. The new
`EgressTrackingService` will invoke `space/egress/track` with new
batches.
@frrist
Copy link
Copy Markdown
Member

frrist commented Oct 1, 2025

Yeah, I'm still concerned about the performance implications. The current implementation calls OpenReadWrite() and Finalize() for every Append operation. Suggestions have been provided on how to address this here.

I spent some time looking at the go-car package design, and have come away with the understanding its API is meant to be used with a single Open, one or more Put() calls, then one Finalize() at the end. You can see this pattern in the test cases for the package.

Currently this implementation does Open -> Put -> Finalize per receipt. That means every append re-opens the file and runs the resume logic, which reads the entire CAR to rebuild the index. We're paying the "resumption" cost for each Append, which is expensive. We can reduce this with a fairly simple change: keep the RWBS open between appends and only call Finalize() when rotating batches, as mentioned previously.

Even with proper API usage, CarV2 builds an index for each batch that gets discarded during consolidation. For append-only sequential access, CarV1 would be more appropriate and have lower bandwidth requirements.

I'm not trying to block progress here, I want to ship this too! But I also don't want to ship something we already know has unnecessary overhead just to get it out the door. The amount node operators get paid is somewhat coupled to the performance of this design, and fixing performance issues in production is more expensive than getting it right now.

Can we please address the open/finalize-per-append pattern first? That seems like a clear bug in how the API is being used, and the fix should be straightforward.

@volmedo
Copy link
Copy Markdown
Contributor Author

volmedo commented Oct 1, 2025

I understood from this example in the docs that ReadWrite was suitable for efficiently resuming the CAR file after finalization. I have just checked the implementation now and I find it very surprising (and a bit frustrating, honestly) that the index is discarded each time the file is resumed. I assumed (wrongly) that resuming the file would be an efficient operation.

I'm confused now. Among the features of CARv2, the docs mention:

Write CARv2 files via Read-Write blockstore API, with support for appending blocks to an existing CARv2 file, and resumption from a partially written CARv2 files.

What is the advantage of CARv2 in this regard? Can a CARv1 file be resumed the same way? I'm happy to use CARv1 if we are not getting any benefits from using v2.

One final consideration, as I replied to your original suggestion, the reason I implemented the Open -> Put -> Finalize each time a receipt is added was to trade performance for consistency. I just checked the implementation and it looks to me that Finalize and Close actually do nothing when using CARv1.

To summarize: I'm moving to CARv1 and will Open at startup and Finalize at rotation, as you suggested. I was clearly running under wrong assumptions.

@frrist
Copy link
Copy Markdown
Member

frrist commented Oct 1, 2025

What is the advantage of CARv2 in this regard? Can a CARv1 file be resumed the same way? I'm happy to use CARv1 if we are not getting any benefits from using v2.

My understanding is that CarV1 doesn't have the notion of resumption since it's just a list of CIDs and their bytes, and resumption is for ensuring the index of a CarV2 is valid, CarV1 has no index, thus the operation is moot.

In my experience CarV2 has only been valuable when performing random access over the data, treating it as a "store", thus why it has an index.

Candidly, a CAR(v1 or v2) file isn't really the correct "thing" for this use case. They are designed for DAGs with root CIDs. The header describes entry points to a graph. But these receipts are independent blocks with no DAG structure. We're passing nil for roots because there aren't any. But using it here is totally fine with me as we use it as the canonical medium for passing around data over a transport layer. Much excitement for Filepack wrt to this point.


Looking at the commit just pushed which moved this to CarV1, here's an sketch for an alternative:

// serilz the receipt data
rcptArchive := rcpt.Archive()
archiveBytes, err := io.ReadAll(rcptArchive)
if err != nil {
    return false, cid.Cid{}, err
}

// make the cid
archiveCID, err := cid.V1Builder{
    Codec:    uint64(multicodec.Car),
    MhType:   uint64(multihash.SHA2_256),
}.Sum(archiveBytes)
if err != nil {
    return false, cid.Cid{}, err
}

// cid to bytes
cidBytes := archiveCID.Bytes()
// append a line in the car file, this is what `Put` is doing internally, but less complicated.
if err := util.LdWrite(s.writer, cidBytes, archiveBytes); err != nil {
    return false, cid.Cid{}, err
}
// record the size of the data written
blockSize := util.LdSize(cidBytes, archiveBytes)
s.currentSize += int64(blockSize)

This will produce a carv1 roughly resembling:

[varint(len)][cid][block data]
[varint(len)][cid][block data]
[varint(len)][cid][block data]

Then it comes to locking, just lock around the util.LdWrite operation. An atomic Integer can be used for tracking size of the batch, and possibly the comparison with w/ maxBatchSize. If rotation is required, we'll need more locking there too. My preference remains to perform locking internal to this method, to prevent callers from ever having the chance to misuse it.

Copy link
Copy Markdown
Member

@frrist frrist left a comment

Choose a reason for hiding this comment

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

Much better, thanks. Left some more comments on the change to the Journal implementation. Happy for them to be addressed in a follow on.

Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
type fsJournal struct {
basePath string
curBatchPath string
rwbs *blockstore.ReadWrite
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we're getting much from this, could just be an *os.File.

Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
Comment on lines +70 to +78
if s.rwbs == nil {
// Open a new read-write blockstore for the current batch
rwbs, err := blockstore.OpenReadWrite(s.curBatchPath, nil, blockstore.WriteAsCarV1(true))
if err != nil {
return false, cid.Cid{}, fmt.Errorf("opening current batch for writing: %w", err)
}

s.rwbs = rwbs
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Preference for this to be done in:

  1. NewFSJournal: this allows initialization errors to be caught at construction, rather than the first call to Append
  2. rotate: Keeps the Append method focused on business logic of writing blocks.

Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
Comment on lines +104 to +108
// rotate the batch if it exceeds the size limit
curSize, err := s.currentBatchSize()
if err != nil {
return false, cid.Cid{}, fmt.Errorf("checking current batch size: %w", err)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd prefer to remove the Stat call caused by this operation, one Stat per Append isn't ideal for hot path code. Instead, we can use the util.Ld* methods I mentioned in my comment, and track the size of the batch directly. NewFSJournal can perform the initial Stat operation for the case of resuming writing to an existing batch.

Comment thread pkg/store/retrievaljournal/fsstore.go Outdated
Comment on lines +160 to +163
// Rename the file to include the CID
if err := os.Rename(s.curBatchPath, newPath); err != nil {
return cid.Cid{}, fmt.Errorf("renaming batch file: %w", err)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not sure what the behavior is from renaming an open file, to be safe I think we'll want to close f before calling this operation. Please address this before merge

@volmedo
Copy link
Copy Markdown
Contributor Author

volmedo commented Oct 2, 2025

@frrist wanna take a final look before merging?

@frrist
Copy link
Copy Markdown
Member

frrist commented Oct 2, 2025

LGTM :shipit:

Comment thread pkg/fx/app/ucan.go Outdated
blobs.Module, // Provides blob service and handler
claims.Module, // Provides claims service and handler
publisher.Module, // Provides publisher service and handler
egresstracking.Module, // Provides egress tracking service
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
egresstracking.Module, // Provides egress tracking service
egresstracker.Module, // Provides egress tracking service

We have aggregator. publisher, replicator, principalresolver already - for more consistency?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, I used egresstracking to differentiate from the egress tracker, which is the service running at the other end. But I plan to rename that to billing server, so 👍🏻

Comment thread pkg/presets/presets.go Outdated
warmStageIndexingServiceDID = lo.Must(did.Parse("did:web:staging.indexer.warm.storacha.network"))

warmStageEgressTrackingServiceURL = lo.Must(url.Parse("https://staging.etracker.warm.storacha.network"))
warmStageEgressTrackingServiceURL = lo.Must(url.Parse("https://staging.etracker.warm.storacha.network/track"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can it...be at the root?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure. It will make more sense when the service accepts other capabilities.

Changed here and storacha/etracker#4

return fmt.Errorf("reading receipt: %w", err)
}

// we're not expecting any meaningful response here so we just check for error
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

...except it's a TODO to get the consolidate task CID from the effects.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

shoot! I totally forgot about that, great catch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah, wait, yes, this is done in #264, my brain is mixing things

Comment thread pkg/store/retrievaljournal/fsjournal.go Outdated
Comment thread pkg/store/retrievaljournal/fsjournal.go Outdated

// Calculate the CID of the current batch
hash := sha256.New()
n, err := io.Copy(hash, j.currBatch)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you should calculate the hash as bytes are being appended, so that when you come to rotate you don't have to read the whole file again, which will be a pause to operations.

Obviously when you start the node you'll have to re-hash anything already written but it's not such a problem since you're already in a paused state...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And if we wanna get real fancy/fast here later we can swap out sha for BLAKE3, which is typically faster.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I love the idea. Unfortunately it's not easy to get ahold of the bytes being written when using util.LdWrite. I implemented it by seeking the current batch stream, but I think that might actually end up being even slower 🤷🏻. Take a look at 3c25095

An alternative would be to use our custom implementation of util.LdWrite and car.WriteHeader that write to the hash while writing to the underlying stream. That would also allow avoiding the use of util.LdSize, I think both util.LdWrite and car.WriteHeader should return the number of bytes written, as is usual in write functions. I'll give a stab at this tomorrow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think ldwrite is just a varint followed by the bytes...should be easy enough to do manually.

You could alternatively tee the stream?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah, that's a great idea!

@volmedo
Copy link
Copy Markdown
Contributor Author

volmedo commented Oct 3, 2025

I think all comments and suggestions have been addressed, so I'm merging this!

@volmedo volmedo merged commit 555644e into main Oct 3, 2025
10 checks passed
@volmedo volmedo deleted the vic/feat/egress-batch-store branch October 3, 2025 14:58
frrist added a commit that referenced this pull request Oct 3, 2025
small fix/follow-on to #245
Ensures when a journal is resumed, the incremental sha is re-computed,
see `TestResumeAfterRestart` for details.

---------

Co-authored-by: Vicente Olmedo <vicente@storacha.network>
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.

3 participants