tanmayrauth commented on code in PR #1477:
URL: https://github.com/apache/iceberg-go/pull/1477#discussion_r3597488881


##########
catalog/rest/scan_planning.go:
##########
@@ -187,9 +189,180 @@ func (r *Catalog) SupportsRemoteScanPlanning() bool {
 }
 
 // PlanFiles plans a scan server-side and returns tasks (and, optionally, a
-// plan-scoped FileIO) for the table to read.
+// plan-scoped FileIO) for the table to read. It submits the plan, polls a
+// submitted plan to completion, and expands any plan-task handles into their
+// tasks before returning.
+//
+// Decoding the returned tasks into table.FileScanTask is the one piece still
+// stubbed: RESTFileScanTask/RESTDeleteFile are empty pending the scan-task
+// decoder phase, so a plan that yields any task surfaces ErrNotImplemented 
(see
+// remoteScanTasks). SupportsRemoteScanPlanning therefore stays false until 
that
+// lands, so auto-mode scans keep planning locally rather than routing here.
 func (r *Catalog) PlanFiles(ctx context.Context, req 
table.ScanPlanningRequest) (table.ScanPlanningResult, error) {
-       return table.ScanPlanningResult{}, fmt.Errorf("%w: REST scan planning", 
iceberg.ErrNotImplemented)
+       wire, err := planTableScanRequestFrom(req)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       resp, err := r.PlanTableScan(ctx, req.Identifier, wire)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       // Resolve to a completed plan: use an inline result as-is, or poll a
+       // submitted one to completion. PlanTableScan maps failed to an error 
and
+       // rejects other statuses, so only completed/submitted reach here.
+       var completed CompletedPlanningResult
+       switch resp.Status {
+       case PlanStatusCompleted:
+               completed = CompletedPlanningResult{
+                       ScanTasks:          resp.ScanTasks,
+                       StorageCredentials: resp.StorageCredentials,
+               }
+       case PlanStatusSubmitted:
+               completed, err = r.WaitForPlan(ctx, req.Identifier, 
*resp.PlanID, WaitForPlanOptions{})
+               if err != nil {
+                       return table.ScanPlanningResult{}, err
+               }
+       default:
+               return table.ScanPlanningResult{}, fmt.Errorf(
+                       "%w: unexpected plan status %q from planTableScan", 
ErrRESTError, resp.Status)
+       }
+
+       files, deletes, err := r.collectScanTasks(ctx, req.Identifier, 
completed.ScanTasks)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       tasks, err := remoteScanTasks(files, deletes)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       return table.ScanPlanningResult{
+               Tasks: tasks,
+               IO:    planIOFromCredentials(completed.StorageCredentials, 
req.MetadataLocation),

Review Comment:
     This resolves the plan's credential array against the metadata location 
and builds a single FileIO for the scan. That matches how the rest of 
iceberg-go handles vended creds (loadTable resolves against MetadataLoc too), 
so it's not a regression — but plan.storage-credentials is an array of 
prefix-scoped creds, and resolving once against the metadata location means a 
plan whose data/delete files sit under a different prefix picks the wrong or no 
credential once the decoder routes reads through this IO. Fine for a 
single-bucket table where data lives under the table root; worth settling the 
PlanIO shape before the
     decoder makes it load-bearing. 



##########
catalog/rest/scan_planning.go:
##########
@@ -187,9 +189,180 @@ func (r *Catalog) SupportsRemoteScanPlanning() bool {
 }
 
 // PlanFiles plans a scan server-side and returns tasks (and, optionally, a
-// plan-scoped FileIO) for the table to read.
+// plan-scoped FileIO) for the table to read. It submits the plan, polls a
+// submitted plan to completion, and expands any plan-task handles into their
+// tasks before returning.
+//
+// Decoding the returned tasks into table.FileScanTask is the one piece still
+// stubbed: RESTFileScanTask/RESTDeleteFile are empty pending the scan-task
+// decoder phase, so a plan that yields any task surfaces ErrNotImplemented 
(see
+// remoteScanTasks). SupportsRemoteScanPlanning therefore stays false until 
that
+// lands, so auto-mode scans keep planning locally rather than routing here.
 func (r *Catalog) PlanFiles(ctx context.Context, req 
table.ScanPlanningRequest) (table.ScanPlanningResult, error) {
-       return table.ScanPlanningResult{}, fmt.Errorf("%w: REST scan planning", 
iceberg.ErrNotImplemented)
+       wire, err := planTableScanRequestFrom(req)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       resp, err := r.PlanTableScan(ctx, req.Identifier, wire)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       // Resolve to a completed plan: use an inline result as-is, or poll a
+       // submitted one to completion. PlanTableScan maps failed to an error 
and
+       // rejects other statuses, so only completed/submitted reach here.
+       var completed CompletedPlanningResult
+       switch resp.Status {
+       case PlanStatusCompleted:
+               completed = CompletedPlanningResult{
+                       ScanTasks:          resp.ScanTasks,
+                       StorageCredentials: resp.StorageCredentials,
+               }
+       case PlanStatusSubmitted:
+               completed, err = r.WaitForPlan(ctx, req.Identifier, 
*resp.PlanID, WaitForPlanOptions{})
+               if err != nil {
+                       return table.ScanPlanningResult{}, err
+               }
+       default:
+               return table.ScanPlanningResult{}, fmt.Errorf(
+                       "%w: unexpected plan status %q from planTableScan", 
ErrRESTError, resp.Status)
+       }
+
+       files, deletes, err := r.collectScanTasks(ctx, req.Identifier, 
completed.ScanTasks)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       tasks, err := remoteScanTasks(files, deletes)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       return table.ScanPlanningResult{
+               Tasks: tasks,
+               IO:    planIOFromCredentials(completed.StorageCredentials, 
req.MetadataLocation),
+       }, nil
+}
+
+// collectScanTasks expands plan-task handles into their tasks, walking the
+// fanout: a fetchScanTasks response can itself return more plan-tasks. It
+// accumulates the file-scan-tasks and delete-files reachable from the initial
+// set. A handle is fetched at most once; a server that re-issues one would
+// otherwise loop forever.
+func (r *Catalog) collectScanTasks(ctx context.Context, ident 
table.Identifier, tasks ScanTasks) ([]RESTFileScanTask, []RESTDeleteFile, 
error) {
+       files := append([]RESTFileScanTask(nil), tasks.FileScanTasks...)
+       deletes := append([]RESTDeleteFile(nil), tasks.DeleteFiles...)
+
+       queue := append([]string(nil), tasks.PlanTasks...)
+       seen := make(map[string]bool, len(queue))
+       for len(queue) > 0 {
+               handle := queue[0]
+               queue = queue[1:]
+               if seen[handle] {
+                       continue
+               }
+               seen[handle] = true
+
+               resp, err := r.FetchScanTasks(ctx, ident, 
FetchScanTasksRequest{PlanTask: handle})
+               if err != nil {
+                       return nil, nil, err
+               }
+               files = append(files, resp.FileScanTasks...)
+               deletes = append(deletes, resp.DeleteFiles...)
+               queue = append(queue, resp.PlanTasks...)
+       }
+
+       return files, deletes, nil
+}
+
+// remoteScanTasks decodes the server's task payload into domain FileScanTasks.
+// Blocked for now: RESTFileScanTask/RESTDeleteFile are empty pending the
+// scan-task decoder phase, so an empty plan decodes to no tasks while any 
actual
+// task surfaces ErrNotImplemented. The decoder phase replaces this body.
+func remoteScanTasks(files []RESTFileScanTask, deletes []RESTDeleteFile) 
([]table.FileScanTask, error) {
+       if len(files) == 0 && len(deletes) == 0 {
+               return nil, nil
+       }
+
+       return nil, fmt.Errorf("%w: decoding remote scan tasks", 
iceberg.ErrNotImplemented)
+}
+
+// planIOFromCredentials wraps a plan's vended storage credentials in a lazy
+// PlanIO. With none vended it returns a nil PlanIO, and the scan falls back to
+// the table's own FileIO.
+func planIOFromCredentials(creds []StorageCredential, location string) 
table.PlanIO {
+       if len(creds) == 0 {
+               return nil
+       }
+
+       return &planScopedIO{creds: creds, location: location}
+}
+
+// planScopedIO builds a FileIO from a plan's vended storage credentials on 
first
+// use and caches it. It satisfies table.PlanIO. Per the delivery contract a 
Scan
+// carrying plan-scoped IO is not read concurrently, but the mutex keeps the 
lazy
+// build and Close honest.
+type planScopedIO struct {
+       creds    []StorageCredential
+       location string
+
+       mu     sync.Mutex
+       cached iceio.IO
+}
+
+func (p *planScopedIO) Load(ctx context.Context) (iceio.IO, error) {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+
+       if p.cached != nil {
+               return p.cached, nil
+       }
+
+       fs, err := iceio.LoadFS(ctx, resolveStorageCredentials(p.creds, 
p.location), p.location)
+       if err != nil {
+               return nil, err
+       }
+       p.cached = fs
+
+       return p.cached, nil
+}
+
+func (p *planScopedIO) Close() error {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+
+       closer, ok := p.cached.(interface{ Close() error })
+       p.cached = nil
+       if ok {
+               return closer.Close()
+       }
+
+       return nil
+}
+
+// planTableScanRequestFrom builds the planTableScan request body from the
+// planner-level request, serializing the row filter to ExpressionParser JSON. 
A
+// nil or always-true filter is left off so the server plans without one.
+func planTableScanRequestFrom(req table.ScanPlanningRequest) 
(PlanTableScanRequest, error) {
+       out := PlanTableScanRequest{

Review Comment:
   Neither the initial plan request here nor the poll at line 223 
(WaitForPlanOptions{}) sets AccessDelegation, so X-Iceberg-Access-Delegation is 
never sent. A spec-compliant server vends storage credentials only when asked, 
so completed.StorageCredentials comes back empty, planIOFromCredentials returns 
nil, and the scan silently falls back to the table's ambient FileIO — for a 
table with no ambient creds that's a read failure at scan time. 
TestPlanFilesSurfacesVendedCredentials passes only because the mock vends 
unconditionally, and ScanPlanningRequest has no field to carry delegation, so a 
caller can't thread it either.  This is the fix that has to land first — the 
planScopedIO path below is unreachable against a real server until it does.



##########
catalog/rest/scan_planning.go:
##########
@@ -187,9 +189,180 @@ func (r *Catalog) SupportsRemoteScanPlanning() bool {
 }
 
 // PlanFiles plans a scan server-side and returns tasks (and, optionally, a
-// plan-scoped FileIO) for the table to read.
+// plan-scoped FileIO) for the table to read. It submits the plan, polls a
+// submitted plan to completion, and expands any plan-task handles into their
+// tasks before returning.
+//
+// Decoding the returned tasks into table.FileScanTask is the one piece still
+// stubbed: RESTFileScanTask/RESTDeleteFile are empty pending the scan-task
+// decoder phase, so a plan that yields any task surfaces ErrNotImplemented 
(see
+// remoteScanTasks). SupportsRemoteScanPlanning therefore stays false until 
that
+// lands, so auto-mode scans keep planning locally rather than routing here.
 func (r *Catalog) PlanFiles(ctx context.Context, req 
table.ScanPlanningRequest) (table.ScanPlanningResult, error) {
-       return table.ScanPlanningResult{}, fmt.Errorf("%w: REST scan planning", 
iceberg.ErrNotImplemented)
+       wire, err := planTableScanRequestFrom(req)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       resp, err := r.PlanTableScan(ctx, req.Identifier, wire)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       // Resolve to a completed plan: use an inline result as-is, or poll a
+       // submitted one to completion. PlanTableScan maps failed to an error 
and
+       // rejects other statuses, so only completed/submitted reach here.
+       var completed CompletedPlanningResult
+       switch resp.Status {
+       case PlanStatusCompleted:
+               completed = CompletedPlanningResult{
+                       ScanTasks:          resp.ScanTasks,
+                       StorageCredentials: resp.StorageCredentials,
+               }
+       case PlanStatusSubmitted:
+               completed, err = r.WaitForPlan(ctx, req.Identifier, 
*resp.PlanID, WaitForPlanOptions{})
+               if err != nil {
+                       return table.ScanPlanningResult{}, err
+               }
+       default:
+               return table.ScanPlanningResult{}, fmt.Errorf(
+                       "%w: unexpected plan status %q from planTableScan", 
ErrRESTError, resp.Status)
+       }
+
+       files, deletes, err := r.collectScanTasks(ctx, req.Identifier, 
completed.ScanTasks)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       tasks, err := remoteScanTasks(files, deletes)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       return table.ScanPlanningResult{
+               Tasks: tasks,
+               IO:    planIOFromCredentials(completed.StorageCredentials, 
req.MetadataLocation),
+       }, nil
+}
+
+// collectScanTasks expands plan-task handles into their tasks, walking the
+// fanout: a fetchScanTasks response can itself return more plan-tasks. It
+// accumulates the file-scan-tasks and delete-files reachable from the initial
+// set. A handle is fetched at most once; a server that re-issues one would
+// otherwise loop forever.
+func (r *Catalog) collectScanTasks(ctx context.Context, ident 
table.Identifier, tasks ScanTasks) ([]RESTFileScanTask, []RESTDeleteFile, 
error) {
+       files := append([]RESTFileScanTask(nil), tasks.FileScanTasks...)
+       deletes := append([]RESTDeleteFile(nil), tasks.DeleteFiles...)
+
+       queue := append([]string(nil), tasks.PlanTasks...)
+       seen := make(map[string]bool, len(queue))
+       for len(queue) > 0 {
+               handle := queue[0]
+               queue = queue[1:]
+               if seen[handle] {
+                       continue
+               }
+               seen[handle] = true
+
+               resp, err := r.FetchScanTasks(ctx, ident, 
FetchScanTasksRequest{PlanTask: handle})
+               if err != nil {
+                       return nil, nil, err
+               }
+               files = append(files, resp.FileScanTasks...)
+               deletes = append(deletes, resp.DeleteFiles...)
+               queue = append(queue, resp.PlanTasks...)
+       }
+
+       return files, deletes, nil
+}
+
+// remoteScanTasks decodes the server's task payload into domain FileScanTasks.
+// Blocked for now: RESTFileScanTask/RESTDeleteFile are empty pending the
+// scan-task decoder phase, so an empty plan decodes to no tasks while any 
actual
+// task surfaces ErrNotImplemented. The decoder phase replaces this body.
+func remoteScanTasks(files []RESTFileScanTask, deletes []RESTDeleteFile) 
([]table.FileScanTask, error) {
+       if len(files) == 0 && len(deletes) == 0 {
+               return nil, nil
+       }
+
+       return nil, fmt.Errorf("%w: decoding remote scan tasks", 
iceberg.ErrNotImplemented)
+}
+
+// planIOFromCredentials wraps a plan's vended storage credentials in a lazy
+// PlanIO. With none vended it returns a nil PlanIO, and the scan falls back to
+// the table's own FileIO.
+func planIOFromCredentials(creds []StorageCredential, location string) 
table.PlanIO {
+       if len(creds) == 0 {
+               return nil
+       }
+
+       return &planScopedIO{creds: creds, location: location}
+}
+
+// planScopedIO builds a FileIO from a plan's vended storage credentials on 
first
+// use and caches it. It satisfies table.PlanIO. Per the delivery contract a 
Scan
+// carrying plan-scoped IO is not read concurrently, but the mutex keeps the 
lazy
+// build and Close honest.
+type planScopedIO struct {
+       creds    []StorageCredential
+       location string
+
+       mu     sync.Mutex
+       cached iceio.IO
+}
+
+func (p *planScopedIO) Load(ctx context.Context) (iceio.IO, error) {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+
+       if p.cached != nil {
+               return p.cached, nil
+       }
+
+       fs, err := iceio.LoadFS(ctx, resolveStorageCredentials(p.creds, 
p.location), p.location)
+       if err != nil {
+               return nil, err
+       }
+       p.cached = fs
+
+       return p.cached, nil
+}
+
+func (p *planScopedIO) Close() error {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+
+       closer, ok := p.cached.(interface{ Close() error })
+       p.cached = nil
+       if ok {
+               return closer.Close()
+       }
+
+       return nil
+}
+
+// planTableScanRequestFrom builds the planTableScan request body from the
+// planner-level request, serializing the row filter to ExpressionParser JSON. 
A
+// nil or always-true filter is left off so the server plans without one.
+func planTableScanRequestFrom(req table.ScanPlanningRequest) 
(PlanTableScanRequest, error) {
+       out := PlanTableScanRequest{
+               SnapshotID:        req.SnapshotID,
+               Select:            req.SelectedFields,
+               MinRowsRequested:  req.MinRowsRequested,
+               CaseSensitive:     req.CaseSensitive,
+               UseSnapshotSchema: req.UseSnapshotSchema,
+               StatsFields:       req.StatsFields,
+       }
+
+       if req.RowFilter != nil && !req.RowFilter.Equals(iceberg.AlwaysTrue{}) {
+               filter, err := json.Marshal(req.RowFilter)

Review Comment:
   Not a bug today — all BooleanExpression types have MarshalJSON and 
predicateType errors on unknown ops. But moving from a dedicated 
MarshalExpressionJSON entry point to bare json.Marshal drops the one place that 
guaranteed an expression is REST-serializable: marshalChildren recurses through 
json.Marshal on the interface, so a future expression type added without a 
MarshalJSON won't fail to compile and won't error — it silently emits 
struct-default JSON and the server gets a malformed filter. Marshaling through 
a helper that asserts the concrete type is known would keep that a loud error 
rather than a silent wrong
     filter.



##########
catalog/rest/scan_planning.go:
##########
@@ -187,9 +189,180 @@ func (r *Catalog) SupportsRemoteScanPlanning() bool {
 }
 
 // PlanFiles plans a scan server-side and returns tasks (and, optionally, a
-// plan-scoped FileIO) for the table to read.
+// plan-scoped FileIO) for the table to read. It submits the plan, polls a
+// submitted plan to completion, and expands any plan-task handles into their
+// tasks before returning.
+//
+// Decoding the returned tasks into table.FileScanTask is the one piece still
+// stubbed: RESTFileScanTask/RESTDeleteFile are empty pending the scan-task
+// decoder phase, so a plan that yields any task surfaces ErrNotImplemented 
(see
+// remoteScanTasks). SupportsRemoteScanPlanning therefore stays false until 
that
+// lands, so auto-mode scans keep planning locally rather than routing here.
 func (r *Catalog) PlanFiles(ctx context.Context, req 
table.ScanPlanningRequest) (table.ScanPlanningResult, error) {
-       return table.ScanPlanningResult{}, fmt.Errorf("%w: REST scan planning", 
iceberg.ErrNotImplemented)
+       wire, err := planTableScanRequestFrom(req)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       resp, err := r.PlanTableScan(ctx, req.Identifier, wire)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       // Resolve to a completed plan: use an inline result as-is, or poll a
+       // submitted one to completion. PlanTableScan maps failed to an error 
and
+       // rejects other statuses, so only completed/submitted reach here.
+       var completed CompletedPlanningResult
+       switch resp.Status {
+       case PlanStatusCompleted:
+               completed = CompletedPlanningResult{
+                       ScanTasks:          resp.ScanTasks,
+                       StorageCredentials: resp.StorageCredentials,
+               }
+       case PlanStatusSubmitted:
+               completed, err = r.WaitForPlan(ctx, req.Identifier, 
*resp.PlanID, WaitForPlanOptions{})
+               if err != nil {
+                       return table.ScanPlanningResult{}, err
+               }
+       default:
+               return table.ScanPlanningResult{}, fmt.Errorf(
+                       "%w: unexpected plan status %q from planTableScan", 
ErrRESTError, resp.Status)
+       }
+
+       files, deletes, err := r.collectScanTasks(ctx, req.Identifier, 
completed.ScanTasks)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       tasks, err := remoteScanTasks(files, deletes)
+       if err != nil {
+               return table.ScanPlanningResult{}, err
+       }
+
+       return table.ScanPlanningResult{
+               Tasks: tasks,
+               IO:    planIOFromCredentials(completed.StorageCredentials, 
req.MetadataLocation),
+       }, nil
+}
+
+// collectScanTasks expands plan-task handles into their tasks, walking the
+// fanout: a fetchScanTasks response can itself return more plan-tasks. It
+// accumulates the file-scan-tasks and delete-files reachable from the initial
+// set. A handle is fetched at most once; a server that re-issues one would
+// otherwise loop forever.
+func (r *Catalog) collectScanTasks(ctx context.Context, ident 
table.Identifier, tasks ScanTasks) ([]RESTFileScanTask, []RESTDeleteFile, 
error) {
+       files := append([]RESTFileScanTask(nil), tasks.FileScanTasks...)
+       deletes := append([]RESTDeleteFile(nil), tasks.DeleteFiles...)
+
+       queue := append([]string(nil), tasks.PlanTasks...)
+       seen := make(map[string]bool, len(queue))
+       for len(queue) > 0 {
+               handle := queue[0]
+               queue = queue[1:]
+               if seen[handle] {
+                       continue
+               }
+               seen[handle] = true
+
+               resp, err := r.FetchScanTasks(ctx, ident, 
FetchScanTasksRequest{PlanTask: handle})
+               if err != nil {
+                       return nil, nil, err
+               }
+               files = append(files, resp.FileScanTasks...)
+               deletes = append(deletes, resp.DeleteFiles...)
+               queue = append(queue, resp.PlanTasks...)
+       }
+
+       return files, deletes, nil
+}
+
+// remoteScanTasks decodes the server's task payload into domain FileScanTasks.
+// Blocked for now: RESTFileScanTask/RESTDeleteFile are empty pending the
+// scan-task decoder phase, so an empty plan decodes to no tasks while any 
actual
+// task surfaces ErrNotImplemented. The decoder phase replaces this body.
+func remoteScanTasks(files []RESTFileScanTask, deletes []RESTDeleteFile) 
([]table.FileScanTask, error) {
+       if len(files) == 0 && len(deletes) == 0 {
+               return nil, nil
+       }
+
+       return nil, fmt.Errorf("%w: decoding remote scan tasks", 
iceberg.ErrNotImplemented)
+}
+
+// planIOFromCredentials wraps a plan's vended storage credentials in a lazy
+// PlanIO. With none vended it returns a nil PlanIO, and the scan falls back to
+// the table's own FileIO.
+func planIOFromCredentials(creds []StorageCredential, location string) 
table.PlanIO {
+       if len(creds) == 0 {
+               return nil
+       }
+
+       return &planScopedIO{creds: creds, location: location}
+}
+
+// planScopedIO builds a FileIO from a plan's vended storage credentials on 
first
+// use and caches it. It satisfies table.PlanIO. Per the delivery contract a 
Scan
+// carrying plan-scoped IO is not read concurrently, but the mutex keeps the 
lazy
+// build and Close honest.
+type planScopedIO struct {
+       creds    []StorageCredential
+       location string
+
+       mu     sync.Mutex
+       cached iceio.IO
+}
+
+func (p *planScopedIO) Load(ctx context.Context) (iceio.IO, error) {
+       p.mu.Lock()
+       defer p.mu.Unlock()
+
+       if p.cached != nil {
+               return p.cached, nil
+       }
+
+       fs, err := iceio.LoadFS(ctx, resolveStorageCredentials(p.creds, 
p.location), p.location)

Review Comment:
   This builds the plan FileIO once and caches it until Close with no expiry 
handling, whereas the normal table path (rest.go:1044) reads vended creds 
through vendedCredentialRefresher, which tracks the credential expiry keys 
parseCredentialExpiry already knows about and re-fetches on TTL. A scan that 
outlives the vended credential's TTL — the long fanout plans that make 
server-side planning worthwhile — starts getting raw storage 403s on later 
ReadTasks reads with no diagnosable "expired" error. Plan creds can't be 
refreshed via the table-cred endpoint, so this needs a deliberate call: at 
least reuse parseCredentialExpiry to surface an expiry error, and ideally don't 
stand up a second vended-cred IO path parallel to vendedCredentialRefresher.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to