Files
zeekayandHanzo Dev 03fcdb55d6 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:27:30 -07:00

252 lines
10 KiB
Go

// Copyright 2024 Hanzo Industries Inc. All Rights Reserved.
//
// Licensed 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 object
import (
"fmt"
"path/filepath"
"github.com/hanzoai/compute/conf"
"github.com/hanzoai/orm/relational"
)
// perOrgModels are the per-TENANT tables. Under the Base backend each gets one
// physical copy per org (its own SQLite file, routed by Owner); under Postgres
// isolation is a WHERE owner=? clause over one shared table. Every model here
// carries an Owner, so it routes cleanly to a per-org DB.
func perOrgModels() []any {
return []any{
new(Asset),
new(Provider),
new(Machine),
new(Record),
new(Session),
new(NodePool),
new(Volume),
new(AgentBinding),
}
}
// sharedModels are the cross-pod SHARED tables. They live on the ONE engine
// every pod sees (Postgres under both backends), NEVER in per-org SQLite:
// - Plan is a global read-only catalog; duplicating it per org would break DRY
// and let an admin price edit diverge pod-to-pod.
// - MeterLease is a cluster-global leader-election lease; a single linearizable
// winner per hour is the whole point, which per-pod SQLite cannot provide.
//
// See LLM.md (Base backend: shared vs per-org) for the full CTO rationale.
func sharedModels() []any {
return []any{
new(Plan),
new(MeterLease),
new(BillingLease),
new(FleetWorker),
new(CostCursor),
}
}
// models is visor's full table registry -- the union of per-org and shared
// tables, defined in exactly one place. The Postgres adapter (Adapter.createTable)
// hosts every table and syncs this full set; the Base backend syncs only
// perOrgModels into each org DB and leaves sharedModels on Postgres.
func models() []any {
return append(perOrgModels(), sharedModels()...)
}
// engineProvider resolves the *relational.Engine(s) that serve visor's tables. It is
// the ONE seam between the shared-Postgres backend (a single engine for every
// owner) and the Base backend (one per-org SQLite engine plus a shared Postgres
// coordination engine). Query and model code is identical on either side; only
// the resolved engine differs.
type engineProvider interface {
// EngineFor returns the engine that serves owner's per-tenant rows.
// Postgres ignores owner (isolation is a WHERE owner=? clause); Base
// returns, lazily opening, the org's own SQLite engine.
EngineFor(owner string) (*relational.Engine, error)
// Shared returns the ONE engine that holds the cross-pod shared tables (the Plan
// catalog and the billing leases). Postgres: a single shared, linearizable engine.
// Base: the pod-local `_global` SQLite engine, made cluster-safe for the leases by
// the single-writer gate plus PullSharedLeases/PushShared across a handoff.
Shared() *relational.Engine
// PullSharedLeases merges the object store's committed billing-lease rows into the
// live coord before a claim (durable lease history across a leadership handoff).
// Postgres: no-op — the shared engine already IS the durable, linearizable source.
// Base: row-merge from the `_global` snapshot into the local coord.
PullSharedLeases() error
// PushShared ships the coord DB to the object store after a winning claim, before
// the caller debits, so the claim is durable before money moves. Postgres: no-op.
PushShared() error
// AllEngines returns every engine a cross-org sweep must union over.
// Postgres: the single engine (it already holds every org's rows).
// Base: one engine per org DB under the data root.
AllEngines() ([]*relational.Engine, error)
// Close releases every engine the provider owns.
Close() error
}
// store is the process-wide provider selected at boot by InitStore. All model
// code routes per-tenant queries through EngineFor(owner), shared-table queries
// through Shared(), and cross-org sweeps through allEngines(). Under the default
// postgres backend all three resolve to the same one engine, so the live path is
// byte-identical to before the seam existed.
var store engineProvider
// StorageBackend names the configured persistence backend.
type StorageBackend string
const (
// BackendPostgres is the historical shared-Postgres backend (default).
BackendPostgres StorageBackend = "postgres"
// BackendBase is the per-org SQLite substrate (hanzoai/base, HIP-0302).
BackendBase StorageBackend = "base"
)
// ConfiguredBackend reads the storageBackend config knob. Default is Base (SQLite
// for everything — the platform rule); a multi-instance deployment opts INTO Postgres
// with storageBackend=postgres.
func ConfiguredBackend() StorageBackend {
if conf.GetConfigString("storageBackend") == string(BackendPostgres) {
return BackendPostgres
}
return BackendBase
}
// InitStore installs the pgStore engine provider for the Postgres backend. Base
// mode is wired directly in InitAdapter (it never opens a Postgres adapter), so
// this runs only on the opt-in Postgres path.
func InitStore() {
store = &pgStore{engine: adapter.engine}
}
// EngineFor returns the storage engine that serves owner under the configured
// backend. This is the single entry point backend-agnostic code MUST use so a
// query works unchanged on either backend.
func EngineFor(owner string) (*relational.Engine, error) {
if store == nil {
return nil, fmt.Errorf("visor: store not initialised (call InitAdapter first)")
}
return store.EngineFor(owner)
}
// mustEngineFor resolves owner's engine or panics. Used only where the caller
// has no error return to thread. Under the default Postgres backend EngineFor
// hands back the one already-open engine and cannot fail, so this panics only
// under the Base backend on a broken data disk -- a fatal infra condition,
// surfaced the same way Adapter.createTable's sync failure already is.
func mustEngineFor(owner string) *relational.Engine {
engine, err := EngineFor(owner)
if err != nil {
panic(err)
}
return engine
}
// Shared returns the engine holding the shared tables (the Plan catalog and the
// MeterLease coordination lease).
//
// It is cross-POD only under the Postgres backend, where it is the one durable
// linearizable store and the insert-once lease PK therefore holds cluster-wide.
// Under Base (the default) it is the pod-local `_global` SQLite coord, hydrated
// from and shipped to the object store — durable across a restart, but NOT
// shared between concurrent pods. There, exactly-once rests on the single-writer
// election in coordinator.go, not on the PK.
//
// This comment used to read "Postgres under both backends", which is false for
// Base and is precisely why the billing gate's importance was invisible: it made
// the election look like belt-and-braces over a global constraint when it is in
// fact the only thing standing between two pods and a double debit.
func Shared() *relational.Engine {
return store.Shared()
}
// Ready reports whether visor can still reach the store every route depends on.
// It is the question a readiness probe asks, answered in the ONE package that
// can answer it: the backend is chosen here and `store` is unexported, so no
// caller outside object could form the answer without a second copy of the
// selection rule.
//
// It pings the SHARED engine and not every org's, because the shared engine is
// the one both backends always have, and a fan-out over per-org SQLite would
// turn a probe that runs every ten seconds into one file open per tenant — a
// health check whose cost grows with the customer list is an outage waiting for
// a big enough customer list.
//
// A nil store is unready rather than an error to shout about: it is what visor
// looks like between process start and InitAdapter, which is exactly the window
// a readiness probe exists to keep traffic out of.
func Ready() error {
if store == nil {
return fmt.Errorf("visor: store not initialised")
}
e := store.Shared()
if e == nil {
return fmt.Errorf("visor: no shared engine")
}
return e.Ping()
}
// allEngines returns every engine a cross-org sweep (billing node-pool report,
// stale-session GC) must union over. One engine under Postgres; one per org DB
// under Base.
func allEngines() ([]*relational.Engine, error) {
if store == nil {
return nil, fmt.Errorf("visor: store not initialised (call InitAdapter first)")
}
return store.AllEngines()
}
// pullSharedLeases merges the durable billing-lease rows into the live coord before a
// claim (guarantee 2 of exactly-once). Backend-agnostic: a no-op under Postgres, a
// row-merge from the `_global` snapshot under Base.
func pullSharedLeases() error {
if store == nil {
return fmt.Errorf("visor: store not initialised (call InitAdapter first)")
}
return store.PullSharedLeases()
}
// pushShared ships the coord DB after a winning claim, before the caller debits, so
// the claim is durable before money moves. No-op under Postgres.
func pushShared() error {
if store == nil {
return fmt.Errorf("visor: store not initialised (call InitAdapter first)")
}
return store.PushShared()
}
// dataRoot is the filesystem root for per-org Base SQLite files (default /data).
func dataRoot() string {
if r := conf.GetConfigString("dataRoot"); r != "" {
return r
}
return "/data"
}
// DBPath is the HIP-0302 per-org SQLite path for a visor org DB. It mirrors the
// layout hanzo/cloud uses (orgs/<org>/<service>.db) so every Hanzo service that
// moves onto Base shares one on-disk convention.
//
// DBPath("/data", "acme") -> /data/orgs/acme/visor.db
//
// The empty owner (catalog / cluster-global rows that no org owns) routes to the
// _global sentinel, matching hanzo/cloud's fail-loud migration bucket.
func DBPath(root, owner string) string {
if owner == "" {
owner = "_global"
}
return filepath.Join(root, "orgs", owner, "visor.db")
}