mirror of
https://github.com/hanzoai/compute.git
synced 2026-10-30 14:50:41 +00:00
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]>
252 lines
10 KiB
Go
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")
|
|
}
|