server

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package server implements the Bolt v5 TCP server for the GoGraph Cypher engine. It handles connection acceptance, Bolt protocol negotiation, session lifecycle, and authentication.

Concurrency

Server is safe for concurrent use by multiple goroutines. Session and State are NOT safe for concurrent use; each connection owns exactly one Session.

Index

Examples

Constants

View Source
const (

	// DefaultMaxInFlightPerConnection is the default value applied to
	// Options.MaxInFlightPerConnection when the caller leaves it at
	// zero. The count tracks all Result cursors appended to the
	// in-progress explicit transaction since BEGIN (both open and
	// already-drained), so it bounds the total number of RUN statements
	// a client may issue without committing. The default of 1024 allows
	// any legitimate workload while still bounding pathological
	// RUN-loop attacks that grow tx.results without bound. Operators
	// that need a stricter limit may lower this value explicitly.
	DefaultMaxInFlightPerConnection = 1024

	// DefaultConnTimeout is the default value applied to Options.ConnTimeout
	// when the caller leaves it at zero. It is the per-message idle deadline
	// applied throughout the post-handshake message loop: the server resets it
	// before each read, so it bounds the time a connection may sit silent
	// between messages, not the total session duration. A non-zero default is
	// mandatory: with no deadline a client that completes the handshake but then
	// stops sending bytes would hold its connection slot and goroutine forever,
	// a Slowloris-style denial of service. The default of 30 s is generous
	// enough not to disturb a legitimate interactive session pausing between
	// queries while still reclaiming abandoned connections. Operators may set a
	// larger value for long-lived idle sessions or a smaller one to reclaim
	// connections more aggressively.
	DefaultConnTimeout = 30 * time.Second

	// DefaultTxTimeout is the default value applied to Options.DefaultTxTimeout
	// when the caller leaves it at zero. It bounds an explicit transaction
	// (opened by BEGIN) when the client supplies no tx_timeout of its own. A
	// finite default is mandatory: an explicit transaction holds the engine's
	// single-writer serialisation from BEGIN until COMMIT/ROLLBACK, so a client
	// that issues BEGIN and then stalls — never sending COMMIT, ROLLBACK, or even
	// RESET — would otherwise block every other writer on the server forever, a
	// liveness denial of service (#1302). The default of 30 s is generous enough
	// not to disturb a legitimate interactive transaction while still guaranteeing
	// the global write lock is reclaimed if a transaction is abandoned. Operators
	// may set a larger value for long-lived batch transactions or a smaller one
	// to reclaim the writer lock more aggressively; the per-statement
	// MaxStatementTimeout, when set, additionally clamps it.
	DefaultTxTimeout = 30 * time.Second

	// DefaultStatementTimeout is the default value applied to
	// Options.DefaultStatementTimeout when the caller leaves it at zero. It
	// bounds an AUTOCOMMIT statement (a bare RUN outside an explicit
	// transaction) when the client supplies no per-statement timeout of its
	// own. A finite default is mandatory for the same reason it is for explicit
	// transactions: an authenticated client can submit a statement whose runtime
	// is super-linear in the graph size yet whose result collapses to a single
	// row (a disconnected multi-pattern Cartesian product such as
	// `MATCH (a),(b),(c),(d),(e) RETURN count(*)`), so the result-row / byte caps
	// never fire and the statement pins a CPU core indefinitely. Explicit
	// transactions already receive DefaultTxTimeout unconditionally; without a
	// symmetric floor here, autocommit RUN was the sole unbounded-runtime path
	// under a default server configuration (#1828). The default of 30 s matches
	// DefaultTxTimeout; operators may set a larger value for long-running
	// analytical statements. A client-supplied `timeout` takes precedence, and
	// MaxStatementTimeout, when set, additionally clamps the effective value.
	DefaultStatementTimeout = 30 * time.Second

	// DefaultHandshakeTimeout is the deadline that bounds the unauthenticated
	// version-negotiation handshake — the cheapest phase for an attacker to
	// abuse, since it requires no valid protocol bytes (a client may open a
	// socket, send a single byte, and otherwise stall). The deadline is applied
	// to the connection before [proto.Negotiate] and cleared on success so it
	// never bleeds into normal operation. It is deliberately shorter than
	// DefaultConnTimeout: a legitimate client sends its 20-byte handshake
	// immediately, so 10 s is ample, while a stalled handshake is reclaimed
	// promptly. The handshake bound is fixed (not configurable via Options) to
	// keep the Options struct small; the package var handshakeTimeout is seeded
	// from this const and overridable only by tests.
	DefaultHandshakeTimeout = 10 * time.Second
)
View Source
const MaxInboundDecodeBytesUnlimited int64 = -1

MaxInboundDecodeBytesUnlimited is the explicit opt-out sentinel for Options.MaxInboundDecodeBytes: set the field to this value to disable the engine-wide inbound-decode ceiling entirely. It is distinct from the zero value, which selects the GOMEMLIMIT-derived default.

Variables

View Source
var (
	// ErrAuthFailed is returned when credentials are invalid.
	ErrAuthFailed = errors.New("bolt: authentication failed")

	// ErrSchemeUnknown is returned when the auth scheme is not supported.
	ErrSchemeUnknown = errors.New("bolt: unknown auth scheme")
)

Common auth errors.

View Source
var ErrInvalidTransition = errors.New("bolt: invalid state transition")

ErrInvalidTransition is returned by Transition when the given message type is not permitted in the current state.

View Source
var ErrNoAuthHandler = errors.New("bolt: no auth handler configured; set Options.Auth to a real AuthHandler, or to NoAuthHandler{} to run without authentication")

ErrNoAuthHandler is returned by NewServer when Options.Auth is nil. The server is secure-by-default: running without authentication must be an explicit opt-in, never an accidental default. Set Options.Auth to a real AuthHandler to require credentials, or set Options.Auth to a NoAuthHandler{} value to run the open-door handler on purpose (development and testing only).

Functions

func ConstantTimeValidate

func ConstantTimeValidate(wantPrincipal, wantCredentials string) func(principal, credentials string) error

ConstantTimeValidate returns a Validate function that accepts only the given principal and credentials, using crypto/subtle.ConstantTimeCompare for both comparisons. The comparison time is independent of the values being compared, eliminating timing side-channels.

Example:

handler := server.BasicAuthHandler{
    Validate: server.ConstantTimeValidate("alice", "correct-horse-battery-staple"),
}

func DefaultTLSConfig

func DefaultTLSConfig() *tls.Config

DefaultTLSConfig returns a hardened baseline tls.Config that operators should use as the STARTING POINT for the server's transport security.

The configuration sets a TLS 1.2 floor and a modern, AEAD-only cipher list for the TLS 1.2 handshake; TLS 1.3 is negotiated automatically when both peers support it (TLS 1.3 cipher suites are fixed by the Go runtime and are always safe, so they are not — and cannot be — listed here). No MaxVersion is set, so a 1.3-capable client always upgrades to 1.3.

The returned config is INCOMPLETE on its own: it carries no certificate. Callers MUST populate it with their own server identity before use, by setting one of:

Then pass the result in Options.TLSConfig.

The server does NOT impose this baseline automatically. It wraps whatever Options.TLSConfig the operator supplies verbatim: passing a nil TLSConfig keeps the existing behaviour of running PLAINTEXT TCP (no TLS at all). DefaultTLSConfig only provides and documents a safe default; it never overrides an operator-supplied config, so embedders are never surprised.

A fresh, independent config is returned on every call (no shared mutable global), so callers may freely mutate the result — adding Certificates, GetCertificate, client-auth policy, etc. — without aliasing another caller's configuration.

func ExtractBookmarks

func ExtractBookmarks(extra map[string]packstream.Value) []string

ExtractBookmarks returns the bookmark list from RUN/BEGIN extra metadata. It reads the "bookmarks" key, which may be a []packstream.Value of strings. Returns nil (not an error) when the key is absent or the value is not a list.

func FailureCode

func FailureCode(err error) string

FailureCode returns the Neo4j-style dot-delimited error code for err. Falls back to "Neo.DatabaseError.General.UnknownError" for unrecognised errors. The lookup uses errors.As and errors.Is so wrapped errors are matched correctly.

func NextBookmark

func NextBookmark() string

NextBookmark generates a new bookmark string for a committed transaction. The format is "FB:kXXXXXX" where XXXXXX is a monotonically increasing counter expressed as a zero-padded 8-digit hexadecimal value.

NextBookmark is safe for concurrent use.

func RoutingTable

func RoutingTable(addr string) map[string]packstream.Value

RoutingTable returns the single-host routing table for the server at addr. The TTL is hardcoded to 300 seconds. All three roles (WRITE, READ, ROUTE) point to the same single-host address.

The returned map matches the Bolt v5 routing table format expected inside a SUCCESS metadata "rt" key.

Types

type AuthHandler

type AuthHandler interface {
	// Authenticate validates the auth scheme, principal, and credentials.
	// On success it returns an Identity; on failure it returns a non-nil error.
	// Returning ErrAuthFailed causes the server to send a Failure with code
	// "Neo.ClientError.Security.Unauthorized". Returning ErrSchemeUnknown
	// causes a Failure with code "Neo.ClientError.Security.AuthProviderFailed".
	Authenticate(scheme, principal, credentials string) (Identity, error)
}

AuthHandler is the pluggable authentication interface. Implementations must be safe for concurrent use.

type BasicAuthHandler

type BasicAuthHandler struct {
	// Validate is called with the principal and credentials from the client.
	// It must return nil on success and a non-nil error on failure.
	// See the type-level documentation for timing side-channel guidance.
	Validate func(principal, credentials string) error
}

BasicAuthHandler validates credentials by delegating to a caller-supplied Validate function. The Validate function must return nil on success and a non-nil error (typically ErrAuthFailed) on failure.

Timing side-channels

Validate is called with the raw credential string from the client. If the implementation compares credentials with == or strings.Equal, an attacker can infer the correct value by measuring response latency (timing side-channel). Always use ConstantTimeValidate or crypto/subtle.ConstantTimeCompare for credential comparison:

handler := BasicAuthHandler{
    Validate: ConstantTimeValidate("alice", "correct-horse-battery-staple"),
}

Do not add rate-limiting or account-lockout logic inside Validate; place it in a middleware wrapping the AuthHandler instead so the Bolt server remains stateless per-connection.

BasicAuthHandler is safe for concurrent use as long as Validate is.

func (BasicAuthHandler) Authenticate

func (h BasicAuthHandler) Authenticate(scheme, principal, credentials string) (Identity, error)

Authenticate implements AuthHandler. It accepts only the "basic" scheme; any other scheme returns ErrSchemeUnknown. It calls h.Validate with the principal and credentials; if Validate returns a non-nil error, Authenticate returns ErrAuthFailed.

type CertReloader

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

CertReloader watches a (certificate, key) PEM file pair on disk and serves the most recent successfully loaded pair via the CertReloader.GetCertificate hook installable on tls.Config.GetCertificate.

The intent is operational: rotate the server's TLS material (e.g. cert-manager / Let's Encrypt) without restarting the Bolt server. The previous certificate stays in service until the new pair is fully validated and only then is the swap performed atomically via sync/atomic.Pointer. A reload that fails to parse leaves the live certificate untouched and surfaces the error via the provided OnError callback (or via stderr when nil).

CertReloader is safe for concurrent use; the hot path is a single atomic.Pointer.Load.

func NewCertReloader

func NewCertReloader(certPath, keyPath string, onError func(error)) (*CertReloader, error)

NewCertReloader loads the certificate + key from disk and returns a CertReloader holding the result. The initial load is mandatory: if the files cannot be read or parsed, NewCertReloader returns the error and the caller MUST fail fast (do not start the server with a broken TLS config).

onError is invoked when a later reload (triggered by Reload or by the optional Watch goroutine) fails to parse the new pair. A nil onError defaults to printing to stderr via fmt.Fprintln.

func (*CertReloader) GetCertificate

func (r *CertReloader) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate is the hook to install on tls.Config.GetCertificate. It returns the most recently loaded certificate. The signature matches the standard library's expectation so callers can do:

cfg := &tls.Config{GetCertificate: reloader.GetCertificate}

The returned *tls.Certificate is shared across all concurrent handshakes; callers must NOT mutate the returned value.

func (*CertReloader) Reload

func (r *CertReloader) Reload() error

Reload re-reads the certificate + key from disk and atomically swaps the live certificate when the parse succeeds. A parse failure leaves the live certificate untouched and returns the error so the caller (or the OnError callback installed via NewCertReloader) can record the incident.

func (*CertReloader) Watch

func (r *CertReloader) Watch(interval time.Duration, stop <-chan struct{})

Watch starts a background goroutine that polls the certificate and key files every interval and calls Reload when either has a fresh mtime. The goroutine exits when stop is closed. Watch returns immediately; pair it with sync.WaitGroup if the caller wants to block on shutdown.

Common usage:

stop := make(chan struct{})
go reloader.Watch(30*time.Second, stop)
defer close(stop)

Errors from Reload are surfaced via the onError callback installed at construction time; Watch itself never returns an error.

type Identity

type Identity struct {
	// Principal is the authenticated username or identifier.
	Principal string
}

Identity carries the authenticated principal's metadata after a successful authentication exchange.

type NoAuthHandler

type NoAuthHandler struct{}

NoAuthHandler accepts any credentials without validation. Suitable for development and testing only.

Because it admits every client, NoAuthHandler is never installed by default: the server is secure-by-default. To run without authentication an embedder must opt in explicitly by setting Options.Auth to a NoAuthHandler{} value. The explicit value is itself the opt-in — self-documenting at the call site and impossible to set by accident — and NewServer logs a loud warning when it sees one. Constructing a server with a nil Options.Auth fails closed with ErrNoAuthHandler. Never expose a NoAuthHandler-backed server on an untrusted network.

NoAuthHandler is safe for concurrent use.

func (NoAuthHandler) Authenticate

func (NoAuthHandler) Authenticate(_, principal, _ string) (Identity, error)

Authenticate implements AuthHandler. It always returns an Identity with the given principal and a nil error.

type Options

type Options struct {
	// MaxConnections is the upper bound on concurrent accepted connections.
	// Zero or negative values default to 1024.
	MaxConnections int

	// MaxMessageBytes caps the cumulative payload size of a single Bolt
	// message reassembled from per-chunk fragments. Zero or negative
	// values default to [proto.DefaultMaxMessageBytes] (16 MiB).
	// Bolt's wire format limits each chunk to 65535 bytes but the
	// chunk count is unbounded; this cap closes the Slowloris-style
	// DoS vector in which a malicious client streams non-zero chunks
	// indefinitely until the server OOMs.
	MaxMessageBytes int

	// MaxInboundDecodeBytes is the engine-wide ceiling, in bytes, on the total
	// decoded-collection memory in flight across ALL connections while messages
	// are being decoded. MaxMessageBytes bounds a single message and the decoder
	// bounds a single message's decoded collections, but without this aggregate
	// bound that per-message cap times MaxConnections is unbounded and reachable
	// before authentication — a memory-exhaustion DoS (CWE-770). When the pool is
	// drawn down, further inbound decodes fail fast with a retryable transient
	// error (backpressure) rather than allocating.
	//
	// Interpretation mirrors cypher.EngineOptions.GlobalMaxResultBytes:
	//   - 0 (the zero value) → derive from the Go soft memory limit: one eighth
	//     of GOMEMLIMIT when the operator has set one (results already claim half,
	//     and inbound decode is transient), else unlimited. This gives default-on
	//     protection precisely when a memory budget is declared, and never rejects
	//     a legitimate workload on a host whose memory the module cannot know.
	//   - [MaxInboundDecodeBytesUnlimited] (-1) → unlimited (explicit opt-out).
	//   - a positive value → used verbatim.
	//
	// GOMEMLIMIT alone cannot mitigate the DoS: in-flight decoded values are live,
	// non-collectable memory during decode, so the ceiling must gate before the
	// allocation.
	//
	// Scope: the ceiling bounds the concurrent DECODE-phase memory across
	// connections — the simultaneous-allocation vector that is the actual OOM
	// risk. A message's reserved bytes are returned as soon as it finishes
	// decoding, so this does not bound a message's lifetime while its handler
	// runs; a single connection may still transiently hold one decoded message
	// (bounded by MaxMessageBytes) during handling. This mirrors the result
	// ceiling's transient-vs-lifetime asymmetry.
	MaxInboundDecodeBytes int64

	// MaxInFlightPerConnection caps the total number of RUN statements
	// that may be issued within a single explicit transaction before
	// COMMIT or ROLLBACK. Zero or negative values default to
	// [DefaultMaxInFlightPerConnection] (1024). The count includes both
	// open (not yet fully PULL'd) and already-drained cursors
	// accumulated in tx.results since BEGIN; auto-commit cursors are
	// not counted (the Bolt v5 state machine already prevents two
	// concurrent auto-commit streams). The cap surfaces as a typed
	// Bolt FAILURE with code "Neo.ClientError.General.LimitExceeded".
	MaxInFlightPerConnection int

	// ConnTimeout is the per-connection idle read deadline applied throughout
	// the post-handshake message loop. Each time the server is about to read
	// the next message, the deadline is reset to now+ConnTimeout, so it bounds
	// the silent gap between messages rather than the total session duration.
	// Zero or negative values default to [DefaultConnTimeout] (30 s); a
	// non-zero deadline is always applied so an idle connection cannot hold its
	// slot and goroutine forever. Set a larger value for long-lived idle
	// sessions. The unauthenticated handshake phase is bounded separately and
	// is not configurable here; see [DefaultHandshakeTimeout].
	ConnTimeout time.Duration

	// MaxStatementTimeout is the server-side upper bound on per-statement
	// execution time. When a client supplies a timeout via the RUN or BEGIN
	// extra metadata, it is silently clamped to MaxStatementTimeout. When
	// a client supplies no timeout and MaxStatementTimeout is positive, the
	// server applies MaxStatementTimeout unconditionally. Zero means no
	// server-side cap (client controls its own timeout).
	MaxStatementTimeout time.Duration

	// DefaultTxTimeout is the bounded timeout applied to an explicit transaction
	// (opened by BEGIN) when the client supplies no tx_timeout. It guarantees the
	// engine's single-writer serialisation, which an explicit transaction holds
	// from BEGIN until COMMIT/ROLLBACK, can never be held indefinitely by an
	// abandoned transaction (#1302). Zero or negative values default to
	// [DefaultTxTimeout] (30 s). A client-supplied tx_timeout takes precedence;
	// MaxStatementTimeout, when set, additionally clamps the effective value. Set
	// a larger value for long-lived batch transactions.
	DefaultTxTimeout time.Duration

	// DefaultStatementTimeout is the bounded timeout applied to an AUTOCOMMIT
	// statement (a bare RUN outside an explicit transaction) when the client
	// supplies no per-statement `timeout` of its own. It is the autocommit
	// counterpart of DefaultTxTimeout: without it, a default-configured server
	// (MaxStatementTimeout left at zero) has no wall-clock bound on an
	// autocommit statement, so an authenticated client can pin a CPU core
	// indefinitely with a super-linear-runtime / single-row-result query whose
	// result-row and byte caps never fire (#1828). Zero or negative values
	// default to [DefaultStatementTimeout] (30 s). A client-supplied `timeout`
	// takes precedence; MaxStatementTimeout, when set, additionally clamps the
	// effective value. Set a larger value for long-running analytical statements.
	DefaultStatementTimeout time.Duration

	// TLSConfig, when non-nil, wraps accepted connections with TLS using
	// the given configuration verbatim. nil means plain TCP (no TLS).
	//
	// The server applies no MinVersion or cipher policy of its own: whatever
	// config is supplied here is used as-is. To start from a hardened baseline
	// (TLS 1.2 floor, modern AEAD/ECDHE cipher list), begin with
	// [DefaultTLSConfig] and add your own Certificates or GetCertificate before
	// assigning it here.
	TLSConfig *tls.Config

	// Auth is the authentication handler invoked during HELLO/LOGON. It is
	// the security boundary of the server: every client must satisfy it
	// before any Cypher statement executes.
	//
	// Auth must be set; leave it nil and [NewServer] returns
	// [ErrNoAuthHandler]. The server is secure-by-default: a nil Auth is NOT
	// silently replaced with an open, accept-everyone handler, so a careless
	// embedder writing Options{} cannot accidentally expose an
	// unauthenticated server. To enforce credentials, set Auth to a real
	// [AuthHandler] such as [BasicAuthHandler]. To run without authentication
	// (development or testing only) set Auth: [NoAuthHandler]{} explicitly:
	// the explicit NoAuthHandler value is itself the opt-in, it is
	// self-documenting at the call site, and it is impossible to set by
	// accident. In that case [NewServer] still emits a loud warning.
	Auth AuthHandler

	// Logger is the structured logger for server events. When nil, the
	// default slog handler is used.
	Logger *slog.Logger

	// Closer, when non-nil, is the store-level teardown owner for the
	// durability stack backing this server's engine — typically a
	// *[github.com/FlavioCFOliveira/GoGraph/store.DB] bundling the WAL writer
	// and the background checkpointer. The server closes it AFTER it has
	// drained every active connection, so it runs the one crash-safe teardown
	// order (stop the checkpoint goroutine, then close the WAL) only once no
	// in-flight transaction can still be writing. Both documented stop
	// mechanisms reach that teardown: [Server.Shutdown] closes it on its
	// drain-success branch, and [Server.Serve] closes it on its own exit path
	// once its connection drain completes (e.g. when the Serve context is
	// cancelled). The close is guarded by a [sync.Once] inside the server, so
	// the closer's Close runs exactly once regardless of which path wins or
	// whether both run; it need not be idempotent itself. Leave it nil for a
	// store-less engine or when the embedder tears the durability stack down
	// itself; the server then closes nothing beyond its connections.
	Closer io.Closer
}

Options configures a Server. It is a plain configuration value read once by NewServer; it is safe for concurrent read use once constructed, but must not be mutated after being passed to NewServer. The referenced TLSConfig, Auth, Logger, and Closer carry their own concurrency contracts.

type Server

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

Server is the Bolt v5 TCP server. It accepts connections from a net.Listener, negotiates the protocol version, and runs the Bolt message loop on each connection.

Server is safe for concurrent use by multiple goroutines.

func NewServer

func NewServer(eng *cypher.Engine, opts Options) (*Server, error)

NewServer creates a Server backed by eng. Zero-value Options fields are filled with sensible defaults.

NewServer is secure-by-default: it never silently installs an accept-everyone authentication handler. If Options.Auth is nil it fails closed and returns ErrNoAuthHandler so that an unauthenticated server is never started by accident. To run without authentication on purpose (development or testing), set Options.Auth to a NoAuthHandler{} value explicitly: NewServer then admits every client and logs a loud warning that the operator has knowingly disabled authentication. The explicit NoAuthHandler value is itself the opt-in — self-documenting at the call site and impossible to set by accident. When Options.Auth is any other (real) handler it is used as-is.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context, addr string) error

ListenAndServe creates a TCP listener on addr and calls Serve. It blocks until the server stops. The listener is closed when Serve returns.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, ln net.Listener) (err error)

Serve accepts connections from ln until ctx is cancelled or Shutdown is called. It blocks until all active connections have closed. The provided ln is closed by Serve when the accept loop exits.

Once every connection has drained, Serve also closes the owned Options.Closer (the store-level teardown owner for the durability stack, typically a *github.com/FlavioCFOliveira/GoGraph/store.DB), so stopping the server by cancelling ctx tears the WAL/checkpoint stack down in its crash-safe order exactly as Server.Shutdown does — no checkpoint goroutine or WAL handle outlives Serve. The close happens strictly after the drain, so it can never race an in-flight write, and it is once-guarded, so a subsequent (or concurrent) Shutdown does not close the closer again. A failed close is returned (joined with any accept error) rather than swallowed. After Serve returns, the durability stack is closed: the Server must not be reused to serve writes again.

Example

ExampleServer_Serve starts a Bolt server backed by an in-memory graph, connects a Bolt client, and runs a query over the session. The listener binds to 127.0.0.1:0 so the OS assigns a free port. Teardown closes the client first, then cancels Serve and waits for it to drain every connection goroutine — leaving no leaked goroutine behind.

The network round-trip is non-deterministic in timing, so the example asserts the deterministic query result rather than any wire-level output.

package main

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/FlavioCFOliveira/GoGraph/bolt/server"
	"github.com/FlavioCFOliveira/GoGraph/cypher"
	"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
	"github.com/FlavioCFOliveira/GoGraph/graph/lpg"

	"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

func main() {
	// Engine over an empty in-memory labelled property graph.
	g := lpg.New[string, float64](adjlist.Config{})
	eng := cypher.NewEngine(g)

	// The explicit NoAuthHandler{} value is the opt-in that lets this example
	// run without credentials; the server is secure-by-default and otherwise
	// refuses to start with a nil Auth handler.
	srv, err := server.NewServer(eng, server.Options{ConnTimeout: 5 * time.Second, Auth: server.NoAuthHandler{}})
	if err != nil {
		fmt.Println("new server:", err)
		return
	}

	// Ephemeral port; ln.Addr() reveals the chosen port for the client.
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		fmt.Println("listen:", err)
		return
	}
	addr := ln.Addr().String()

	ctx, cancel := context.WithCancel(context.Background())
	serveErr := make(chan error, 1)
	go func() { serveErr <- srv.Serve(ctx, ln) }()

	// Connect a Bolt client and run a trivial read query.
	driver, err := neo4j.NewDriverWithContext("bolt://"+addr, neo4j.NoAuth())
	if err != nil {
		fmt.Println("driver:", err)
		cancel()
		<-serveErr
		return
	}

	sess := driver.NewSession(ctx, neo4j.SessionConfig{})
	result, err := sess.Run(ctx, "RETURN 1 AS n", nil)
	if err != nil {
		fmt.Println("run:", err)
	} else if rec, err := result.Single(ctx); err != nil {
		fmt.Println("single:", err)
	} else {
		n, _ := rec.Get("n")
		fmt.Println("n =", n)
	}
	_ = sess.Close(ctx)

	// Clean shutdown: close the client so server-side connection goroutines
	// observe EOF, then cancel Serve and wait for it to return. Serve only
	// returns after every connection goroutine has finished.
	_ = driver.Close(ctx)
	cancel()
	<-serveErr
}
Output:
n = 1

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown gracefully stops accepting new connections and waits for active connections to finish. If connections do not finish within 30 seconds, it closes the listener forcefully and returns an error.

When the server was constructed with Options.Closer (the store-level teardown owner for the durability stack, typically a *github.com/FlavioCFOliveira/GoGraph/store.DB), Shutdown closes it AFTER every active connection has drained — so the WAL/checkpoint teardown runs in its crash-safe order only once no in-flight transaction can still be writing. Closing it before the drain could let a still-executing write race the WAL close. The closer is therefore NOT torn down on the timeout or ctx-cancellation paths: an undrained connection may still hold a transaction, so tearing the WAL down underneath it is exactly what must be avoided; in those cases the connections are abandoned. A still-running Server.Serve remains blocked on the same drain, and when the abandoned connections do eventually finish (idle timeout, transaction reap, client exit), Serve's own exit path performs the post-drain close — so the closer is torn down as soon as a full drain truly completes, and is left for process exit only if it never does. The close is once-guarded: whichever of Serve or Shutdown drains first runs it, and the other observes the same cached result, so the closer is never closed twice (including on a double Shutdown). A failed WAL close is surfaced rather than swallowed.

type Session

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

Session holds all per-connection state for a single Bolt v5 client connection.

Session is NOT safe for concurrent use. Each accepted TCP connection owns exactly one Session, and the message loop is single-threaded per connection.

func (*Session) Close added in v0.2.0

func (s *Session) Close()

Close tears the session down on connection teardown: it drains any open cursor and rolls back any open explicit transaction so the engine writer serialisation is released immediately rather than lingering until the GC finalises the leaked Result/transaction (#1309). It is safe to call exactly once from the connection handler's deferred cleanup on every exit path (clean close, read or write error, panic). Idempotent: a second call, or a call on a session with no open transaction, is a no-op.

An explicit transaction still open at this point is an abnormal disconnect — the client dropped the connection (or hit an idle timeout, or the handler panicked) without sending COMMIT, ROLLBACK, or RESET. Close counts it as [metricTxAbandoned] before the rollback so an operator can distinguish a leaked transaction reclaimed here from one ended in an orderly way. This is the only site that emits tx.abandoned: a FAILED-transition reclaim (#1312) goes through [Session.abortTx] directly and is an in-session state change, not a disconnect, so it is not counted abandoned.

func (*Session) HandleMessage

func (s *Session) HandleMessage(ctx context.Context, msg any) ([]any, error)

HandleMessage dispatches msg to the correct per-state handler and returns the response messages to send to the client.

On an illegal state transition or internal error the session moves to FAILED and HandleMessage returns a single *proto.Failure response. The caller is responsible for encoding and sending all returned messages.

When a record sink is installed (see [Session.setRecordSink]), the RECORD messages of a PULL are written through the sink as the cursor is iterated and are NOT part of the returned slice, which then carries only the trailing SUCCESS or FAILURE. A sink write failure is surfaced as an error wrapping [errRecordWrite]: the connection framing is unrecoverable and the caller must tear the connection down without writing anything further.

type State

type State uint8

State represents the Bolt v5 per-connection protocol state machine state.

const (
	// StateConnected is the initial state: TCP connection established, no
	// protocol negotiation has occurred yet.
	StateConnected State = iota

	// StateNegotiation is reached after version negotiation; the server awaits
	// the client's HELLO message.
	StateNegotiation

	// StateAuthentication is the pre-LOGON state reached after a successful
	// credential-less HELLO on Bolt >= 5.1. Bolt 5.1 split authentication out of
	// HELLO into a dedicated LOGON message, so a 5.1+ client sends a HELLO
	// carrying only driver metadata and then a LOGON carrying the credentials.
	// In this state the connection is not yet authenticated; only LOGON, LOGOFF,
	// RESET, and GOODBYE are legal, and a successful LOGON transitions to
	// StateReady. On Bolt <= 5.0 (and the white-box tests, which run at the
	// zero-value version) HELLO authenticates inline and goes straight to
	// StateReady, so this state is never entered. (task #1470)
	StateAuthentication

	// StateReady is the idle state after a successful HELLO or after a result
	// set has been fully consumed, committed, or rolled back.
	StateReady

	// StateStreaming is active when a query has been run (auto-commit) and
	// records are available to pull.
	StateStreaming

	// StateTxReady is reached after BEGIN; the server awaits RUN, COMMIT, or
	// ROLLBACK within an explicit transaction.
	StateTxReady

	// StateTxStreaming is active when a query has been run inside an explicit
	// transaction and records are available to pull.
	StateTxStreaming

	// StateFailed is entered when a request fails; the server ignores further
	// requests until RESET is received.
	StateFailed

	// StateDefunct is the terminal state: the connection is closed and no
	// further messages are processed.
	StateDefunct
)

func HelloTransition added in v0.3.0

func HelloTransition(current State, ver proto.Version, success bool) (State, error)

HelloTransition computes the next state for a successful HELLO given the negotiated Bolt version. It is the version-aware variant of the NEGOTIATION→HELLO branch of Transition:

  • Bolt <= 5.0 (and the zero-value version used by direct white-box tests): HELLO authenticates inline and advances straight to StateReady.
  • Bolt >= 5.1: HELLO is credential-less by spec, so a successful HELLO advances to the pre-LOGON StateAuthentication, from which a successful LOGON reaches StateReady.

It is only valid in StateNegotiation; any other current state, or a failed HELLO, is delegated to Transition, which returns StateFailed (with ErrInvalidTransition for an illegal current state). (task #1470)

func StreamingTransition

func StreamingTransition(current State, hasMore bool) (State, error)

StreamingTransition is a variant of Transition for PULL in STREAMING or TX_STREAMING states when there are more records to deliver (has_more=true). In that case the connection remains in the same streaming state instead of returning to READY/TX_READY.

func Transition

func Transition(current State, msg any, success bool) (State, error)

Transition computes the next state given the current state, the incoming message, and whether the operation succeeded.

msg must be one of the pointer types from the proto package (e.g. *proto.Run, *proto.Pull, etc.). success indicates whether the server-side operation succeeded; on failure the next state is StateFailed (unless the transition itself is illegal).

Returns (StateFailed, ErrInvalidTransition) for illegal state/message combinations.

func (State) String

func (s State) String() string

String returns the name of the state for logging and diagnostics.

type Tx

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

Tx wraps an engine-level explicit transaction (cypher.ExplicitTx) for a single Bolt transaction opened by a BEGIN message. Every RUN issued between BEGIN and COMMIT/ROLLBACK executes against the SAME underlying engine transaction, so the statements are atomic together: COMMIT makes them durable and visible as one unit, ROLLBACK unwinds all of them (#1280). This replaces the previous behaviour in which each RUN opened and committed its own autocommit transaction and ROLLBACK undid nothing.

Tx is NOT safe for concurrent use; it is owned by a single Session whose message loop is single-threaded per connection.

func (*Tx) Commit

func (tx *Tx) Commit() error

Commit makes every statement issued since BEGIN durable and visible as one atomic unit, then releases the transaction's resources. The engine fsyncs the WAL exactly once for the whole transaction (WAL-backed) and commits the secondary-index buffer; on a store-less engine the writes are already visible and the index buffer is finalised. The writer serialisation is released.

Open result cursors are closed first (releasing their iterator state); the commit decision itself is made by the engine transaction, not by the cursors.

func (*Tx) Rollback

func (tx *Tx) Rollback() error

Rollback unwinds every statement issued since BEGIN — restoring the in-memory graph to its pre-transaction state via the engine's accumulated undo log, and (WAL-backed) discarding the WAL transaction so a fresh recovery observes none of the writes — then releases the transaction's resources. It is best-effort and always releases the writer serialisation, even if an inverse operation fails.

func (*Tx) Run

func (tx *Tx) Run(query string, params map[string]any) (*cypher.Result, error)

Run executes query inside the transaction WITHOUT committing, buffers the result cursor, and returns it to the caller for streaming. The statement's writes accumulate in the engine transaction and become durable/visible only on Commit.

Runtime pipeline errors (where the query compiled and executed under the visibility barrier but the execution pipeline failed — e.g. a constraint violation or a type error mid-pipeline) are wrapped in a zero-row cypher.Result whose Err() carries the error. This preserves the Bolt v5 state-machine contract: the session stays in TX_STREAMING so the driver can drain the cursor via PULL (where the FAILURE surfaces), rather than receiving a FAILURE directly from RUN. Build-phase errors (context cancellation, parse/sema/plan failures, DDL rejection) are propagated as a non-nil error return so the server enters FAILED at RUN time.

Jump to

Keyboard shortcuts

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