Skip to content

Implement MVP mirror support in append lifecycle - #1081

Open
AlCutter wants to merge 5 commits into
transparency-dev:mainfrom
AlCutter:add_mirror_gateway
Open

Implement MVP mirror support in append lifecycle#1081
AlCutter wants to merge 5 commits into
transparency-dev:mainfrom
AlCutter:add_mirror_gateway

Conversation

@AlCutter

@AlCutter AlCutter commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

This PR adds a simple MVP implementation of mirroring support in the append lifecycle.

This approach is almost certainly going to change, but this will let us make progress in the meantime.

Towards #945

@AlCutter
AlCutter force-pushed the add_mirror_gateway branch 16 times, most recently from 6db5fc3 to 300f4a3 Compare August 5, 2026 11:51
@AlCutter
AlCutter force-pushed the add_mirror_gateway branch 2 times, most recently from 82e7391 to a3494d5 Compare August 5, 2026 15:00
@AlCutter
AlCutter force-pushed the add_mirror_gateway branch from a3494d5 to 148d465 Compare August 5, 2026 15:52
@AlCutter AlCutter changed the title Implement mirror support in append lifecycle Implement MVP mirror support in append lifecycle Aug 5, 2026
@AlCutter
AlCutter requested a review from phbnf August 5, 2026 15:52
@AlCutter
AlCutter marked this pull request as ready for review August 5, 2026 15:53
@AlCutter
AlCutter requested a review from a team as a code owner August 5, 2026 15:53
Comment thread append_lifecycle.go
// mirrorCheckpoint takes care of mirroring the given checkpoint with the provided mirror policy.
// Returns signatures from mirrors, ready to append to the checkpoint, or an error.
func mirrorCheckpoint(ctx context.Context, cp []byte, cpSize uint64, mirrors WitnessGroup, lr LogReader, httpClient *http.Client, opts MirroringOptions) ([]byte, error) {
func mirrorCheckpoint(ctx context.Context, gw *m_gateway.Gateway, policy *WitnessGroup, cp []byte, size uint64, failOpen bool) ([]byte, error) {

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.

Suggested change
func mirrorCheckpoint(ctx context.Context, gw *m_gateway.Gateway, policy *WitnessGroup, cp []byte, size uint64, failOpen bool) ([]byte, error) {
func mirrorCheckpoint(ctx context.Context, gw *m_gateway.Gateway, policy *WitnessGroup, cp []byte, size uint64, failOpen bool) ([]byte, error) {
// TODO(al): Add metrics
checkPolicy := func(sigs []byte) bool {
newCP := append(slices.Clone(cp), sigs...)
return policy.Satisfied(newCP)
}
exit := func(sigs []byte) (][byte, error) {
newCP, ok := checkPolicy(sigs)
if ok {
return sigs, nil
}
if failOpen {
slog.WarnContext(ctx, "MirrorGateway: policy not met, failing-open")
return sigs, nil
}
return sigs, fmt.Errorf("MirrorGateway: policy not met")
}
return otel.Trace(ctx, "tessera.mirrorCheckpoint", tracer, func(ctx context.Context, span trace.Span) ([]byte, error) {
sigCh := gw.CosignCheckpoint(ctx, cp, size)
var sigBlock bytes.Buffer
for {
select {
case <-ctx.Done():
return exit(sigBlock.Bytes())
case sig, ok := <-sigCh:
if !ok {
return exit(sigBlock.Bytes())
}
sigBlock.Write(sig)
return checkPolicy(sig)
}
}
})
}

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.

Very nit, but I think you can do something along these lines. (but don't apply the suggestion, you'll get into CLA issues)

Comment thread append_lifecycle.go
}
}

// CheckpointPublisherContext returns a function which should be used to create, sign, and potentially witness a new checkpoint.

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.

potentially witness and/or mirror a new checkpoint

Comment thread append_lifecycle.go
defer cancel()
var err error
ms, err = mirrorCheckpoint(ctx, cp, size, o.mirrors, lr, httpClient, o.mirrorOpts)
ms, err = mirrorCheckpoint(mirrorCtx, gw, &o.mirrors, cp, size, o.mirrorOpts.FailOpen)

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.

nit: you could move cp, size before &o.mirrors to match with witnessCheckpoint

Comment thread append_lifecycle.go
if err != nil {
return nil, fmt.Errorf("failed to parse mirror URLs: %w", err)
}
gw, err := m_gateway.NewGateway(ctx, m_gateway.Options{

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.

Should this be skipped is len(mirrorURLs)==0? I don't think it's required, but it could help readability, and avoid "ghost" metrics further down the line like it was was the case with witnessing. You could even check the length of WitnessEndpoint and only parse URLs if need be.

WithBundleFetcher(opts.LogReader.ReadEntryBundle).
WithMirrorCheckpointFetcher(mirrorFetcher.ReadCheckpoint)

c, err := mirror.NewClient(ctx, mOpts)

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.

Can the context be removed from NewClient?

}()

if err != nil {
slog.ErrorContext(ctx, "MirrorGateway: Sync failed", slog.String("url", target.url.String()), slog.Any("error", err))

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.

Should this be an error if we're replacing a goal, which hasn't even been picked up by any worker yet?

// This replacement is "racy", but the worst that can happen is that the worker has already
// picked up the old goal and we end up simply queuing the new goal instead of replacing the old one.
select {
case oldGoal := <-target.goals:

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.

Could this behavior get a slow mirror in a state where it never catches up? Should target.goals have a bit of a buffer to have piecemeal uploads?

g.targets = append(g.targets, target)

// Start the worker goroutine.
go g.runWorker(ctx, target)

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.

Should this happen in a separate loop? Otherwise, if for some reason client.NewHTTPFetcher fails, then workers that have already been started will stay around.

slog.DebugContext(cctx, "MirrorGateway: Syncing mirror", slog.String("url", target.url.String()), slog.Uint64("goal", job.cpSize))
rSigs, rErr = target.client.Sync(cctx, job.cp, job.cpSize)
if rErr != nil {
// TODO(al): Update the client so we can tell whether an error is permanent or transient, and abandon jobs which will never succeed.

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.

In the meantime, should goal include cosignCheckpoint's ctx such that chaseGoal can return if this context is being cancelled?

Comment thread append_lifecycle.go
}

// CheckpointPublisher returns a function which should be used to create, sign, and potentially witness a new checkpoint.
// Deprecated: Use CheckpointPublisherContext.

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.

Very nit: should validate throw a warning message, or at least prevent CheckpointPublisher, and CheckpointPublisherContext from both being called?

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.

2 participants