Skip to content

Engine skeleton with catalog reload loop#248

Draft
em3s wants to merge 11 commits intomainfrom
feature/engine-skeleton
Draft

Engine skeleton with catalog reload loop#248
em3s wants to merge 11 commits intomainfrom
feature/engine-skeleton

Conversation

@em3s
Copy link
Copy Markdown
Contributor

@em3s em3s commented Apr 13, 2026

Summary

Phase 1 of the V3-native engine. A new Engine class lives in engine.runtime with zero v2.* and zero Spring imports. PeriodicCatalogLoader mirrors Graph.startMetastoreReload but the reload body is empty until phase 2.

Graph is unchanged. The new bean runs alongside it under actionbase.catalog.reload-interval.

Refs #247

Changes

  • Engine — composition root, AutoCloseable, Engine.create(...) factory.
  • CatalogLoader — small interface (bind + idempotent close). Catalog = the registry of Database / Table / Alias definitions, mirroring industry terms (Spark / Trino / Iceberg / Glue).
  • PeriodicCatalogLoader — single Reactor pipeline. Flux.interval for periodic, Mono.delay(...).flux() for one-shot. Both share the same operators, error handler, and disposable.
  • EngineConfiguration — single @Bean calling Engine.create(...). Only Engine is imported.
  • ServerProperties.CatalogProperties — nested under the existing actionbase prefix.
  • application.yamlactionbase.catalog.reload-interval: 1m alongside the existing kc.graph.metastoreReloadInterval.
  • Tests: construction binding, lifecycle delegation, periodic schedule, bind-triggers-exactly-one-reload, disabled-but-still-initial, initial-delay defers first reload.

How to Test

./gradlew :engine:test --tests "com.kakao.actionbase.engine.runtime.*"
./gradlew :server:compileKotlin

Boot the server and look for: Starting Flux.interval for reloading catalog every 60000 ms after 0 ms delay.

Phase 1 of the V3-native engine: a composition root with a single child,
a MetadataLoader bound at construction time. PeriodicMetadataLoader
mirrors Graph.startMetastoreReload but the reload body is empty until
phase 2 fills it in.

Engine.create() is the canonical entry point — Spring config, CLI, and
tests all use it, and the engine.runtime package has zero Spring import
and zero v2 import. The Engine class itself only delegates: new
responsibilities arrive as new children rather than as new methods on
Engine. This is the structural defense against the Graph.kt god-class
pattern.

Highlights:

- MetadataLoader is a small interface (bind + AutoCloseable). The bind
  contract is documented: called once by Engine's ctor, must not call
  back into the engine synchronously, idempotent close.
- PeriodicMetadataLoader runs a single Reactor pipeline. Periodic mode
  uses Flux.interval(initialDelay, interval); disabled mode uses
  Mono.delay(initialDelay).flux() for a one-shot. Both branches share
  the same operators, error handler, and disposable so the engine gets
  exactly one initial reload regardless of configuration.
- bind / close are synchronized; the bound flag prevents double-binding
  and unbound-close noise.
- ServerProperties.MetastoreProperties groups the two new settings under
  actionbase.metastore.*; application.yaml mirrors the existing
  kc.graph.metastoreReloadInterval value and notes the two configs run
  side by side until phase 2 completes the cutover.
- Tests cover construction binding, lifecycle delegation, periodic
  scheduling, bind-triggers-exactly-one-reload, the disabled-but-still-
  initial path, and that the configured initial delay actually defers
  the first reload.
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Apr 13, 2026
em3s added 3 commits April 13, 2026 18:39
- Drop KDoc blocks from Engine, MetadataLoader, and PeriodicMetadataLoader.
- Rename Engine's ctor parameter from `loader` to `metadataLoader` for symmetry with the property in `create`.
- Reformat `Engine.create` as a block body with a named local for clarity.
The engine runtime tracks Database, Table, and Alias definitions —
exactly what the industry calls a catalog (Spark/Trino/Iceberg/Glue
all use this term). Metadata was too broad; Catalog names what we
actually load.

Renames:
- MetadataLoader -> CatalogLoader
- PeriodicMetadataLoader -> PeriodicCatalogLoader
- Engine.metadataLoader -> catalogLoader
- metastoreReloadInitialDelay/Interval -> catalogReloadInitialDelay/Interval
- ServerProperties.metastore -> catalog (CatalogProperties)
- actionbase.metastore.* -> actionbase.catalog.*
- log strings: 'reloading metastore' -> 'reloading catalog' etc.

Metastore (the JDBC backing store) keeps its name; the catalog is
what lives in memory and is loaded *from* the metastore.
@em3s em3s marked this pull request as draft April 13, 2026 09:49
@em3s em3s changed the title Engine skeleton with metadata reload loop Engine skeleton with catalog reload loop Apr 13, 2026
em3s added 7 commits April 13, 2026 18:55
- com.kakao.actionbase.engine.Engine (was engine.runtime.Engine).
  Engine now sits next to QueryEngine, MutationEngine, and the
  Actionbase stub it will eventually replace.
- com.kakao.actionbase.engine.catalog.{CatalogLoader, PeriodicCatalogLoader}
  (was engine.runtime.*). Future catalog types (CatalogSnapshot,
  TableId, push-based loaders) get a natural home.
- The empty engine.runtime package is removed.
The interface IS the catalog (the registry of databases / tables /
aliases), matching how Spark, Iceberg, Trino, and Glue use the term.
`bind` is part of the catalog's lifecycle, the same way Spark's
ExternalCatalog has `init`/`close` next to its query methods.

- CatalogLoader -> Catalog
- PeriodicCatalogLoader -> PeriodicCatalog
- log strings and field names follow the rename.
- Catalog interface exposes three maps directly (DatabaseDescriptor,
  TableDescriptor, AliasDescriptor) so callers can write catalog.tables[id]
  instead of catalog.snapshot.tables[id].
- PeriodicCatalog keeps a private nested Snapshot data class holding
  all three maps in one `@Volatile` reference, so reload swaps them
  atomically (a single reference write). Each getter delegates to the
  current snapshot, giving consistent per-call reads without putting
  the snapshot type on the public API.
- EngineTest's FakeLoader implements the three maps directly (empty).
- Catalog: restore KDoc covering the bind/close contract, single-
  threaded lifecycle assumption, and the reader contract for databases/
  tables/aliases (immutable or snapshot-at-read; single-getter
  consistency guaranteed, cross-map best-effort).
- PeriodicCatalog: mark `engine` as @volatile so the null-check in
  reload() has a happens-before with close()'s write — the code now
  actually matches what the guard comment claims.
- PeriodicCatalog: regroup fields into state / public views so readers
  see a single ordering.
The runtime object for a table was called TableBinding, which is an
unusual term. The industry-standard pair for what we have is
TableDescriptor (declarative spec) and Table (live runtime, cached)
— see Spark, Iceberg, Trino, Glue.

Renames (interface + concrete + test + callers):
- engine.binding.TableBinding -> engine.binding.Table
- v2.engine.v3.V2BackedTableBinding -> V2BackedTable
- v2.engine.v3.NilTableBinding -> NilTable
- V2BackedTableBindingTest -> V2BackedTableTest
- QueryEngine.getTableBinding -> getTable
- MutationEngine.getTableBinding -> getTable
- HBaseIndexedLabel.tableBinding -> table

The package `engine.binding` is kept as-is for now — moving it to
`engine.table` or `engine.catalog` is a separate cleanup. Prepares
for the Catalog to cache Table instances in phase 3.
The catalog snapshot now holds fully-resolved runtime objects instead
of raw descriptors, matching Iceberg's Table / Catalog model.

- Add engine.catalog.Database  (thin wrapper over DatabaseDescriptor).
- Add engine.catalog.Alias     (AliasDescriptor + resolved Table).
- Move engine.binding.Table -> engine.catalog.Table and add
  `val descriptor: TableDescriptor<*>` so Table carries both its spec
  and its runtime ops — the core of the user's 'Table = Descriptor +
  runtime' model.
- V3TableDescriptor gains `toTableDescriptor(entity)` so the v2
  adapters can build the full core.metadata.TableDescriptor from a
  LabelEntity without pulling in V3MetadataConverter.
- V2BackedTable and NilTable implement the new `descriptor` field
  using the V3TableDescriptor conversion. NilTable now takes the
  LabelEntity too so it can build its descriptor.
- Catalog interface exposes Database / Table / Alias maps; Snapshot in
  PeriodicCatalog swaps runtime types atomically via a single reference.

Phase 2 will read the metastore through `engine` inside reload() and
build fresh Database / Table / Alias instances, reusing existing Table
instances when their descriptors haven't changed.
@em3s em3s self-assigned this Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant