Files
node/db/rpcdb/service.go
T
Hanzo AI 8d461c661e node: refresh config + rpcdb + vm registry + subprocess initializer
Routine maintenance: config.go, rpcdb/service.go, node/node.go,
vms/registry/registry.go, vms/rpcchainvm/runtime/subprocess/initializer.go.
2026-05-20 16:26:18 -07:00

265 lines
9.0 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package rpcdb is luxd's canonical Layer C home for the rpcdb service:
// one Service implementation backed by a database.Database, with a
// transport adapter per supported wire protocol.
//
// Layered topology:
//
// Layer A — wire framing (github.com/luxfi/api/zap)
// Layer B — service spec (data carriers) (github.com/luxfi/proto/rpcdb)
// Layer C — service impl + transports (this package)
//
// One Service. Many transport adapters. Adding a new transport is a
// new file that wraps `*Service`; the storage logic stays here and
// stays orthogonal to framing concerns.
//
// Files:
// - service.go (this) — transport-neutral Service struct + methods
// - grpc_server.go (build tag `grpc`) — gRPC adapter
// - zap_server.go (default) — ZAP adapter
package rpcdb
import (
"context"
"errors"
"sync"
"github.com/luxfi/database"
rpcdb "github.com/luxfi/proto/rpcdb"
)
// errUnknownIterator is the sentinel returned by IteratorNext / Error
// / Release when the caller hands in an iterator id that was never
// allocated (or has already been released). Transport adapters should
// surface this however their wire spec demands.
var errUnknownIterator = errors.New("rpcdb: unknown iterator")
// iterationBatchBytes is the upper bound on bytes batched into a
// single IteratorNext response. Picked to match the cevm-side
// expectation (RemoteZapDB::iterator_next reads up to ~128 KiB per
// roundtrip).
const iterationBatchBytes = 128 * 1024
// Service is the rpcdb service implementation. It holds one
// `database.Database` and a server-managed iterator pool, and exposes
// every rpcdb operation as a (request → response) method on Go data
// carriers from the proto/rpcdb wire-types package.
//
// Service is transport-agnostic: it knows nothing about gRPC, ZAP, or
// any other framing. Each transport adapter wraps `*Service` and
// translates its own wire format into these method calls.
type Service struct {
db database.Database
// iteratorMu serializes mutations of nextIteratorID and iterators.
// It does NOT serialize calls to a particular iterator — the
// underlying database.Iterator is documented as not safe for
// concurrent use, and the caller is expected to respect that.
iteratorMu sync.RWMutex
nextIteratorID uint64
iterators map[uint64]database.Iterator
}
// NewService wraps `db` so it can be served over any transport.
func NewService(db database.Database) *Service {
return &Service{
db: db,
iterators: make(map[uint64]database.Iterator),
}
}
// DB returns the underlying database. Adapters that need to surface
// transport-specific behavior (eg. health-check serialization) can
// reach in for it; everyone else uses the methods.
func (s *Service) DB() database.Database {
return s.db
}
// CloseIterators releases every server-side iterator. Adapters call
// this on shutdown so iterators don't leak past the listener.
func (s *Service) CloseIterators() {
s.iteratorMu.Lock()
defer s.iteratorMu.Unlock()
for id, it := range s.iterators {
it.Release()
delete(s.iterators, id)
}
}
// errToCode maps a database error to the wire enum.
func errToCode(err error) rpcdb.Error {
switch {
case err == nil:
return rpcdb.Error_ERROR_UNSPECIFIED
case errors.Is(err, database.ErrNotFound):
return rpcdb.Error_ERROR_NOT_FOUND
case errors.Is(err, database.ErrClosed):
return rpcdb.Error_ERROR_CLOSED
default:
// Any other error is reported as CLOSED on the wire — the
// proto enum doesn't have a richer error space. Adapters that
// can carry a transport-level error message should also pass
// the original err out-of-band.
return rpcdb.Error_ERROR_CLOSED
}
}
// CodeToErr maps a wire-typed Error back to a database sentinel. Used
// by remote clients on the receive side; lives here so server and
// client agree on the inverse of errToCode.
func CodeToErr(code rpcdb.Error) error {
switch code {
case rpcdb.Error_ERROR_UNSPECIFIED:
return nil
case rpcdb.Error_ERROR_NOT_FOUND:
return database.ErrNotFound
case rpcdb.Error_ERROR_CLOSED:
return database.ErrClosed
default:
return database.ErrClosed
}
}
// Has services rpcdb.Has.
func (s *Service) Has(_ context.Context, req *rpcdb.HasRequest) (*rpcdb.HasResponse, error) {
has, err := s.db.Has(req.Key)
if err != nil {
return &rpcdb.HasResponse{Has: false, Err: errToCode(err)}, nil
}
return &rpcdb.HasResponse{Has: has, Err: rpcdb.Error_ERROR_UNSPECIFIED}, nil
}
// Get services rpcdb.Get.
func (s *Service) Get(_ context.Context, req *rpcdb.GetRequest) (*rpcdb.GetResponse, error) {
value, err := s.db.Get(req.Key)
if err != nil {
return &rpcdb.GetResponse{Value: nil, Err: errToCode(err)}, nil
}
return &rpcdb.GetResponse{Value: value, Err: rpcdb.Error_ERROR_UNSPECIFIED}, nil
}
// Put services rpcdb.Put.
func (s *Service) Put(_ context.Context, req *rpcdb.PutRequest) (*rpcdb.PutResponse, error) {
return &rpcdb.PutResponse{Err: errToCode(s.db.Put(req.Key, req.Value))}, nil
}
// Delete services rpcdb.Delete.
func (s *Service) Delete(_ context.Context, req *rpcdb.DeleteRequest) (*rpcdb.DeleteResponse, error) {
return &rpcdb.DeleteResponse{Err: errToCode(s.db.Delete(req.Key))}, nil
}
// WriteBatch services rpcdb.WriteBatch.
func (s *Service) WriteBatch(_ context.Context, req *rpcdb.WriteBatchRequest) (*rpcdb.WriteBatchResponse, error) {
batch := s.db.NewBatch()
for _, p := range req.Puts {
if err := batch.Put(p.Key, p.Value); err != nil {
return &rpcdb.WriteBatchResponse{Err: errToCode(err)}, nil
}
}
for _, d := range req.Deletes {
if err := batch.Delete(d.Key); err != nil {
return &rpcdb.WriteBatchResponse{Err: errToCode(err)}, nil
}
}
return &rpcdb.WriteBatchResponse{Err: errToCode(batch.Write())}, nil
}
// Compact services rpcdb.Compact.
func (s *Service) Compact(_ context.Context, req *rpcdb.CompactRequest) (*rpcdb.CompactResponse, error) {
return &rpcdb.CompactResponse{Err: errToCode(s.db.Compact(req.Start, req.Limit))}, nil
}
// Close services rpcdb.Close.
func (s *Service) Close(_ context.Context, _ *rpcdb.CloseRequest) (*rpcdb.CloseResponse, error) {
return &rpcdb.CloseResponse{Err: errToCode(s.db.Close())}, nil
}
// HealthCheck services rpcdb.HealthCheck.
func (s *Service) HealthCheck(ctx context.Context) (*rpcdb.HealthCheckResponse, error) {
health, err := s.db.HealthCheck(ctx)
if err != nil {
return &rpcdb.HealthCheckResponse{Details: []byte(err.Error())}, nil
}
details := []byte("healthy")
if health != nil {
if str, ok := health.(string); ok {
details = []byte(str)
}
}
return &rpcdb.HealthCheckResponse{Details: details}, nil
}
// NewIteratorWithStartAndPrefix services rpcdb.NewIteratorWithStartAndPrefix.
func (s *Service) NewIteratorWithStartAndPrefix(_ context.Context, req *rpcdb.NewIteratorWithStartAndPrefixRequest) (*rpcdb.NewIteratorWithStartAndPrefixResponse, error) {
it := s.db.NewIteratorWithStartAndPrefix(req.Start, req.Prefix)
s.iteratorMu.Lock()
id := s.nextIteratorID
s.nextIteratorID++
s.iterators[id] = it
s.iteratorMu.Unlock()
return &rpcdb.NewIteratorWithStartAndPrefixResponse{Id: id}, nil
}
// IteratorNext services rpcdb.IteratorNext, returning up to
// iterationBatchBytes worth of (key, value) pairs per call. An empty
// Data slice signals exhaustion.
//
// Returned bytes are deep-copied so the caller can hold them past the
// next iterator advance; the underlying database.Iterator is allowed
// to recycle its internal buffers.
func (s *Service) IteratorNext(_ context.Context, req *rpcdb.IteratorNextRequest) (*rpcdb.IteratorNextResponse, error) {
s.iteratorMu.RLock()
it, ok := s.iterators[req.Id]
s.iteratorMu.RUnlock()
if !ok {
return nil, errUnknownIterator
}
var (
size int
data []*rpcdb.PutRequest
)
for size < iterationBatchBytes && it.Next() {
k := it.Key()
v := it.Value()
size += len(k) + len(v)
kc := make([]byte, len(k))
copy(kc, k)
vc := make([]byte, len(v))
copy(vc, v)
data = append(data, &rpcdb.PutRequest{Key: kc, Value: vc})
}
return &rpcdb.IteratorNextResponse{Data: data}, nil
}
// IteratorError services rpcdb.IteratorError.
func (s *Service) IteratorError(_ context.Context, req *rpcdb.IteratorErrorRequest) (*rpcdb.IteratorErrorResponse, error) {
s.iteratorMu.RLock()
it, ok := s.iterators[req.Id]
s.iteratorMu.RUnlock()
if !ok {
return &rpcdb.IteratorErrorResponse{Err: rpcdb.Error_ERROR_NOT_FOUND}, nil
}
return &rpcdb.IteratorErrorResponse{Err: errToCode(it.Error())}, nil
}
// IteratorRelease services rpcdb.IteratorRelease. Idempotent: calling
// it twice for the same id returns UNSPECIFIED on the second call.
func (s *Service) IteratorRelease(_ context.Context, req *rpcdb.IteratorReleaseRequest) (*rpcdb.IteratorReleaseResponse, error) {
s.iteratorMu.Lock()
it, ok := s.iterators[req.Id]
if !ok {
s.iteratorMu.Unlock()
return &rpcdb.IteratorReleaseResponse{Err: rpcdb.Error_ERROR_UNSPECIFIED}, nil
}
delete(s.iterators, req.Id)
s.iteratorMu.Unlock()
dbErr := it.Error()
it.Release()
return &rpcdb.IteratorReleaseResponse{Err: errToCode(dbErr)}, nil
}