Help us shape the future of Go! Take the 2026 Go Developer Survey

qortoo

package module
v0.0.0-...-29d0b62 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

qortoo-go

codecov CI CodSpeed GitHub commit activity GitHub Actions Workflow Status

Go binding for the Qortoo CRDT SDK. It wraps qortoo-ffi's C ABI with cgo and exposes idiomatic Go types (Client, Counter, Variable, Handler) over the same conflict-free datatypes, transactions, and observability pipeline as the Rust core.

Qortoo has not reached a first release yet: the public API, the native SDK distribution format, and this package's own versioning are all still subject to change without notice.

Requirements

  • Go 1.25 or newer (see go.mod).
  • CGO_ENABLED=1 and a C toolchain, since this package links libqortoo_ffi.a through cgo.
  • A qortoo-ffi native SDK: the compiled static library, its C header, and pkg-config metadata, built from a qortoo-rs checkout. There is no published native SDK release yet, so building one locally is currently the only option.

Building and Testing

With a qortoo-rs checkout next to this one (../qortoo-rs):

make test

This stages qortoo-ffi's native SDK from ../qortoo-rs automatically and points CGO_CFLAGS/CGO_LDFLAGS at it. Use QORTOO_RS_DIR=/path/to/qortoo-rs make test for a checkout in a different location.

Without a qortoo-rs checkout available — for example, against a native SDK installed some other way — set CGO_CFLAGS/CGO_LDFLAGS yourself and make test uses them as given, skipping auto-staging:

export CGO_CFLAGS="-I/path/to/native-sdk/include"
export CGO_LDFLAGS="-L/path/to/native-sdk/lib"
make test

make test is equivalent to go vet ./... and go test -a -race -count=1 ./... once CGO_CFLAGS/CGO_LDFLAGS are in place. Rebuilding and rerunning ensures the tests use the staged native library, whose changes Go's caches do not track.

The local quality gates use the same native SDK setup:

make lint
make coverage

make lint checks gofmt and runs the explicit linter set in .golangci.yml. Install golangci-lint 2.12.2 or newer before running it; CI pins exactly 2.12.2. make coverage runs the root binding package with race detection and atomic statement coverage, enforces a 90% minimum, and writes coverage.out plus coverage.html. Override the paths or threshold with COVERAGE_PROFILE, COVERAGE_HTML, or GO_COVERAGE_MIN. Example main packages remain outside the coverage denominator and are compiled separately by CI.

Importing this package checks the linked native library's ABI major version against the version this package was built against (see version.go) and panics on a mismatch, rather than risking undefined behavior across the FFI boundary.

See Getting Started for the complete native SDK layout, pkg-config setup, and supported build targets.

Usage

package main

import (
    "fmt"
    "log"

    "github.com/qortoo/qortoo-go"
)

func main() {
    client, err := qortoo.NewClient("my-collection", "my-alias")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    counter, err := client.SubscribeOrCreateCounter("visits", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer counter.Close()

    value, err := counter.IncreaseBy(1)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("counter value:", value)
}

A Variable holds a single JSON value under last-writer-wins resolution. Set takes any value encoding/json can marshal and returns the value held just before the call; Get decodes the current value into a destination pointer:

variable, err := client.SubscribeOrCreateVariable("profile", nil)
if err != nil {
    log.Fatal(err)
}
defer variable.Close()

type Profile struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

previous, err := variable.Set(Profile{Name: "ada", Age: 36})
if err != nil {
    log.Fatal(err)
}
fmt.Println("previous value:", previous) // nil before the first Set

var profile Profile
if err := variable.Get(&profile); err != nil {
    log.Fatal(err)
}

The stored JSON is what a Rust or any other binding reads back, so both sides must agree on the schema.

Without a connectivity option, NewClient uses the SDK-default no-op backend, so this example never leaves the process. See qortoo.NewLocalConnectivity for the in-memory backend used in tests.

Documentation

The C ABI architecture, error mapping, callback/userdata ownership, header generation, and native SDK release process remain in qortoo-rs.

Examples

examples/observability/ contains runnable trace, log, metrics, and profiling programs; see its README for setup against a local Grafana stack.

License

Apache License 2.0 — see LICENSE.

Documentation

Overview

Package qortoo is the Go binding of the Qortoo CRDT SDK (qortoo-rs).

It statically links the qortoo-ffi native library. This package carries no default include or library search path: point CGO_CFLAGS at the SDK's include directory and CGO_LDFLAGS at the directory holding libqortoo_ffi.a before building or testing, e.g.

export CGO_CFLAGS="-I/path/to/native-sdk/include"
export CGO_LDFLAGS="-L/path/to/native-sdk/lib"
go test ./...

Inside this repository, `make test` stages a matching SDK from the qortoo-rs checkout selected by QORTOO_RS_DIR.

A Client provides two datatypes: Counter, a conflict-free counter, and Variable, a last-writer-wins holder of a single JSON value. Variable values cross the boundary as encoding/json output and are stored verbatim, so they must be JSON-representable and every language reading the same variable must agree on the schema.

Every object returned by this package owns a native handle and must be released with its Close method. As a backstop, an unreachable wrapper frees its native handle via a runtime.AddCleanup cleanup, but cleanups are not guaranteed to run — treat Close as the contract, not the cleanup. A datatype keeps its Client reachable, so an otherwise unreferenced Client is not cleaned up while any of its datatypes is still alive.

Close must not be called concurrently with other methods on the same object; all other methods are safe for concurrent use.

Handler callbacks (Handler.OnStateChange / Handler.OnError) are invoked on Qortoo-owned tokio worker threads, asynchronously to the triggering operation. They must be safe for concurrent use and must not block for long. A blocking handler also delays Client.Close, which shuts the worker threads down, and ShutdownObservability, which flushes the exporters.

The Rust core emits tracing spans and metrics but installs no subscriber, recorder, or exporter on its own: call InitObservability once at startup to choose where they go, and the *Context methods (Counter.SyncContext, Variable.TransactionContext, and their siblings) to keep them in the trace of the calling span.

Index

Constants

View Source
const (
	// ClientError (100–)
	ErrCodeInvalidCollectionName             int32 = 100
	ErrCodeFailedToSubscribeOrCreateDatatype int32 = 101

	// DatatypeError (200–)
	ErrCodeTransactionFailed            int32 = 201
	ErrCodeInternal                     int32 = 202
	ErrCodeDisallowed                   int32 = 205
	ErrCodeNotWritable                  int32 = 206
	ErrCodeReadonlyViolation            int32 = 207
	ErrCodeSyncFailed                   int32 = 210
	ErrCodePushBufferExceededMaxMemSize int32 = 211
	ErrCodeServerRejected               int32 = 213
	ErrCodeValueConversion              int32 = 214

	// Observability (900–), only from InitObservability/ShutdownObservability
	ErrCodeObservabilityAlreadyInitialized   int32 = 900
	ErrCodeObservabilityShutDown             int32 = 901
	ErrCodeObservabilitySubscriber           int32 = 902
	ErrCodeObservabilityExporter             int32 = 903
	ErrCodeObservabilityRecorder             int32 = 904
	ErrCodeObservabilityInvalidConfig        int32 = 905
	ErrCodeObservabilityPartiallyInitialized int32 = 906

	// FFI boundary (see qortoo-ffi)
	ErrCodeInvalidArgument int32 = 998
	ErrCodeInternalFFI     int32 = 999
)

Error codes mirroring the #[repr(i32)] discriminants of the Rust error enums.

Variables

This section is empty.

Functions

func InitObservability

func InitObservability(opts ObservabilityOptions) error

InitObservability installs the SDK's logging, tracing, and metrics pipelines.

Call it once per process, before creating clients. It returns an error when it is called twice, when it is called after ShutdownObservability, when another library already installed a Rust global subscriber or metrics recorder, when an exporter cannot start, or when the options are invalid — never silently, because losing configured telemetry is worse than failing loudly at startup.

func ShutdownObservability

func ShutdownObservability(ctx context.Context) error

ShutdownObservability flushes pending telemetry and stops the exporters, using the deadline remaining in ctx as its shutdown budget (5s when it has none). A context already canceled or expired is returned before crossing the FFI boundary. It is a no-op when nothing was initialized, so deferring it is always safe.

This is terminal: a later InitObservability fails instead of pretending to restore the pipelines. The recommended order at exit is Counter.Close → Client.Close → ShutdownObservability → the application's own telemetry shutdown.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the entry point of the SDK, scoped by a collection name and alias.

func NewClient

func NewClient(collection, alias string, opts ...ClientOption) (*Client, error)

NewClient builds a client. Without a connectivity option it uses the SDK-default no-op backend.

func (*Client) Alias

func (c *Client) Alias() string

Alias returns the alias of this client.

func (*Client) Close

func (c *Client) Close()

Close releases the client and shuts down its internal runtime. Counters created from it remain safe to use until their own Close, but can no longer synchronize. Close must not be called concurrently with other methods on the same object.

func (*Client) Collection

func (c *Client) Collection() string

Collection returns the collection name of this client.

func (*Client) CreateCounter

func (c *Client) CreateCounter(key string, opts *DatatypeOptions) (*Counter, error)

CreateCounter builds a counter in StateCreating (writable).

func (*Client) CreateVariable

func (c *Client) CreateVariable(key string, opts *DatatypeOptions) (*Variable, error)

CreateVariable builds a variable in StateCreating (writable).

func (*Client) SubscribeCounter

func (c *Client) SubscribeCounter(key string, opts *DatatypeOptions) (*Counter, error)

SubscribeCounter builds a counter in StateSubscribing (read-only until synced).

func (*Client) SubscribeOrCreateCounter

func (c *Client) SubscribeOrCreateCounter(key string, opts *DatatypeOptions) (*Counter, error)

SubscribeOrCreateCounter builds a counter in StateSubscribingOrCreating (writable).

func (*Client) SubscribeOrCreateVariable

func (c *Client) SubscribeOrCreateVariable(key string, opts *DatatypeOptions) (*Variable, error)

SubscribeOrCreateVariable builds a variable in StateSubscribingOrCreating (writable).

func (*Client) SubscribeVariable

func (c *Client) SubscribeVariable(key string, opts *DatatypeOptions) (*Variable, error)

SubscribeVariable builds a variable in StateSubscribing (read-only until synced).

func (*Client) UnsubscribeDatatype

func (c *Client) UnsubscribeDatatype(key string) error

UnsubscribeDatatype marks the datatype identified by key as unsubscribing. With manual connectivity, a following Counter.Sync drives it to StateDisabled.

type ClientOption

type ClientOption func(*clientConfig)

ClientOption configures NewClient.

func WithLocalConnectivity

func WithLocalConnectivity(conn *LocalConnectivity) ClientOption

WithLocalConnectivity attaches a shared in-memory backend to the client.

type Counter

type Counter struct {
	// contains filtered or unexported fields
}

Counter is a conflict-free counter datatype.

func (*Counter) ClientVersion

func (d *Counter) ClientVersion() uint64

ClientVersion returns the number of local operations.

func (*Counter) Close

func (c *Counter) Close()

Close releases this handle. The underlying datatype stays registered in its client. No-op for transaction-scoped handles. Close must not be called concurrently with other methods on the same object.

func (*Counter) Increase

func (c *Counter) Increase() (int64, error)

Increase adds 1 and returns the new value.

func (*Counter) IncreaseBy

func (c *Counter) IncreaseBy(delta int64) (int64, error)

IncreaseBy adds delta (which may be negative) and returns the new value.

func (*Counter) Key

func (d *Counter) Key() string

Key returns the datatype key.

func (*Counter) ServerVersion

func (d *Counter) ServerVersion() uint64

ServerVersion returns the server-side version (0 before the first sync).

func (*Counter) SetHandler

func (d *Counter) SetHandler(priority uint, h *Handler)

SetHandler registers (or replaces) a handler at the given priority (lower priority runs first).

func (*Counter) State

func (d *Counter) State() DatatypeState

State returns the current lifecycle state.

func (*Counter) Sync

func (d *Counter) Sync() error

Sync performs a blocking push/pull with the connectivity backend. Use SyncContext to keep the sync inside the trace of a calling span.

func (*Counter) SyncContext

func (d *Counter) SyncContext(ctx context.Context) error

SyncContext is Sync continuing the trace of ctx.

The Rust spans of this sync — including the push/pull that runs on a worker thread and the handler callbacks it dispatches — become children of the span in ctx. Without a span in ctx it behaves exactly like Sync. Cancellation of ctx is not honoured: the underlying sync is a blocking call.

func (*Counter) SyncedClientVersion

func (d *Counter) SyncedClientVersion() uint64

SyncedClientVersion returns the last client version acknowledged by the server.

func (*Counter) Transaction

func (c *Counter) Transaction(tag string, fn func(tx *Counter) error) error

Transaction executes fn atomically: if fn returns an error (or panics), every operation performed through tx is rolled back. fn runs inline on the calling goroutine; tx is only valid during the call. Use TransactionContext to keep the commit inside the trace of a calling span.

func (*Counter) TransactionContext

func (c *Counter) TransactionContext(ctx context.Context, tag string, fn func(tx *Counter) error) error

TransactionContext is Transaction continuing the trace of ctx: the commit and any sync it triggers become children of the span in ctx. Without a span in ctx it behaves exactly like Transaction. Cancellation of ctx is not honoured — fn runs inline on the calling goroutine.

func (*Counter) Type

func (d *Counter) Type() DataType

Type returns the datatype kind, which is fixed by the concrete datatype (TypeCounter for a Counter).

func (*Counter) UnsetHandler

func (d *Counter) UnsetHandler(priority uint) bool

UnsetHandler removes the handler at the given priority. Returns true if one was removed.

func (*Counter) Unsubscribe

func (d *Counter) Unsubscribe() error

Unsubscribe marks this datatype as unsubscribing (see Client.UnsubscribeDatatype).

func (*Counter) Value

func (c *Counter) Value() int64

Value returns the current counter value.

type DataType

type DataType int32

DataType identifies the kind of CRDT datatype (mirrors qortoo::DataType).

const (
	TypeCounter  DataType = 0
	TypeVariable DataType = 1
	TypeMap      DataType = 2
)

type DatatypeOptions

type DatatypeOptions struct {
	// Readonly rejects all local writes regardless of state.
	Readonly bool
	// MaxPushBufferSize in bytes; 0 keeps the SDK default (clamped by the SDK).
	MaxPushBufferSize uint64
	// Handler registered at build time (needed to observe the first auto-sync
	// with realtime connectivity). HandlerPriority orders multiple handlers.
	Handler         *Handler
	HandlerPriority uint
}

DatatypeOptions configures datatype construction. The zero value (or nil pointer) selects the SDK defaults.

type DatatypeState

type DatatypeState int32

DatatypeState mirrors qortoo::DatatypeState (lifecycle and write access).

const (
	StateCreating              DatatypeState = 0
	StateSubscribing           DatatypeState = 1
	StateSubscribingOrCreating DatatypeState = 2
	StateSubscribed            DatatypeState = 3
	StateUnsubscribing         DatatypeState = 4
	StateDeleting              DatatypeState = 5
	StateDisabled              DatatypeState = 6
)

func (DatatypeState) String

func (s DatatypeState) String() string

type Error

type Error struct {
	Code int32
	Msg  string
}

Error is an error surfaced from the Qortoo SDK across the FFI boundary.

Two errors with the same Code represent the same error variant; Msg is informational only (matching the variant-only equality of the Rust enums).

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports code-based equality so errors.Is(err, &Error{Code: ...}) works.

type Handler

type Handler struct {
	// OnStateChange is called as (oldState, newState) after a lifecycle transition.
	OnStateChange func(oldState, newState DatatypeState)
	// OnError is called with errors surfaced from the sync path.
	OnError func(err *Error)
}

Handler receives datatype notifications. Both fields are optional. Callbacks run on Qortoo-owned worker threads (see package docs).

type LocalConnectivity

type LocalConnectivity struct {
	// contains filtered or unexported fields
}

LocalConnectivity is an in-memory synchronization backend for testing and development, mirroring the Rust core type of the same name. Share one instance between clients to synchronize them.

func NewLocalConnectivity

func NewLocalConnectivity() *LocalConnectivity

NewLocalConnectivity creates an in-memory backend in realtime mode.

func (*LocalConnectivity) Close

func (l *LocalConnectivity) Close()

Close releases the native handle. Clients built with this connectivity keep their own reference and stay functional. Close must not be called concurrently with other methods on the same object.

func (*LocalConnectivity) SetRealtime

func (l *LocalConnectivity) SetRealtime(realtime bool)

SetRealtime toggles between realtime (auto-sync) and manual (explicit Sync) mode.

type LogFormat

type LogFormat int32

LogFormat selects the stdout log stream written by the Rust side.

const (
	// LogFormatOff writes no logs. Combined with TraceEnabled=false it leaves the
	// Rust global subscriber untouched for other libraries in the process.
	LogFormatOff LogFormat = 0
	// LogFormatJSON writes one structured JSON object per event.
	LogFormatJSON LogFormat = 1
	// LogFormatText writes Qortoo's compact human-readable format. Colors are enabled
	// only when stdout is connected to a terminal.
	LogFormatText LogFormat = 2
)

type ObservabilityOptions

type ObservabilityOptions struct {
	// ServiceName is reported as service.name to the trace backend. Empty uses
	// "qortoo"; set it to the same name the application uses for its own telemetry so
	// Go and Rust spans land in one service.
	ServiceName string
	// LogFilter holds tracing filter directives (e.g. "qortoo=debug"). Empty falls
	// back to RUST_LOG, then to "qortoo=info".
	LogFilter string
	// LogFormat selects the stdout log stream (default: none).
	LogFormat LogFormat

	// TraceEnabled exports spans over OTLP/gRPC.
	TraceEnabled bool
	// OTLPEndpoint overrides the trace endpoint. Empty falls back to
	// OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, then OTEL_EXPORTER_OTLP_ENDPOINT, then
	// http://localhost:4317.
	OTLPEndpoint string

	// MetricsEnabled installs the metrics recorder and serves a Prometheus scrape
	// endpoint.
	MetricsEnabled bool
	// MetricsListenAddr is the address of that endpoint. Empty uses 0.0.0.0:9000.
	MetricsListenAddr string
}

ObservabilityOptions configures the SDK's telemetry pipelines. The zero value is valid and installs nothing.

type Variable

type Variable struct {
	// contains filtered or unexported fields
}

Variable is a last-writer-wins datatype holding a single JSON value.

A variable always holds a value; before the first Set that value is JSON null, which is indistinguishable from an explicitly stored nil. Concurrent writes are resolved by the logical timestamp of each Set, so every replica converges on the same winner regardless of the order in which the writes arrive.

Values cross the boundary as JSON produced by encoding/json, which is also what a Rust or any other binding reads back — so the value must be JSON-representable and both sides must agree on the schema. Map keys follow encoding/json: strings, integer types, and types implementing encoding.TextMarshaler are supported; other key types are not. The stored bytes are kept verbatim: they are never reparsed, reordered, or re-encoded by the SDK.

func (*Variable) ClientVersion

func (d *Variable) ClientVersion() uint64

ClientVersion returns the number of local operations.

func (*Variable) Close

func (v *Variable) Close()

Close releases this handle. The underlying datatype stays registered in its client. No-op for transaction-scoped handles. Close must not be called concurrently with other methods on the same object.

func (*Variable) Get

func (v *Variable) Get(dst any) error

Get decodes the current value into dst, which must be a non-nil pointer, exactly as encoding/json would. JSON numbers reaching an any destination are kept as json.Number.

The initial and an explicitly stored null both decode as JSON null, which leaves a non-nullable destination unchanged; decode into a pointer, an any, or a type with a custom unmarshaller to tell null apart from a stored zero.

Get reads local state only: it produces no operation and changes neither the datatype version nor the push buffer, so it is allowed in every state.

func (*Variable) Key

func (d *Variable) Key() string

Key returns the datatype key.

func (*Variable) ServerVersion

func (d *Variable) ServerVersion() uint64

ServerVersion returns the server-side version (0 before the first sync).

func (*Variable) Set

func (v *Variable) Set(value any) (any, error)

Set stores value as the new JSON value and returns the value held just before this call — nil for the first Set and for a stored JSON null.

The previous value has no destination type to decode into (a variable may hold a different shape on every Set), so it is returned dynamically, with JSON numbers kept as json.Number.

value is marshalled with encoding/json before any native call: a marshalling error is returned as-is and leaves the variable untouched.

func (*Variable) SetHandler

func (d *Variable) SetHandler(priority uint, h *Handler)

SetHandler registers (or replaces) a handler at the given priority (lower priority runs first).

func (*Variable) State

func (d *Variable) State() DatatypeState

State returns the current lifecycle state.

func (*Variable) Sync

func (d *Variable) Sync() error

Sync performs a blocking push/pull with the connectivity backend. Use SyncContext to keep the sync inside the trace of a calling span.

func (*Variable) SyncContext

func (d *Variable) SyncContext(ctx context.Context) error

SyncContext is Sync continuing the trace of ctx.

The Rust spans of this sync — including the push/pull that runs on a worker thread and the handler callbacks it dispatches — become children of the span in ctx. Without a span in ctx it behaves exactly like Sync. Cancellation of ctx is not honoured: the underlying sync is a blocking call.

func (*Variable) SyncedClientVersion

func (d *Variable) SyncedClientVersion() uint64

SyncedClientVersion returns the last client version acknowledged by the server.

func (*Variable) Transaction

func (v *Variable) Transaction(tag string, fn func(tx *Variable) error) error

Transaction executes fn atomically: if fn returns an error (or panics), every operation performed through tx is rolled back — restoring both the value and the timestamp the variable held before the transaction. fn runs inline on the calling goroutine; tx is only valid during the call. Use TransactionContext to keep the commit inside the trace of a calling span.

func (*Variable) TransactionContext

func (v *Variable) TransactionContext(ctx context.Context, tag string, fn func(tx *Variable) error) error

TransactionContext is Transaction continuing the trace of ctx: the commit and any sync it triggers become children of the span in ctx. Without a span in ctx it behaves exactly like Transaction. Cancellation of ctx is not honoured — fn runs inline on the calling goroutine.

func (*Variable) Type

func (d *Variable) Type() DataType

Type returns the datatype kind, which is fixed by the concrete datatype (TypeCounter for a Counter).

func (*Variable) UnsetHandler

func (d *Variable) UnsetHandler(priority uint) bool

UnsetHandler removes the handler at the given priority. Returns true if one was removed.

func (*Variable) Unsubscribe

func (d *Variable) Unsubscribe() error

Unsubscribe marks this datatype as unsubscribing (see Client.UnsubscribeDatatype).

Directories

Path Synopsis
examples
counter command
Command counter is the "Create and Update a Counter" walkthrough from docs/getting-started.md, kept runnable so an API change that breaks it is caught by CI's example-build step rather than silently going stale in the doc.
Command counter is the "Create and Update a Counter" walkthrough from docs/getting-started.md, kept runnable so an API change that breaks it is caught by CI's example-build step rather than silently going stale in the doc.
observability/log command
Command log demonstrates Qortoo's managed stdout logging from Go.
Command log demonstrates Qortoo's managed stdout logging from Go.
observability/metrics command
Command metrics demonstrates Qortoo metrics exported to Prometheus.
Command metrics demonstrates Qortoo metrics exported to Prometheus.
observability/profile command
Command profile demonstrates CPU profile export to Pyroscope.
Command profile demonstrates CPU profile export to Pyroscope.
observability/trace command
Command trace demonstrates a Go OpenTelemetry span continued by Qortoo's Rust core.
Command trace demonstrates a Go OpenTelemetry span continued by Qortoo's Rust core.
variable command
Command variable is the "Store a JSON Value in a Variable" walkthrough from docs/getting-started.md, kept runnable so an API change that breaks it is caught by CI's example-build step rather than silently going stale in the doc.
Command variable is the "Store a JSON Value in a Variable" walkthrough from docs/getting-started.md, kept runnable so an API change that breaks it is caught by CI's example-build step rather than silently going stale in the doc.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL