laskoviymishka commented on code in PR #1430:
URL: https://github.com/apache/iceberg-go/pull/1430#discussion_r3570272993
##########
table/table.go:
##########
@@ -157,6 +168,13 @@ func (t *Table) Refresh(ctx context.Context) error {
t.fsF = fresh.fsF
t.metadataLocation = fresh.metadataLocation
t.planner = fresh.planner
+ // Only inherit the catalog-derived reporter when the caller hasn't set
one
+ // of their own. Refresh runs inside commit retry loops, so
unconditionally
+ // copying fresh.reporter would silently revert a
WithMetricsReporter-injected
+ // reporter to the catalog default mid-operation.
+ if _, isNop := t.reporter.(metrics.NopReporter); isNop {
Review Comment:
This is the one thing I'd tighten before approving. The guard uses the
concrete `NopReporter` type as a stand-in for "the caller never set a
reporter," and that conflates two different intents.
A caller who explicitly opts out with
`WithMetricsReporter(metrics.NopReporter{})` — or passes `metrics.Combine(nil,
nil)`, which resolves to `NopReporter{}` — reads as "unset" here and gets
silently reverted to the catalog default on the next `Refresh`. Since this is
the exact invariant last round was about, I'd rather make it explicit with a
was-set flag:
```go
// in WithMetricsReporter, when r != nil:
t.reporter = r
t.reporterSet = true
// in Refresh:
if !t.reporterSet {
t.reporter = fresh.reporter
}
```
That also drops the type dependency entirely. And I'd add the companion test
— `WithMetricsReporter(metrics.NopReporter{})`, refresh against a catalog that
hands back a logging reporter, assert the reporter is still `NopReporter{}`.
Right now that case would silently fail. wdyt?
##########
table/table.go:
##########
@@ -873,28 +905,48 @@ func (t Table) Scan(opts ...ScanOption) *Scan {
return s
}
+// Option configures a [Table] at construction. Options are applied in order
+// after the core fields are set.
+type Option func(*Table)
+
+// WithMetricsReporter sets the metrics reporter for the table; scans created
+// from the table inherit it. A nil reporter is ignored (the table keeps its
+// default no-op reporter).
+func WithMetricsReporter(r metrics.Reporter) Option {
+ return func(t *Table) {
Review Comment:
`WithReporter` returns the package-level `noopOption` when `r` is nil, but
this allocates a live closure that no-ops on nil. Could mirror `WithReporter`
with a shared `noopTableOption` so the two idioms match. Non-blocking.
##########
metrics/registry.go:
##########
@@ -0,0 +1,99 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package metrics
+
+import (
+ "fmt"
+ "sync"
+)
+
+// ReporterImplKey is the catalog/table property that selects a registered
+// reporter by name (e.g. "logging"). It is the Go analogue of Java's
+// metrics-reporter-impl; Go uses a name→factory registry rather than
+// reflection over a class name.
+const ReporterImplKey = "metrics-reporter-impl"
+
+// Built-in reporter names usable as the value of [ReporterImplKey].
+const (
+ ReporterNameNop = "nop"
+ ReporterNameLogging = "logging"
+)
+
+// Factory builds a Reporter from configuration properties. The same property
+// map that selected the reporter is passed in, so a factory may read its own
+// configuration keys.
+type Factory func(props map[string]string) (Reporter, error)
+
+var (
+ registryMu sync.RWMutex
+ registry = map[string]Factory{}
+)
+
+func init() {
+ Register(ReporterNameNop, func(map[string]string) (Reporter, error) {
return NopReporter{}, nil })
+ Register(ReporterNameLogging, func(map[string]string) (Reporter, error)
{
+ return NewLoggingReporter(nil), nil
+ })
+}
+
+// Register makes a reporter factory available under name. It panics if name is
+// empty or already registered, mirroring database/sql.Register — registration
+// is expected to happen once, from package init.
+func Register(name string, factory Factory) {
+ if name == "" {
+ panic("metrics: Register called with empty name")
+ }
+ if factory == nil {
+ panic("metrics: Register called with nil factory")
+ }
+ registryMu.Lock()
+ defer registryMu.Unlock()
+ if _, dup := registry[name]; dup {
+ panic("metrics: Register called twice for " + name)
+ }
+ registry[name] = factory
+}
+
+// Deregister removes a previously registered reporter factory. It is a no-op
if
+// name is not registered. This exists primarily so tests can register a
factory
+// and undo it via t.Cleanup, keeping the process-global registry re-runnable
+// under go test -count=N.
+func Deregister(name string) {
+ registryMu.Lock()
+ defer registryMu.Unlock()
+ delete(registry, name)
+}
+
+// FromProperties builds the reporter named by props[ReporterImplKey]. An
absent
+// or empty name yields [NopReporter] (reporting is opt-in), and an
unrecognized name is
Review Comment:
The "reporting is opt-in" choice is reasonable, but it diverges from Java,
where an absent `metrics-reporter-impl` defaults to `LoggingMetricsReporter`,
not a nop. Folks migrating from Java will expect scan/commit reports to show up
by default, so I'd add a line to this godoc noting the divergence is
intentional. Non-blocking.
##########
catalog/hive/hive.go:
##########
@@ -226,12 +233,18 @@ func (c *Catalog) RegisterTable(ctx context.Context,
identifier table.Identifier
return nil, fmt.Errorf("%w: %s.%s",
catalog.ErrTableAlreadyExists, database, tableName)
}
+ reporter, err := metrics.FromProperties(c.opts.props)
+ if err != nil {
+ return nil, fmt.Errorf("failed to initialize metrics reporter:
%w", err)
+ }
+
tbl, err := table.NewFromLocation(
ctx,
identifier,
metadataLocation,
io.LoadFSFunc(c.opts.props, metadataLocation),
c,
+ table.WithMetricsReporter(reporter),
)
if err != nil {
return nil, fmt.Errorf("failed to read table metadata from %s:
%s", metadataLocation, err)
Review Comment:
Not from this PR, but it's now sitting right under the new reporter block
and the reporter error just above it uses `%w`. I'd switch this `%s` to `%w` so
`errors.Is`/`errors.As` can unwrap it. Non-blocking.
--
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]