diff --git a/packages/orchestrator/chunks.proto b/packages/orchestrator/chunks.proto index 74e1ed2700..dee6f5ebd9 100644 --- a/packages/orchestrator/chunks.proto +++ b/packages/orchestrator/chunks.proto @@ -20,8 +20,8 @@ message PeerAvailability { message GetBuildFileSizeRequest { string build_id = 1; - // file_name is one of the seekable diff files: "memfile", "rootfs.ext4" - string file_name = 2; + // name is one of the seekable diff files: "memfile", "rootfs.ext4" + string name = 2; } message GetBuildFileSizeResponse { @@ -31,8 +31,8 @@ message GetBuildFileSizeResponse { message GetBuildFileExistsRequest { string build_id = 1; - // file_name is one of: "snapfile", "metadata.json" - string file_name = 2; + // name is one of: "snapfile", "metadata.json" + string name = 2; } message GetBuildFileExistsResponse { @@ -41,7 +41,7 @@ message GetBuildFileExistsResponse { message ReadAtBuildSeekableRequest { string build_id = 1; - string file_name = 2; + string name = 2; int64 offset = 3; int64 length = 4; } @@ -54,8 +54,8 @@ message ReadAtBuildSeekableResponse { message GetBuildBlobRequest { string build_id = 1; - // file_name is one of: "snapfile", "metadata.json", "memfile.header", "rootfs.ext4.header" - string file_name = 2; + // name is one of: "snapfile", "metadata.json", "memfile.header", "rootfs.ext4.header" + string name = 2; } message GetBuildBlobResponse { diff --git a/packages/orchestrator/pkg/sandbox/template/peerclient/blob.go b/packages/orchestrator/pkg/sandbox/template/peerclient/blob.go index c810578ad3..65891b2f70 100644 --- a/packages/orchestrator/pkg/sandbox/template/peerclient/blob.go +++ b/packages/orchestrator/pkg/sandbox/template/peerclient/blob.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "sync" "sync/atomic" "go.uber.org/zap" @@ -16,22 +17,49 @@ import ( var _ storage.Blob = (*peerBlob)(nil) +// peerBlob reads from the peer first; on fallthrough, opens base lazily. +// The base path is fixed at construction (blobs are not compressed). type peerBlob struct { - peerHandle[storage.Blob] + peerHandle + + openBase func(ctx context.Context) (storage.Blob, error) + + mu sync.Mutex + base storage.Blob + loaded bool +} + +func (b *peerBlob) getBase(ctx context.Context) (storage.Blob, error) { + b.mu.Lock() + defer b.mu.Unlock() + + if b.loaded { + return b.base, nil + } + + base, err := b.openBase(ctx) + if err != nil { + return nil, err + } + + b.base = base + b.loaded = true + + return base, nil } func (b *peerBlob) WriteTo(ctx context.Context, dst io.Writer) (int64, error) { - return withPeerFallback(ctx, &b.peerHandle, "peer-blob-write-to", attrOpWriteTo, + res, err := tryPeer(ctx, &b.peerHandle, "peer-blob-write-to", attrOpWriteTo, func(ctx context.Context) (peerAttempt[int64], error) { streamCtx, cancel := context.WithCancel(ctx) recv, err := openPeerBlobStream(streamCtx, b.client, &orchestrator.GetBuildBlobRequest{ - BuildId: b.buildID, - FileName: b.fileName, + BuildId: b.buildID, + Name: b.name, }, b.uploaded) if err != nil { cancel() - logger.L().Warn(ctx, "failed to open peer blob stream", logger.WithBuildID(b.buildID), zap.String("file_name", b.fileName), zap.Error(err)) + logger.L().Warn(ctx, "failed to open peer blob stream", logger.WithBuildID(b.buildID), zap.String("file_name", b.name), zap.Error(err)) return peerAttempt[int64]{}, nil } @@ -42,43 +70,55 @@ func (b *peerBlob) WriteTo(ctx context.Context, dst io.Writer) (int64, error) { n, err := io.Copy(dst, reader) if err != nil { return peerAttempt[int64]{value: n, bytes: n, hit: true}, - fmt.Errorf("failed to stream file %q from peer: %w", b.fileName, err) + fmt.Errorf("failed to stream file %q from peer: %w", b.name, err) } return peerAttempt[int64]{value: n, bytes: n, hit: true}, nil - }, - func(ctx context.Context, base storage.Blob) (int64, error) { - return base.WriteTo(ctx, dst) - }, - ) + }) + if res.hit { + return res.value, err + } + + base, err := b.getBase(ctx) + if err != nil { + return 0, err + } + + return base.WriteTo(ctx, dst) } func (b *peerBlob) Exists(ctx context.Context) (bool, error) { - return withPeerFallback(ctx, &b.peerHandle, "peer-blob-exists", attrOpExists, + res, err := tryPeer(ctx, &b.peerHandle, "peer-blob-exists", attrOpExists, func(ctx context.Context) (peerAttempt[bool], error) { resp, err := b.client.GetBuildFileExists(ctx, &orchestrator.GetBuildFileExistsRequest{ - BuildId: b.buildID, - FileName: b.fileName, + BuildId: b.buildID, + Name: b.name, }) if err == nil && checkPeerAvailability(resp.GetAvailability(), b.uploaded) { return peerAttempt[bool]{value: true, hit: true}, nil } if err != nil { - logger.L().Warn(ctx, "failed to check build file exists from peer", logger.WithBuildID(b.buildID), zap.String("file_name", b.fileName), zap.Error(err)) + logger.L().Warn(ctx, "failed to check build file exists from peer", logger.WithBuildID(b.buildID), zap.String("file_name", b.name), zap.Error(err)) } return peerAttempt[bool]{}, nil - }, - func(ctx context.Context, base storage.Blob) (bool, error) { - return base.Exists(ctx) - }, - ) + }) + if res.hit { + return res.value, err + } + + base, err := b.getBase(ctx) + if err != nil { + return false, err + } + + return base.Exists(ctx) } func (b *peerBlob) Put(ctx context.Context, data []byte, opts ...storage.PutOption) error { // Writes always go to the base provider (GCS/S3); the peer is read-only. - fallback, err := b.getOrOpenBase(ctx) + fallback, err := b.getBase(ctx) if err != nil { return err } diff --git a/packages/orchestrator/pkg/sandbox/template/peerclient/blob_test.go b/packages/orchestrator/pkg/sandbox/template/peerclient/blob_test.go index a27babc852..2038326329 100644 --- a/packages/orchestrator/pkg/sandbox/template/peerclient/blob_test.go +++ b/packages/orchestrator/pkg/sandbox/template/peerclient/blob_test.go @@ -27,13 +27,13 @@ func TestPeerBlob_WriteTo_PeerSucceeds(t *testing.T) { client := orchestratormocks.NewMockChunkServiceClient(t) client.EXPECT().GetBuildBlob(mock.Anything, mock.MatchedBy(func(req *orchestrator.GetBuildBlobRequest) bool { - return req.GetBuildId() == "build-1" && req.GetFileName() == "snapfile" + return req.GetBuildId() == "build-1" && req.GetName() == "snapfile" })).Return(stream, nil) - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{ + blob := &peerBlob{peerHandle: peerHandle{ client: client, buildID: "build-1", - fileName: "snapfile", + name: "snapfile", uploaded: &atomic.Bool{}, }} @@ -62,15 +62,17 @@ func TestPeerBlob_WriteTo_PeerNotAvailable_FallsBackToBase(t *testing.T) { base := storage.NewMockStorageProvider(t) base.EXPECT().OpenBlob(mock.Anything, "build-1/snapfile", storage.SnapfileObjectType).Return(baseBlob, nil) - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{ - client: client, - buildID: "build-1", - fileName: "snapfile", - uploaded: &atomic.Bool{}, - openFn: func(ctx context.Context) (storage.Blob, error) { + blob := &peerBlob{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: "snapfile", + uploaded: &atomic.Bool{}, + }, + openBase: func(ctx context.Context) (storage.Blob, error) { return base.OpenBlob(ctx, "build-1/snapfile", storage.SnapfileObjectType) }, - }} + } var buf bytes.Buffer n, err := blob.WriteTo(t.Context(), &buf) @@ -94,15 +96,17 @@ func TestPeerBlob_WriteTo_PeerError_FallsBackToBase(t *testing.T) { base := storage.NewMockStorageProvider(t) base.EXPECT().OpenBlob(mock.Anything, "build-1/snapfile", storage.SnapfileObjectType).Return(baseBlob, nil) - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{ - client: client, - buildID: "build-1", - fileName: "snapfile", - uploaded: &atomic.Bool{}, - openFn: func(ctx context.Context) (storage.Blob, error) { + blob := &peerBlob{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: "snapfile", + uploaded: &atomic.Bool{}, + }, + openBase: func(ctx context.Context) (storage.Blob, error) { return base.OpenBlob(ctx, "build-1/snapfile", storage.SnapfileObjectType) }, - }} + } var buf bytes.Buffer _, err := blob.WriteTo(t.Context(), &buf) @@ -139,15 +143,17 @@ func TestPeerBlob_WriteTo_UploadedSetMidStream_CompletesFromPeerThenFallsBack(t base := storage.NewMockStorageProvider(t) base.EXPECT().OpenBlob(mock.Anything, "build-1/snapfile", storage.SnapfileObjectType).Return(baseBlob, nil) - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{ - client: client, - buildID: "build-1", - fileName: "snapfile", - uploaded: uploaded, - openFn: func(ctx context.Context) (storage.Blob, error) { + blob := &peerBlob{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: "snapfile", + uploaded: uploaded, + }, + openBase: func(ctx context.Context) (storage.Blob, error) { return base.OpenBlob(ctx, "build-1/snapfile", storage.SnapfileObjectType) }, - }} + } // First download: in-flight stream completes from peer despite uploaded being set mid-stream. var buf1 bytes.Buffer @@ -170,10 +176,10 @@ func TestPeerBlob_Exists_PeerHasFile(t *testing.T) { client := orchestratormocks.NewMockChunkServiceClient(t) client.EXPECT().GetBuildFileExists(mock.Anything, mock.MatchedBy(func(req *orchestrator.GetBuildFileExistsRequest) bool { - return req.GetBuildId() == "build-1" && req.GetFileName() == "snapfile" + return req.GetBuildId() == "build-1" && req.GetName() == "snapfile" })).Return(&orchestrator.GetBuildFileExistsResponse{}, nil) - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{client: client, buildID: "build-1", fileName: "snapfile", uploaded: &atomic.Bool{}}} + blob := &peerBlob{peerHandle: peerHandle{client: client, buildID: "build-1", name: "snapfile", uploaded: &atomic.Bool{}}} ok, err := blob.Exists(t.Context()) require.NoError(t, err) assert.True(t, ok) @@ -190,15 +196,17 @@ func TestPeerBlob_Exists_PeerNotAvailable_FallsBackToBase(t *testing.T) { base := storage.NewMockStorageProvider(t) base.EXPECT().OpenBlob(mock.Anything, "build-1/snapfile", storage.SnapfileObjectType).Return(baseBlob, nil) - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{ - client: client, - buildID: "build-1", - fileName: "snapfile", - uploaded: &atomic.Bool{}, - openFn: func(ctx context.Context) (storage.Blob, error) { + blob := &peerBlob{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: "snapfile", + uploaded: &atomic.Bool{}, + }, + openBase: func(ctx context.Context) (storage.Blob, error) { return base.OpenBlob(ctx, "build-1/snapfile", storage.SnapfileObjectType) }, - }} + } ok, err := blob.Exists(t.Context()) require.NoError(t, err) @@ -217,15 +225,17 @@ func TestPeerBlob_Exists_UseStorage_FallsBackToBase(t *testing.T) { base.EXPECT().OpenBlob(mock.Anything, "build-1/snapfile", storage.SnapfileObjectType).Return(baseBlob, nil) uploaded := &atomic.Bool{} - blob := &peerBlob{peerHandle: peerHandle[storage.Blob]{ - client: client, - buildID: "build-1", - fileName: "snapfile", - uploaded: uploaded, - openFn: func(ctx context.Context) (storage.Blob, error) { + blob := &peerBlob{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: "snapfile", + uploaded: uploaded, + }, + openBase: func(ctx context.Context) (storage.Blob, error) { return base.OpenBlob(ctx, "build-1/snapfile", storage.SnapfileObjectType) }, - }} + } ok, err := blob.Exists(t.Context()) require.NoError(t, err) diff --git a/packages/orchestrator/pkg/sandbox/template/peerclient/seekable.go b/packages/orchestrator/pkg/sandbox/template/peerclient/seekable.go index 7d09b34a4f..35fd45b1ad 100644 --- a/packages/orchestrator/pkg/sandbox/template/peerclient/seekable.go +++ b/packages/orchestrator/pkg/sandbox/template/peerclient/seekable.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "sync" "sync/atomic" "go.uber.org/zap" @@ -17,9 +18,20 @@ import ( var _ storage.Seekable = (*peerSeekable)(nil) // peerSeekable reads from the peer orchestrator first. -// calls (e.g. ReadAt then OpenRangeReader) do not re-open the underlying GCS object. +// Peer fetches always use the basic (uncompressed) name. Only the base +// (GCS/S3) fallthrough path needs to know the current compression type — +// it's resolved per call from the live FrameTable, so a header swap from +// V3 to V4 (or vice versa) is reflected on the next read. type peerSeekable struct { - peerHandle[storage.Seekable] + peerHandle + + basePersistence storage.StorageProvider + objType storage.SeekableObjectType + + mu sync.Mutex + base storage.Seekable + baseCT storage.CompressionType + loaded bool // transitionEmitted ensures we signal PeerTransitionedError at most once // after the peer flips uploaded=true. The caller (build.File) reacts by @@ -30,12 +42,37 @@ type peerSeekable struct { transitionEmitted atomic.Bool } +// getBase returns a base Seekable opened against the storage path composed +// from (buildID, basic name, ct). Reopens if ct differs from the cached +// entry — a no-op for V3 (always None) but essential after a V3→V4 swap. +func (s *peerSeekable) getBase(ctx context.Context, ct storage.CompressionType) (storage.Seekable, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.loaded && s.baseCT == ct { + return s.base, nil + } + + path := storage.Paths{BuildID: s.buildID}.DataFile(s.name, ct) + + base, err := s.basePersistence.OpenSeekable(ctx, path, s.objType) + if err != nil { + return nil, err + } + + s.base = base + s.baseCT = ct + s.loaded = true + + return base, nil +} + func (s *peerSeekable) Size(ctx context.Context) (int64, error) { - return withPeerFallback(ctx, &s.peerHandle, "size peer-seekable", attrOpSize, + res, err := tryPeer(ctx, &s.peerHandle, "size peer-seekable", attrOpSize, func(ctx context.Context) (peerAttempt[int64], error) { resp, err := s.client.GetBuildFileSize(ctx, &orchestrator.GetBuildFileSizeRequest{ - BuildId: s.buildID, - FileName: s.fileName, + BuildId: s.buildID, + Name: s.name, }) if err == nil && checkPeerAvailability(resp.GetAvailability(), s.uploaded) { return peerAttempt[int64]{value: resp.GetTotalSize(), hit: true}, nil @@ -46,23 +83,32 @@ func (s *peerSeekable) Size(ctx context.Context) (int64, error) { } return peerAttempt[int64]{}, nil - }, - func(ctx context.Context, base storage.Seekable) (int64, error) { - return base.Size(ctx) - }, - ) + }) + if res.hit { + return res.value, err + } + + // Size only reaches base for V3 builds (uncompressedSize unknown); + // V4 builds carry the size in the header so the chunker never calls Size. + // V3 implies CompressionNone, matching reality. + base, err := s.getBase(ctx, storage.CompressionNone) + if err != nil { + return 0, err + } + + return base.Size(ctx) } func (s *peerSeekable) OpenRangeReader(ctx context.Context, off int64, length int64, frameTable *storage.FrameTable) (io.ReadCloser, error) { - return withPeerFallback(ctx, &s.peerHandle, "peer-seekable-open-range-reader", attrOpRangeReader, + res, err := tryPeer(ctx, &s.peerHandle, "peer-seekable-open-range-reader", attrOpRangeReader, func(ctx context.Context) (peerAttempt[io.ReadCloser], error) { streamCtx, cancel := context.WithCancel(ctx) recv, err := openPeerSeekableStream(streamCtx, s.client, &orchestrator.ReadAtBuildSeekableRequest{ - BuildId: s.buildID, - FileName: s.fileName, - Offset: off, - Length: length, + BuildId: s.buildID, + Name: s.name, + Offset: off, + Length: length, }, s.uploaded) if err != nil { logger.L().Warn(ctx, "failed to open range reader from peer", logger.WithBuildID(s.buildID), zap.Int64("off", off), zap.Int64("length", length), zap.Error(err)) @@ -75,28 +121,32 @@ func (s *peerSeekable) OpenRangeReader(ctx context.Context, off int64, length in value: newPeerStreamReader(recv, cancel), hit: true, }, nil - }, - func(ctx context.Context, base storage.Seekable) (io.ReadCloser, error) { - // Signal the caller once to fetch the post-upload header from storage; - // thereafter fall through so V3 builds (no V4 to upgrade to) don't - // loop against PeerTransitionedError. - if s.uploaded != nil && s.uploaded.Load() && s.transitionEmitted.CompareAndSwap(false, true) { - return nil, &storage.PeerTransitionedError{} - } + }) + if res.hit { + return res.value, err + } - return base.OpenRangeReader(ctx, off, length, frameTable) - }, - ) -} + if s.uploaded != nil && s.uploaded.Load() && s.transitionEmitted.CompareAndSwap(false, true) { + return nil, &storage.PeerTransitionedError{} + } -func (s *peerSeekable) StoreFile(ctx context.Context, path string, opts ...storage.PutOption) (*storage.FrameTable, [32]byte, error) { - // Writes always go to the base provider (GCS/S3); the peer is read-only. - fallback, err := s.getOrOpenBase(ctx) + base, err := s.getBase(ctx, frameTable.CompressionType()) if err != nil { - return nil, [32]byte{}, err + return nil, err } - return fallback.StoreFile(ctx, path, opts...) + return base.OpenRangeReader(ctx, off, length, frameTable) +} + +func (s *peerSeekable) StoreFile(context.Context, string, ...storage.PutOption) (*storage.FrameTable, [32]byte, error) { + // peerSeekable only exists when routingProvider routed this buildID to an + // active peer at open time, i.e. the file is being P2P-served (the peer + // owns the upload). Asking the local orchestrator to upload it is a + // contradiction. The write path uses bare persistence (Upload.store) and + // does not flow through routingProvider, so this is unreachable today; + // returning an error keeps the contradiction explicit rather than letting + // a future caller silently upload to the wrong path. + return nil, [32]byte{}, fmt.Errorf("peerSeekable: StoreFile not supported (build %s is P2P-served; writes must use the base provider directly)", s.buildID) } // openPeerSeekableStream opens a ReadAtBuildSeekable stream, checks peer availability, diff --git a/packages/orchestrator/pkg/sandbox/template/peerclient/seekable_test.go b/packages/orchestrator/pkg/sandbox/template/peerclient/seekable_test.go index 60ae758604..b5266e9094 100644 --- a/packages/orchestrator/pkg/sandbox/template/peerclient/seekable_test.go +++ b/packages/orchestrator/pkg/sandbox/template/peerclient/seekable_test.go @@ -2,7 +2,6 @@ package peerclient import ( "bytes" - "context" "errors" "io" "sync/atomic" @@ -22,10 +21,10 @@ func TestPeerSeekable_Size_PeerSucceeds(t *testing.T) { client := orchestratormocks.NewMockChunkServiceClient(t) client.EXPECT().GetBuildFileSize(mock.Anything, mock.MatchedBy(func(req *orchestrator.GetBuildFileSizeRequest) bool { - return req.GetBuildId() == "build-1" && req.GetFileName() == storage.MemfileName + return req.GetBuildId() == "build-1" && req.GetName() == storage.MemfileName })).Return(&orchestrator.GetBuildFileSizeResponse{TotalSize: 4096}, nil) - s := &peerSeekable{peerHandle: peerHandle[storage.Seekable]{client: client, buildID: "build-1", fileName: storage.MemfileName, uploaded: &atomic.Bool{}}} + s := &peerSeekable{peerHandle: peerHandle{client: client, buildID: "build-1", name: storage.MemfileName, uploaded: &atomic.Bool{}}} size, err := s.Size(t.Context()) require.NoError(t, err) assert.Equal(t, int64(4096), size) @@ -43,15 +42,16 @@ func TestPeerSeekable_Size_PeerNotAvailable_FallsBackToBase(t *testing.T) { base := storage.NewMockStorageProvider(t) base.EXPECT().OpenSeekable(mock.Anything, "build-1/memfile", storage.MemfileObjectType).Return(baseSeekable, nil) - s := &peerSeekable{peerHandle: peerHandle[storage.Seekable]{ - client: client, - buildID: "build-1", - fileName: storage.MemfileName, - uploaded: &atomic.Bool{}, - openFn: func(ctx context.Context) (storage.Seekable, error) { - return base.OpenSeekable(ctx, "build-1/memfile", storage.MemfileObjectType) + s := &peerSeekable{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: storage.MemfileName, + uploaded: &atomic.Bool{}, }, - }} + basePersistence: base, + objType: storage.MemfileObjectType, + } size, err := s.Size(t.Context()) require.NoError(t, err) assert.Equal(t, int64(8192), size) @@ -71,7 +71,7 @@ func TestPeerSeekable_OpenRangeReader_PeerSucceeds(t *testing.T) { return req.GetOffset() == 10 && req.GetLength() == int64(len(data)) })).Return(stream, nil) - s := &peerSeekable{peerHandle: peerHandle[storage.Seekable]{client: client, buildID: "build-1", fileName: storage.MemfileName, uploaded: &atomic.Bool{}}} + s := &peerSeekable{peerHandle: peerHandle{client: client, buildID: "build-1", name: storage.MemfileName, uploaded: &atomic.Bool{}}} rc, err := s.OpenRangeReader(t.Context(), 10, int64(len(data)), nil) require.NoError(t, err) defer rc.Close() @@ -94,15 +94,16 @@ func TestPeerSeekable_OpenRangeReader_PeerError_FallsBackToBase(t *testing.T) { base := storage.NewMockStorageProvider(t) base.EXPECT().OpenSeekable(mock.Anything, "build-1/memfile", storage.MemfileObjectType).Return(baseSeekable, nil) - s := &peerSeekable{peerHandle: peerHandle[storage.Seekable]{ - client: client, - buildID: "build-1", - fileName: storage.MemfileName, - uploaded: &atomic.Bool{}, - openFn: func(ctx context.Context) (storage.Seekable, error) { - return base.OpenSeekable(ctx, "build-1/memfile", storage.MemfileObjectType) + s := &peerSeekable{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: storage.MemfileName, + uploaded: &atomic.Bool{}, }, - }} + basePersistence: base, + objType: storage.MemfileObjectType, + } rc, err := s.OpenRangeReader(t.Context(), 0, int64(len(baseData)), nil) require.NoError(t, err) defer rc.Close() @@ -115,24 +116,25 @@ func TestPeerSeekable_OpenRangeReader_PeerError_FallsBackToBase(t *testing.T) { func TestPeerSeekable_OpenRangeReader_Uploaded_ReturnsPeerTransitionedError(t *testing.T) { t.Parallel() + // Once uploaded flips, the first OpenRangeReader returns + // PeerTransitionedError without touching either the peer or base. The + // caller is expected to refresh its header and retry. client := orchestratormocks.NewMockChunkServiceClient(t) + base := storage.NewMockStorageProvider(t) uploaded := &atomic.Bool{} uploaded.Store(true) - baseSeekable := storage.NewMockSeekable(t) - base := storage.NewMockStorageProvider(t) - base.EXPECT().OpenSeekable(mock.Anything, "build-1/memfile", storage.MemfileObjectType).Return(baseSeekable, nil) - - s := &peerSeekable{peerHandle: peerHandle[storage.Seekable]{ - client: client, - buildID: "build-1", - fileName: storage.MemfileName, - uploaded: uploaded, - openFn: func(ctx context.Context) (storage.Seekable, error) { - return base.OpenSeekable(ctx, "build-1/memfile", storage.MemfileObjectType) + s := &peerSeekable{ + peerHandle: peerHandle{ + client: client, + buildID: "build-1", + name: storage.MemfileName, + uploaded: uploaded, }, - }} + basePersistence: base, + objType: storage.MemfileObjectType, + } _, err := s.OpenRangeReader(t.Context(), 0, 100, nil) require.Error(t, err) @@ -140,3 +142,85 @@ func TestPeerSeekable_OpenRangeReader_Uploaded_ReturnsPeerTransitionedError(t *t var transErr *storage.PeerTransitionedError require.ErrorAs(t, err, &transErr) } + +// TestPeerStorageProvider_FullTransitionFlow walks the whole peerclient +// surface across a peer→storage transition with a header swap from V3 (basic +// path) to V4 (zstd-compressed path). Regression cover for the bug where the +// post-transition read kept hitting the original uncompressed path. +// +// Sequence: +// 1. Pre-transition: caller passes ft={ct=None}; peer answers; bytes flow. +// 2. Peer signals UseStorage; uploaded flips to true. +// 3. First post-transition call: peerSeekable returns PeerTransitionedError +// immediately (no peer call, no base open). +// 4. Caller (build.File.retryOnTransition, simulated here) reloads the V4 +// header and retries with ft={ct=Zstd}. +// 5. peerSeekable falls through to base, which opens "build-1/memfile.zstd" +// (not "build-1/memfile") and serves the compressed bytes. +func TestPeerStorageProvider_FullTransitionFlow(t *testing.T) { + t.Parallel() + + uploaded := &atomic.Bool{} + + prePeerBytes := []byte("pre-transition peer payload") + postBaseBytes := []byte("post-transition compressed payload") + + // Pre-transition peer stream: serves bytes once, then EOF. uploaded is + // flipped via UseStorage on the EOF response so subsequent calls skip + // the peer. + preStream := orchestratormocks.NewMockChunkService_ReadAtBuildSeekableClient(t) + preStream.EXPECT().Recv().Return(&orchestrator.ReadAtBuildSeekableResponse{Data: prePeerBytes}, nil).Once() + preStream.EXPECT().Recv().RunAndReturn(func() (*orchestrator.ReadAtBuildSeekableResponse, error) { + uploaded.Store(true) + + return nil, io.EOF + }).Once() + + client := orchestratormocks.NewMockChunkServiceClient(t) + client.EXPECT().ReadAtBuildSeekable(mock.Anything, mock.MatchedBy(func(req *orchestrator.ReadAtBuildSeekableRequest) bool { + // Peer is asked by basic name only. + return req.GetBuildId() == "build-1" && req.GetName() == storage.MemfileName + })).Return(preStream, nil).Once() + + // Base is only consulted post-transition, and only against the compressed + // path. If the bug regresses (uncompressed path), this expectation fails. + postBaseSeekable := storage.NewMockSeekable(t) + postBaseSeekable.EXPECT(). + OpenRangeReader(mock.Anything, int64(0), int64(len(postBaseBytes)), mock.Anything). + Return(io.NopCloser(bytes.NewReader(postBaseBytes)), nil).Once() + + base := storage.NewMockStorageProvider(t) + base.EXPECT(). + OpenSeekable(mock.Anything, "build-1/memfile.zstd", storage.MemfileObjectType). + Return(postBaseSeekable, nil).Once() + + p := newPeerStorageProvider(base, client, uploaded) + seekable, err := p.OpenSeekable(t.Context(), "build-1/memfile", storage.MemfileObjectType) + require.NoError(t, err) + + // 1. Pre-transition read via peer. ft={ct=None} (V3 header). + rc, err := seekable.OpenRangeReader(t.Context(), 0, int64(len(prePeerBytes)), + storage.NewFrameTable(storage.CompressionNone, nil)) + require.NoError(t, err) + got, err := io.ReadAll(rc) + require.NoError(t, err) + require.NoError(t, rc.Close()) + assert.Equal(t, prePeerBytes, got) + require.True(t, uploaded.Load(), "uploaded flag should be set after peer EOF with UseStorage") + + // 2. First post-transition call: retriable error, no peer/base contact. + _, err = seekable.OpenRangeReader(t.Context(), 0, 1, + storage.NewFrameTable(storage.CompressionNone, nil)) + var transErr *storage.PeerTransitionedError + require.ErrorAs(t, err, &transErr) + + // 3. Caller reloads V4 header and retries with ct=Zstd. This must hit the + // compressed path on base. + rc, err = seekable.OpenRangeReader(t.Context(), 0, int64(len(postBaseBytes)), + storage.NewFrameTable(storage.CompressionZstd, nil)) + require.NoError(t, err) + got, err = io.ReadAll(rc) + require.NoError(t, err) + require.NoError(t, rc.Close()) + assert.Equal(t, postBaseBytes, got) +} diff --git a/packages/orchestrator/pkg/sandbox/template/peerclient/storage.go b/packages/orchestrator/pkg/sandbox/template/peerclient/storage.go index ec9fc46945..9e7b7b6883 100644 --- a/packages/orchestrator/pkg/sandbox/template/peerclient/storage.go +++ b/packages/orchestrator/pkg/sandbox/template/peerclient/storage.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "sync" "sync/atomic" "time" @@ -128,31 +127,43 @@ func newPeerStorageProvider( } func (p *peerStorageProvider) OpenBlob(_ context.Context, path string, objType storage.ObjectType) (storage.Blob, error) { - buildID, fileName := storage.SplitPath(path) - - return &peerBlob{peerHandle: peerHandle[storage.Blob]{ - client: p.peerClient, - buildID: buildID, - fileName: fileName, - uploaded: p.uploaded, - openFn: func(ctx context.Context) (storage.Blob, error) { + buildID, t := storage.SplitPath(path) + + return &peerBlob{ + peerHandle: peerHandle{ + client: p.peerClient, + buildID: buildID, + name: t, + uploaded: p.uploaded, + }, + openBase: func(ctx context.Context) (storage.Blob, error) { return p.base.OpenBlob(ctx, path, objType) }, - }}, nil + }, nil } func (p *peerStorageProvider) OpenSeekable(_ context.Context, path string, objType storage.SeekableObjectType) (storage.Seekable, error) { - buildID, fileName := storage.SplitPath(path) - - return &peerSeekable{peerHandle: peerHandle[storage.Seekable]{ - client: p.peerClient, - buildID: buildID, - fileName: fileName, - uploaded: p.uploaded, - openFn: func(ctx context.Context) (storage.Seekable, error) { - return p.base.OpenSeekable(ctx, path, objType) + // Strip any compression suffix so peerSeekable holds the basic name. The + // base fallthrough path composes the actual storage path from + // (buildID, name, ct) per-call. Peer routing usually engages only + // pre-finalization (basic name in, no-op strip), but the Redis peer-key + // TTL outlives the upload by ~2 min: a fresh orchestrator can resolve a + // stale entry for a finalized V4/Zstd build, in which case StorageDiff + // hands us "buildID/memfile.zstd" — without stripping, getBase would + // double-suffix to "memfile.zstd.zstd" on fallthrough. + buildID, t := storage.SplitPath(path) + t = storage.StripCompression(t) + + return &peerSeekable{ + peerHandle: peerHandle{ + client: p.peerClient, + buildID: buildID, + name: t, + uploaded: p.uploaded, }, - }}, nil + basePersistence: p.base, + objType: objType, + }, nil } func (p *peerStorageProvider) DeleteObjectsWithPrefix(ctx context.Context, prefix string) error { @@ -182,40 +193,16 @@ func checkPeerAvailability(avail *orchestrator.PeerAvailability, uploaded *atomi return true } -type peerHandle[Base any] struct { +// peerHandle holds the peer-side identity shared by peerBlob and peerSeekable. +// fileName is the basic (uncompressed) name — peer fetches always use it. +type peerHandle struct { client orchestrator.ChunkServiceClient buildID string - fileName string + name string uploaded *atomic.Bool - - mu sync.Mutex - base Base - loaded bool - openFn func(ctx context.Context) (Base, error) } -func (h *peerHandle[Base]) getOrOpenBase(ctx context.Context) (Base, error) { - h.mu.Lock() - defer h.mu.Unlock() - - if h.loaded { - return h.base, nil - } - - b, err := h.openFn(ctx) - if err != nil { - var zero Base - - return zero, err - } - - h.base = b - h.loaded = true - - return b, nil -} - -// peerAttempt is the result of a peer read attempt, used with withPeerFallback. +// peerAttempt is the result of a peer read attempt. // hit=true means the peer had data (value is populated); when hit=true and the // caller also returns a non-nil error the helper records a partial failure. type peerAttempt[T any] struct { @@ -224,61 +211,53 @@ type peerAttempt[T any] struct { hit bool } -func withPeerFallback[Base, T any]( +// tryPeer attempts a peer read if the peer is still authoritative for this +// build. It records peer telemetry and returns the attempt; the caller +// inspects res.hit to decide whether to fall through to base. tryPeer never +// opens base. +func tryPeer[T any]( ctx context.Context, - h *peerHandle[Base], + h *peerHandle, spanName string, opAttr attribute.KeyValue, peerFn func(ctx context.Context) (peerAttempt[T], error), - useBase func(ctx context.Context, base Base) (T, error), -) (T, error) { +) (peerAttempt[T], error) { ctx, span := tracer.Start(ctx, spanName, trace.WithAttributes( - attribute.String("file_name", h.fileName), + attribute.String("file_name", h.name), )) defer span.End() - if !h.uploaded.Load() { - timer := peerReadTimerFactory.Begin(opAttr) - - res, err := peerFn(ctx) - if res.hit { - if err != nil { - span.RecordError(err) - timer.Failure(ctx, res.bytes) + if h.uploaded.Load() { + span.SetAttributes(attrPeerHitFalse) - return res.value, err - } - - span.SetAttributes(attrPeerHitTrue) - timer.Success(ctx, res.bytes) + return peerAttempt[T]{}, nil + } - return res.value, nil - } + timer := peerReadTimerFactory.Begin(opAttr) + res, err := peerFn(ctx) + if res.hit { if err != nil { span.RecordError(err) - } + timer.Failure(ctx, res.bytes) - timer.Failure(ctx, 0) - } - - span.SetAttributes(attrPeerHitFalse) - - base, err := h.getOrOpenBase(ctx) - if err != nil { - span.RecordError(err) + return res, err + } - var zero T + span.SetAttributes(attrPeerHitTrue) + timer.Success(ctx, res.bytes) - return zero, err + return res, nil } - result, err := useBase(ctx, base) if err != nil { span.RecordError(err) } - return result, err + timer.Failure(ctx, 0) + span.SetAttributes(attrPeerHitFalse) + + return peerAttempt[T]{}, nil } var _ io.ReadCloser = (*peerStreamReader)(nil) diff --git a/packages/orchestrator/pkg/sandbox/template/peerclient/storage_test.go b/packages/orchestrator/pkg/sandbox/template/peerclient/storage_test.go index 7e9e895c3f..ac34565de0 100644 --- a/packages/orchestrator/pkg/sandbox/template/peerclient/storage_test.go +++ b/packages/orchestrator/pkg/sandbox/template/peerclient/storage_test.go @@ -24,7 +24,7 @@ func TestPeerStorageProvider_OpenBlob_ExtractsFileName(t *testing.T) { client := orchestratormocks.NewMockChunkServiceClient(t) client.EXPECT().GetBuildBlob(mock.Anything, mock.MatchedBy(func(req *orchestrator.GetBuildBlobRequest) bool { - return req.GetBuildId() == "build-1" && req.GetFileName() == "snapfile" + return req.GetBuildId() == "build-1" && req.GetName() == "snapfile" })).Return(stream, nil) base := storage.NewMockStorageProvider(t) @@ -44,7 +44,7 @@ func TestPeerStorageProvider_OpenSeekable_ExtractsFileName(t *testing.T) { client := orchestratormocks.NewMockChunkServiceClient(t) client.EXPECT().GetBuildFileSize(mock.Anything, mock.MatchedBy(func(req *orchestrator.GetBuildFileSizeRequest) bool { - return req.GetBuildId() == "build-1" && req.GetFileName() == "memfile" + return req.GetBuildId() == "build-1" && req.GetName() == "memfile" })).Return(&orchestrator.GetBuildFileSizeResponse{TotalSize: 512}, nil) base := storage.NewMockStorageProvider(t) diff --git a/packages/orchestrator/pkg/server/chunks.go b/packages/orchestrator/pkg/server/chunks.go index 91488dbebc..8ce89fb9aa 100644 --- a/packages/orchestrator/pkg/server/chunks.go +++ b/packages/orchestrator/pkg/server/chunks.go @@ -48,7 +48,7 @@ func toGRPCError(err error) error { } func (s *Server) GetBuildFileSize(ctx context.Context, req *orchestrator.GetBuildFileSizeRequest) (*orchestrator.GetBuildFileSizeResponse, error) { - telemetry.SetAttributes(ctx, telemetry.WithBuildID(req.GetBuildId()), attribute.String("file_name", req.GetFileName())) + telemetry.SetAttributes(ctx, telemetry.WithBuildID(req.GetBuildId()), attribute.String("file_name", req.GetName())) if s.uploadedBuilds.Get(req.GetBuildId()) != nil { telemetry.SetAttributes(ctx, attribute.Bool("uploaded", true)) @@ -56,7 +56,7 @@ func (s *Server) GetBuildFileSize(ctx context.Context, req *orchestrator.GetBuil return &orchestrator.GetBuildFileSizeResponse{Availability: peerUseStorage}, nil } - src, err := peerserver.ResolveSeekable(s.templateCache, req.GetBuildId(), req.GetFileName()) + src, err := peerserver.ResolveSeekable(s.templateCache, req.GetBuildId(), req.GetName()) if err != nil { if errors.Is(err, peerserver.ErrNotAvailable) { return &orchestrator.GetBuildFileSizeResponse{Availability: peerNotAvailable}, nil @@ -65,7 +65,7 @@ func (s *Server) GetBuildFileSize(ctx context.Context, req *orchestrator.GetBuil return nil, toGRPCError(err) } - telemetry.ReportEvent(ctx, "getting file size", telemetry.WithBuildID(req.GetBuildId()), attribute.String("file_name", req.GetFileName())) + telemetry.ReportEvent(ctx, "getting file size", telemetry.WithBuildID(req.GetBuildId()), attribute.String("file_name", req.GetName())) size, err := src.Size(ctx) if err != nil { @@ -76,7 +76,7 @@ func (s *Server) GetBuildFileSize(ctx context.Context, req *orchestrator.GetBuil } func (s *Server) GetBuildFileExists(ctx context.Context, req *orchestrator.GetBuildFileExistsRequest) (*orchestrator.GetBuildFileExistsResponse, error) { - telemetry.SetAttributes(ctx, telemetry.WithBuildID(req.GetBuildId()), attribute.String("file_name", req.GetFileName())) + telemetry.SetAttributes(ctx, telemetry.WithBuildID(req.GetBuildId()), attribute.String("file_name", req.GetName())) if s.uploadedBuilds.Get(req.GetBuildId()) != nil { telemetry.SetAttributes(ctx, attribute.Bool("uploaded", true)) @@ -84,7 +84,7 @@ func (s *Server) GetBuildFileExists(ctx context.Context, req *orchestrator.GetBu return &orchestrator.GetBuildFileExistsResponse{Availability: peerUseStorage}, nil } - src, err := peerserver.ResolveBlob(s.templateCache, req.GetBuildId(), req.GetFileName()) + src, err := peerserver.ResolveBlob(s.templateCache, req.GetBuildId(), req.GetName()) if err != nil { if errors.Is(err, peerserver.ErrNotAvailable) { return &orchestrator.GetBuildFileExistsResponse{Availability: peerNotAvailable}, nil @@ -117,7 +117,7 @@ func (s *Server) ReadAtBuildSeekable(req *orchestrator.ReadAtBuildSeekableReques telemetry.SetAttributes(ctx, telemetry.WithBuildID(req.GetBuildId()), - attribute.String("file_name", req.GetFileName()), + attribute.String("file_name", req.GetName()), attribute.Int64("offset", offset), attribute.Int64("length", length), ) @@ -128,7 +128,7 @@ func (s *Server) ReadAtBuildSeekable(req *orchestrator.ReadAtBuildSeekableReques return stream.Send(&orchestrator.ReadAtBuildSeekableResponse{Availability: peerUseStorage}) } - src, err := peerserver.ResolveSeekable(s.templateCache, req.GetBuildId(), req.GetFileName()) + src, err := peerserver.ResolveSeekable(s.templateCache, req.GetBuildId(), req.GetName()) if err != nil { if errors.Is(err, peerserver.ErrNotAvailable) { return stream.Send(&orchestrator.ReadAtBuildSeekableResponse{Availability: peerNotAvailable}) @@ -154,7 +154,7 @@ func (s *Server) GetBuildBlob(req *orchestrator.GetBuildBlobRequest, stream orch telemetry.SetAttributes(ctx, telemetry.WithBuildID(req.GetBuildId()), - attribute.String("file_name", req.GetFileName()), + attribute.String("file_name", req.GetName()), ) if s.uploadedBuilds.Get(req.GetBuildId()) != nil { @@ -163,7 +163,7 @@ func (s *Server) GetBuildBlob(req *orchestrator.GetBuildBlobRequest, stream orch return stream.Send(&orchestrator.GetBuildBlobResponse{Availability: peerUseStorage}) } - src, err := peerserver.ResolveBlob(s.templateCache, req.GetBuildId(), req.GetFileName()) + src, err := peerserver.ResolveBlob(s.templateCache, req.GetBuildId(), req.GetName()) if err != nil { if errors.Is(err, peerserver.ErrNotAvailable) { return stream.Send(&orchestrator.GetBuildBlobResponse{Availability: peerNotAvailable}) diff --git a/packages/orchestrator/pkg/server/chunks_test.go b/packages/orchestrator/pkg/server/chunks_test.go index dbc36643a3..d950a68065 100644 --- a/packages/orchestrator/pkg/server/chunks_test.go +++ b/packages/orchestrator/pkg/server/chunks_test.go @@ -19,10 +19,10 @@ func TestReadAtBuildSeekable_RejectsNegativeRange(t *testing.T) { s := &Server{} err := s.ReadAtBuildSeekable(&orchestrator.ReadAtBuildSeekableRequest{ - BuildId: "build-1", - FileName: "memfile", - Offset: -1, - Length: 1, + BuildId: "build-1", + Name: "memfile", + Offset: -1, + Length: 1, }, stream) require.Error(t, err) st, ok := status.FromError(err) diff --git a/packages/shared/pkg/grpc/orchestrator/chunks.pb.go b/packages/shared/pkg/grpc/orchestrator/chunks.pb.go index 70aee86388..94372f198f 100644 --- a/packages/shared/pkg/grpc/orchestrator/chunks.pb.go +++ b/packages/shared/pkg/grpc/orchestrator/chunks.pb.go @@ -88,8 +88,8 @@ type GetBuildFileSizeRequest struct { unknownFields protoimpl.UnknownFields BuildId string `protobuf:"bytes,1,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` - // file_name is one of the seekable diff files: "memfile", "rootfs.ext4" - FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + // name is one of the seekable diff files: "memfile", "rootfs.ext4" + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` } func (x *GetBuildFileSizeRequest) Reset() { @@ -131,9 +131,9 @@ func (x *GetBuildFileSizeRequest) GetBuildId() string { return "" } -func (x *GetBuildFileSizeRequest) GetFileName() string { +func (x *GetBuildFileSizeRequest) GetName() string { if x != nil { - return x.FileName + return x.Name } return "" } @@ -199,8 +199,8 @@ type GetBuildFileExistsRequest struct { unknownFields protoimpl.UnknownFields BuildId string `protobuf:"bytes,1,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` - // file_name is one of: "snapfile", "metadata.json" - FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + // name is one of: "snapfile", "metadata.json" + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` } func (x *GetBuildFileExistsRequest) Reset() { @@ -242,9 +242,9 @@ func (x *GetBuildFileExistsRequest) GetBuildId() string { return "" } -func (x *GetBuildFileExistsRequest) GetFileName() string { +func (x *GetBuildFileExistsRequest) GetName() string { if x != nil { - return x.FileName + return x.Name } return "" } @@ -301,10 +301,10 @@ type ReadAtBuildSeekableRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - BuildId string `protobuf:"bytes,1,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` - FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` - Offset int64 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` - Length int64 `protobuf:"varint,4,opt,name=length,proto3" json:"length,omitempty"` + BuildId string `protobuf:"bytes,1,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Offset int64 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + Length int64 `protobuf:"varint,4,opt,name=length,proto3" json:"length,omitempty"` } func (x *ReadAtBuildSeekableRequest) Reset() { @@ -346,9 +346,9 @@ func (x *ReadAtBuildSeekableRequest) GetBuildId() string { return "" } -func (x *ReadAtBuildSeekableRequest) GetFileName() string { +func (x *ReadAtBuildSeekableRequest) GetName() string { if x != nil { - return x.FileName + return x.Name } return "" } @@ -429,8 +429,8 @@ type GetBuildBlobRequest struct { unknownFields protoimpl.UnknownFields BuildId string `protobuf:"bytes,1,opt,name=build_id,json=buildId,proto3" json:"build_id,omitempty"` - // file_name is one of: "snapfile", "metadata.json", "memfile.header", "rootfs.ext4.header" - FileName string `protobuf:"bytes,2,opt,name=file_name,json=fileName,proto3" json:"file_name,omitempty"` + // name is one of: "snapfile", "metadata.json", "memfile.header", "rootfs.ext4.header" + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` } func (x *GetBuildBlobRequest) Reset() { @@ -472,9 +472,9 @@ func (x *GetBuildBlobRequest) GetBuildId() string { return "" } -func (x *GetBuildBlobRequest) GetFileName() string { +func (x *GetBuildBlobRequest) GetName() string { if x != nil { - return x.FileName + return x.Name } return "" } @@ -544,79 +544,77 @@ var file_chunks_proto_rawDesc = []byte{ 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6e, 0x6f, 0x74, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x73, - 0x65, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x22, 0x51, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x42, + 0x65, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x22, 0x48, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x1b, - 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x47, - 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x35, 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, - 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x53, 0x0a, - 0x19, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x45, 0x78, 0x69, - 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, - 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x35, 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x76, 0x61, - 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, - 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x84, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x61, 0x64, - 0x41, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, - 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, - 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x68, - 0x0a, 0x1b, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, - 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x12, 0x35, 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x76, - 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x61, 0x76, 0x61, 0x69, - 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x4d, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, - 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, - 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x61, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, - 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, 0x65, 0x72, - 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x61, 0x76, - 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x32, 0xb9, 0x02, 0x0a, 0x0c, 0x43, - 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, - 0x18, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, - 0x7a, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x47, 0x65, 0x74, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x46, 0x69, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x47, 0x65, 0x74, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, - 0x64, 0x46, 0x69, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1b, 0x2e, 0x52, 0x65, 0x61, - 0x64, 0x41, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x14, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, - 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, - 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x32, 0x62, - 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, 0x72, 0x63, 0x68, 0x65, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, + 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x35, 0x0a, + 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x79, 0x22, 0x4a, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x46, 0x69, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x22, 0x53, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, + 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, + 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x7b, 0x0a, 0x1a, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x22, 0x68, 0x0a, 0x1b, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, + 0x65, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, + 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x44, 0x0a, 0x13, + 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x22, 0x61, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x6c, + 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, + 0x0a, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x69, 0x6c, 0x69, 0x74, 0x79, 0x32, 0xb9, 0x02, 0x0a, 0x0c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x18, 0x2e, 0x47, 0x65, 0x74, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, + 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4d, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x45, + 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x46, 0x69, 0x6c, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x46, 0x69, 0x6c, 0x65, + 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, + 0x0a, 0x13, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, + 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1b, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x41, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x53, 0x65, 0x65, 0x6b, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x6c, + 0x6f, 0x62, 0x12, 0x14, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, + 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, + 0x01, 0x42, 0x2f, 0x5a, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x32, 0x62, 0x2d, 0x64, 0x65, 0x76, 0x2f, + 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2f, 0x6f, 0x72, 0x63, 0x68, 0x65, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var (