Documentation
¶
Index ¶
- Constants
- Variables
- type AdaptivePersister
- type Degree
- type FlatGraph
- func (g *FlatGraph) AddEdge(from, to, relationship string) error
- func (g *FlatGraph) AddNode(nodeID, nodeType string) error
- func (g *FlatGraph) CheckEdge(from, to, relationship string) error
- func (g *FlatGraph) Clear() error
- func (g *FlatGraph) EdgeCount() int
- func (g *FlatGraph) EdgeCountForTenant(tenantPrefix string) (int, error)
- func (g *FlatGraph) FindPath(from, to string, maxDepth int) ([]string, error)
- func (g *FlatGraph) GetAllNodes() []string
- func (g *FlatGraph) GetAllNodesForTenant(tenantPrefix string) ([]string, error)
- func (g *FlatGraph) GetDegree(nodeID string) (Degree, error)
- func (g *FlatGraph) GetIncomingEdges(nodeID string) (map[string]string, error)
- func (g *FlatGraph) GetNeighbors(nodeID string) (map[string]string, error)
- func (g *FlatGraph) GetNodeInfo(nodeID string) (*NodeInfo, error)
- func (g *FlatGraph) GetNodesByType(entityType string) []string
- func (g *FlatGraph) GetNodesByTypeForTenant(tenantPrefix, entityType string) ([]string, error)
- func (g *FlatGraph) HasCycle() bool
- func (g *FlatGraph) HasCycleForTenant(tenantPrefix string) (bool, error)
- func (g *FlatGraph) Load(filename string) error
- func (g *FlatGraph) NodeCount() int
- func (g *FlatGraph) NodeCountForTenant(tenantPrefix string) (int, error)
- func (g *FlatGraph) NodeExists(nodeID string) bool
- func (g *FlatGraph) PathExists(from, to string, maxDepth int) (bool, int, error)
- func (g *FlatGraph) RemoveEdge(from, to string) error
- func (g *FlatGraph) RemoveNode(nodeID string) error
- func (g *FlatGraph) Save(filename string) error
- func (g *FlatGraph) SetCycleCheckLimit(n int)
- func (g *FlatGraph) SharedOutNeighbors(nodeA, nodeB string) ([]string, error)
- func (g *FlatGraph) UpdateFromEntity(entity string, id int, data map[string]interface{}) error
- func (g *FlatGraph) UpdateFromEntityForTenant(tenantID uint16, entity string, id int, data map[string]interface{}) error
- func (g *FlatGraph) VodeCount() int
- func (g *FlatGraph) VodeCountForTenant(tenantPrefix string) (int, error)
- type Graph
- type NodeInfo
Constants ¶
const CycleCheckBudgetExceeded = "cycle-check-budget-exceeded" // informational tag only
CycleCheckBudgetExceeded is the sentinel returned when wouldCreateCycle exhausts its BFS budget without confirming a cycle. This is indistinguishable from a true cycle at the API level: AddEdge returns ErrCycleDetected and the HTTP handler surfaces a 409. Operators and callers should be aware that on large or dense graphs the default budget of DefaultCycleCheckLimit (512) visited nodes may be exceeded by a legitimate (non-cyclic) edge addition. The limit is configurable via FlatGraph.SetCycleCheckLimit or the OLU_GRAPH_CYCLE_CHECK_LIMIT environment variable. This behaviour is documented here and in GRAPH_API.md so that callers can reason about it.
Note: this constant is informational only — the code uses cycleCheckLimit directly. It is not returned as an error value; use ErrCycleDetected to detect both true cycles and budget-exhaustion rejections.
const DefaultCycleCheckLimit = 512
DefaultCycleCheckLimit is the default BFS node-visit budget for wouldCreateCycle. It is exported so that callers (e.g. the startup diagnostic print) can display the effective default without hard-coding it. Override at runtime via FlatGraph.SetCycleCheckLimit or the OLU_GRAPH_CYCLE_CHECK_LIMIT environment variable.
const NodeTypeVode = "__vode__"
NodeTypeVode is the synthetic type assigned to a graph node that was created implicitly by AddEdge as a forward-reference placeholder. A vode represents a node that has been pointed at by a REF field but whose entity data has not yet arrived (e.g. during streaming graph hydration where edges may be replayed before their target entities are written).
Vode lifecycle:
AddEdge("a:1", "b:1", "R") is called when "b:1" does not exist yet. addEdgeLocked creates "b:1" with type NodeTypeVode as a placeholder. VodeCount increments.
The entity for "b:1" is later written and UpdateFromEntity / AddNode is called with the real type. addNodeLocked detects the promotion from NodeTypeVode, removes the node from the vode type index, adds it to the real type index, and decrements VodeCount.
At the end of successful hydration, VodeCount() should be zero. A non-zero count indicates dangling references — entity data that was referenced by a REF field but never written to the store.
The string value "__vode__" is intentionally non-colliding with any valid olu entity type name (which may not contain underscores by convention). "Vode" is a domain-specific term; it is not an acronym and should not be confused with REF, which is the *edge* pointing at a vode node.
Variables ¶
var ( ErrCycleDetected = errors.New("adding this edge would create a cycle") ErrCrossTenantEdge = errors.New("edge endpoints belong to different tenants") ErrMalformedNodeID = errors.New("node ID contains '@' but is not a valid XXXX@-prefixed ID") // ErrEdgeAlreadyExists is returned by AddEdge when an edge from→to already // exists with a *different* relationship label. The same pair with the same // label is a no-op (idempotent); only label conflicts trigger this. // // NOTE: this sentinel is also used internally by UpdateFromEntityForTenant // as a signal that an existing edge needs its label updated. External callers // should only see it when they call AddEdge directly. If the semantics of // this error ever change, UpdateFromEntityForTenant must be audited too. ErrEdgeAlreadyExists = errors.New("edge already exists with a different relationship name") // ErrNodeTypeMismatch is returned by AddNode when the node already exists // with an established non-vode type and the caller supplies a different type. // Silently retyping a node corrupts the type index and is almost certainly a // caller bug. // // Vode nodes (type NodeTypeVode, created implicitly by AddEdge as forward- // reference placeholders) are explicitly exempt: promoting a vode to a real // type via AddNode is the intended completion path and does not trigger this // error. ErrNodeTypeMismatch = errors.New("node already exists with a different type") )
Sentinel errors. Previously re-exported from pkg/graph/state; now defined here directly after the removal of IndexedGraph and the state package.
Functions ¶
This section is empty.
Types ¶
type AdaptivePersister ¶
type AdaptivePersister struct {
// contains filtered or unexported fields
}
AdaptivePersister handles graph persistence with adaptive timing. Under high write load, saves are batched to reduce I/O. Under low load, changes persist quickly.
DEPRECATED: The JSON filestore backend is deprecated. The AdaptivePersister is only started when that backend is in use (storeHasEdgeTable == false in main.go). SQLite deployments — i.e. all production configurations — never start it; graph persistence is handled by the store's edge tables in that path. When the JSON filestore is removed, this entire type should be deleted along with it.
func NewAdaptivePersister ¶
func NewAdaptivePersister(g Graph, filename string, logger zerolog.Logger) *AdaptivePersister
NewAdaptivePersister creates a new adaptive persister
func (*AdaptivePersister) MarkDirty ¶
func (p *AdaptivePersister) MarkDirty()
MarkDirty signals that the graph has been modified and needs to be persisted. Call this after any mutation (AddEdge, RemoveNode, etc.).
func (*AdaptivePersister) Start ¶
func (p *AdaptivePersister) Start()
Start begins the background persistence loop. It is safe to call more than once — only the first call starts the loop. Calling Stop without a prior Start is safe and returns immediately.
func (*AdaptivePersister) Stats ¶
func (p *AdaptivePersister) Stats() map[string]interface{}
Stats returns current persister statistics
func (*AdaptivePersister) Stop ¶
func (p *AdaptivePersister) Stop()
Stop gracefully shuts down, ensuring a final save. If Start was never called, Stop is a no-op and returns immediately.
func (*AdaptivePersister) WriterEnter ¶
func (p *AdaptivePersister) WriterEnter()
WriterEnter increments active writer count. Call at the start of a write operation.
func (*AdaptivePersister) WriterExit ¶
func (p *AdaptivePersister) WriterExit()
WriterExit decrements active writer count. Call at the end of a write operation (defer recommended).
type FlatGraph ¶
type FlatGraph struct {
// contains filtered or unexported fields
}
FlatGraph implements Graph with a single node map per instance.
func NewFlatGraph ¶
func NewFlatGraph() *FlatGraph
NewFlatGraph returns an empty FlatGraph with cycle detection disabled.
func NewFlatGraphWithCycleDetection ¶
NewFlatGraphWithCycleDetection returns a FlatGraph with the given cycle detection mode ("ignore", "warn", or "error"). An unrecognised mode prints a warning to stderr and falls back to "ignore".
func NewFlatGraphWithLogger ¶
NewFlatGraphWithLogger returns a FlatGraph that emits structured log events via the supplied zerolog.Logger. Use zerolog.Nop() (the default) to suppress all graph-layer logging.
func (*FlatGraph) CheckEdge ¶
CheckEdge runs the same pre-flight checks as AddEdge without modifying the graph. It is safe to call concurrently. Returns nil if AddEdge would accept the edge, or the first error AddEdge would return.
Note: there is an inherent TOCTOU gap between CheckEdge and a subsequent AddEdge call — a concurrent write could change the graph state in between. CheckEdge is intended for use as a guard before an external storage write (e.g. SQLite); false rejections are possible under high concurrency but are safe (the write is simply refused). False acceptances are not possible for single-threaded or low-concurrency workloads.
func (*FlatGraph) EdgeCountForTenant ¶
func (*FlatGraph) GetAllNodes ¶
func (*FlatGraph) GetAllNodesForTenant ¶
func (*FlatGraph) GetIncomingEdges ¶
GetIncomingEdges returns a copy of the incoming-edge map for nodeID. If nodeID does not exist, an empty map is returned with a nil error (same silent-empty contract as GetNeighbors; see its comment).
func (*FlatGraph) GetNeighbors ¶
GetNeighbors returns a copy of the outgoing-edge map for nodeID. If nodeID does not exist, an empty map is returned with a nil error — deliberately unlike GetNodeInfo / GetDegree which error on absent nodes. The server layer relies on this: it calls GetNeighbors before deciding whether to add/remove edges and treats "no neighbours" the same as "node not yet in graph".
func (*FlatGraph) GetNodesByType ¶
func (*FlatGraph) GetNodesByTypeForTenant ¶
func (*FlatGraph) HasCycleForTenant ¶
HasCycleForTenant reports whether the tenant-scoped subgraph (nodes whose ID carries tenantPrefix) contains a directed cycle. Only nodes with that prefix are visited; edges to other tenants are not followed. Returns (false, ErrTenantRequired) when tenantPrefix is empty.
func (*FlatGraph) Load ¶
Load replaces the graph's in-memory state with the contents of filename. If the file was written by a current version of olu, the cycle-detection mode and limit are restored from the file. Files written by older versions that lack these fields leave the current runtime configuration unchanged, so existing deployments that configure the mode via NewFlatGraphWithCycleDetection continue to work without modification. Load is a no-op (returns nil) when filename does not exist.
func (*FlatGraph) NodeCountForTenant ¶
func (*FlatGraph) NodeExists ¶
func (*FlatGraph) PathExists ¶
func (*FlatGraph) RemoveEdge ¶
func (*FlatGraph) RemoveNode ¶
func (*FlatGraph) Save ¶
Save serialises the graph to filename using an atomic write (write to .tmp then rename). The cycle-detection mode and limit are included in the file so that Load can restore them without additional caller configuration. The read lock is held only long enough to snapshot in-memory state; JSON marshalling and disk I/O happen outside the lock.
func (*FlatGraph) SetCycleCheckLimit ¶
SetCycleCheckLimit sets the maximum number of unique nodes the BFS in wouldCreateCycle may visit before returning true conservatively. A value of 0 restores the package default (DefaultCycleCheckLimit = 512). Negative values are treated as 0 (i.e. the default is used). This method is safe to call concurrently; it acquires the write lock. It is the runtime counterpart of the OLU_GRAPH_CYCLE_CHECK_LIMIT config key.
func (*FlatGraph) SharedOutNeighbors ¶
SharedOutNeighbors returns nodes reachable from both nodeA and nodeB via outgoing edges. Only the out-maps are consulted: a node C that points to nodeA or nodeB via an incoming edge is not included.
When nodeA == nodeB the method returns all outgoing neighbours of that single node — every neighbour trivially satisfies "reachable from both".
Returns a non-nil (possibly empty) slice. Callers may rely on this; they do not need to guard against nil before ranging or checking length.
Formerly named CommonNeighbors; renamed to SharedOutNeighbors to make the directed out-edge-only semantics explicit and avoid confusion with the graph-theory term "common neighbours" which is typically undirected.
func (*FlatGraph) UpdateFromEntity ¶
func (*FlatGraph) UpdateFromEntityForTenant ¶
type Graph ¶
type Graph interface {
// Mutation
AddNode(nodeID string, nodeType string) error
RemoveNode(nodeID string) error
AddEdge(from, to, relationship string) error
RemoveEdge(from, to string) error
// CheckEdge runs the same pre-flight checks as AddEdge (cross-tenant guard,
// cycle detection) without modifying the graph. Returns nil if the edge
// would be accepted, or the same error that AddEdge would return.
// Use this to validate edges before committing the owning entity to storage,
// so that a cycle-detection rejection cannot leave the SQLite edge table and
// the in-memory graph in disagreement.
CheckEdge(from, to, relationship string) error
UpdateFromEntityForTenant(tenantID uint16, entity string, id int, data map[string]interface{}) error
UpdateFromEntity(entity string, id int, data map[string]interface{}) error
// Traversal
GetNeighbors(nodeID string) (map[string]string, error)
GetIncomingEdges(nodeID string) (map[string]string, error)
FindPath(from, to string, maxDepth int) ([]string, error)
PathExists(from, to string, maxDepth int) (bool, int, error)
// via outgoing edges — i.e. shared out-neighbours in a directed graph.
// Nodes that point *to* nodeA or nodeB (incoming edges) are not
// considered. Returns an empty (non-nil) slice when there is no overlap.
// Previously named CommonNeighbors; renamed to match graph-theory convention
// (shared directed out-neighbours, not undirected common neighbours).
SharedOutNeighbors(nodeA, nodeB string) ([]string, error)
// Node queries
NodeExists(nodeID string) bool
GetNodeInfo(nodeID string) (*NodeInfo, error)
GetNodesByType(entityType string) []string
GetAllNodes() []string
GetDegree(nodeID string) (Degree, error)
// Metrics
NodeCount() int
EdgeCount() int
HasCycle() bool
// HasCycleForTenant reports whether the subgraph for the given tenant
// prefix contains a directed cycle. It performs a DFS scoped to nodes
// carrying that prefix; nodes from other tenants are not visited.
// Returns an error if tenantPrefix is empty (same guard as the other
// *ForTenant methods).
HasCycleForTenant(tenantPrefix string) (bool, error)
// VodeCount returns the total number of vode (placeholder) nodes currently
// in the graph across all tenants. Should be zero after successful hydration.
VodeCount() int
// VodeCountForTenant returns the vode count for a single tenant. Returns
// ErrTenantRequired if tenantPrefix is empty.
VodeCountForTenant(tenantPrefix string) (int, error)
// Tenant-scoped queries
NodeCountForTenant(tenantPrefix string) (int, error)
EdgeCountForTenant(tenantPrefix string) (int, error)
GetAllNodesForTenant(tenantPrefix string) ([]string, error)
GetNodesByTypeForTenant(tenantPrefix, entityType string) ([]string, error)
// Persistence
Save(filename string) error
Load(filename string) error
Clear() error
}
Graph interface defines all graph operations. The interface is intentionally broad so that the server layer never needs to type-assert to a concrete implementation.