For the complete documentation index, see llms.txt. This page is also available as Markdown.

Generic Types

Data Types and Interfaces Used in Ergo Framework

Ergo Framework uses several specialized types for identifying and addressing processes, nodes, and other entities in the system. Understanding these types is essential for working with the framework.

Identifiers and Names

gen.Atom

gen.Atom is a specialized string used for names - node names, process names, event names. While technically just a string, treating it as a distinct type allows the framework to optimize how these names are handled in the network stack.

Atoms appear in single quotes when printed:

fmt.Printf("%s", gen.Atom("myprocess"))
// Output: 'myprocess'

The network stack caches atoms and maps them to numeric IDs to reduce bandwidth when the same names appear repeatedly in messages.

gen.PID

A gen.PID uniquely identifies a process. It contains the node name where the process lives, a unique sequential ID, and a creation timestamp. The creation timestamp changes when a node restarts, allowing you to detect if you're talking to a reincarnation of a node rather than the original.

gen.PID values print with the node name hashed for brevity:

pid := gen.PID{Node: "node@localhost", ID: 1001, Creation: 1685523227}
fmt.Printf("%s", pid)
// Output: <2C323E75.0.1001>

The leading hash is a CRC32 of the node name, computed with the framework's own polynomial table rather than a standard one, so node@localhost always prints as 2C323E75. It keeps the printed form compact while staying distinct per node. The two numbers after it are the halves of the single ID field - high 32 bits, then low - so a small ID prints its high half as 0. Creation is not in the printed form at all.

gen.ProcessID

A gen.ProcessID identifies a process by its registered name rather than gen.PID. This is useful when you need to address a process but don't know its gen.PID, or when the gen.PID might change across restarts but the name remains constant.

The name is appended as-is here. Only gen.Atom quotes itself when printed.

gen.Ref

gen.Ref values are unique identifiers generated by nodes. They're used for correlating requests and responses in synchronous calls, and as tokens when registering events.

A gen.Ref is guaranteed unique within a node for its lifetime. The structure includes the node name, creation time, and a unique ID array.

References can also embed deadlines (stored in ID[2]) for timeout tracking. Recipients can check ref.IsAlive() to see if a request is still valid.

gen.Alias

gen.Alias is like a temporary gen.PID. Processes create aliases for additional addressability without registering names. Meta processes use aliases as their primary identifier.

Aliases use the same structure as references but print with a different prefix:

gen.Event

gen.Event values represent named message streams that processes can subscribe to. A gen.Event identifier consists of a name and the node where it's registered.

gen.Env

Environment variable names in Ergo are case-insensitive. The gen.Env type ensures this by converting to uppercase.

This allows processes to inherit environment variables from parents, leaders, and the node, with consistent naming regardless of how they're specified.

Errors

gen.Error

gen.Error is a wrapping error type used as a process exit reason. It carries a message, the causes it wraps - whose identity is observable through errors.Is and survives a trip across the network - and an optional captured mailbox.

  • Msg is the message text. gen.Errorf fills it with fmt.Errorf output, which means the causes' own text ends up inside it as well.

  • Wrapped holds the errors the %w substitutions referred to, in argument order. Reading them is described below.

  • Mailbox carries the captured mailbox of a panicked process for replay on supervisor restart. Excluded from network encoding via edf:"-", so it never crosses the wire.

Most user code never constructs *gen.Error directly. Use gen.Errorf instead, which mirrors fmt.Errorf and produces a *gen.Error with the wrap chain preserved:

gen.Errorf composes a value at the point of failure. It is not a way to declare a sentinel: package-level markers must be errors.New values, because a *gen.Error cannot be registered for the wire and loses its identity across a node hop. See Encoding Errors.

Wrapped is what tells apart the three basic shapes gen.Errorf can produce. A single cause is the common one:

Several causes at one level state independent facts about the same failure. This is a set rather than a chain: Wrapped[0] carries no special meaning, and the order follows the order of the arguments, not the position of the verbs in the format string:

Nesting happens when a *gen.Error is itself wrapped. Every level renders the inner text into its own Msg, so the text accumulates as the chain grows:

The shapes mix freely - a nested error may carry several causes of its own, at any depth - and the way to read them does not depend on which one you got. To ask whether a failure is of a given kind, use errors.Is. To pull out a value of a known type, use errors.As. Both visit every cause depth-first:

errors.Unwrap is not on that list. It calls only the Unwrap() error form, and gen.Error implements Unwrap() []error in order to carry several causes, so errors.Unwrap returns nil for every shape above - including a single cause, where nothing at the call site hints at the multi-error form. errors.Join behaves the same way for the same reason.

When the cause objects themselves are needed, assert to *gen.Error and read Wrapped, checking its length rather than indexing it straight away. gen.Errorf leaves the list empty whenever a %w argument was nil or was not an error, and in both cases the message still reads plausibly:

One thing not to do is recover a single level's own text by subtracting an inner error's text from the outer Msg. The wrap chain preserves identity, not the boundaries between the pieces of the message, and the format string those pieces were glued with is not part of any contract. When a receiver needs a value separately from the message, that value has to travel as a value: a registered marker if it is one of an enumerable set, a typed field beside the error for anything else.

The framework uses gen.Error in two places today:

  • Restart intensity exceeded. The supervisor's exit reason on overflow is built with gen.Errorf("supervisor restart intensity exceeded (max %d in %ds): %w: %w", intensity, period, gen.ErrExceeded, lastChildReason). Both gen.ErrExceeded and the original child reason are reachable via errors.Is. See Restart Intensity.

  • Mailbox preservation across panic restart. When a process spawned with Options.PreserveMailbox: true terminates abnormally, the runtime captures its mailbox into Mailbox and wraps the original reason in Wrapped. The supervising parent automatically picks it up and hands it to the restart. See Mailbox Preservation.

Core Interfaces

The framework defines several interfaces that provide access to different parts of the system.

gen.Node

The gen.Node interface is what you get when you start a node. It provides methods for spawning processes, managing applications, configuring networking, and controlling the node lifecycle.

Node operations can be called from any goroutine. The node manages processes but isn't itself an actor.

gen.Process

The gen.Process interface represents a running actor. It provides methods for sending messages, spawning children, linking to other processes, and managing the actor's lifecycle.

Actors typically embed this interface:

Process methods enforce state-based access control. Some operations are only available when the process is in certain states, ensuring actor model constraints are maintained.

gen.Network

The gen.Network interface manages distributed communication. It handles connections to remote nodes, routing, and service discovery.

Network transparency means sending messages to remote processes uses the same API as local processes. The gen.Network interface is where you configure how that transparency is achieved.

gen.RemoteNode

A gen.RemoteNode represents a connection to another Ergo node. Through this interface, you can spawn processes on the remote node or start applications there.

The remote operations require the target node to have enabled the corresponding permissions.

gen.Application

The gen.Application interface is the runtime view of a loaded application. It exposes application metadata (name, env, mode, state) and mutators for dynamic fields (tags, weight) that propagate to the registrar.

Application behaviors should embed app.Application rather than implementing the interface manually. The embed provides the framework entry point, default lifecycle callbacks, and the runtime binding. From inside callbacks the interface methods are available via the embed:

Processes within the application can access the same interface through Process.Application().

Type Design Philosophy

These types reflect a few design decisions worth understanding.

Hashing for readability - Node names are hashed in output to keep logs and traces readable while maintaining uniqueness. Full names can be verbose, especially in distributed systems with descriptive naming.

Separate types for concepts - gen.PID, gen.ProcessID, gen.Alias, and gen.Event are distinct types even though they could have been unified. Each represents a different way of addressing or identifying something in the system, and the type system helps keep these concepts clear.

Network-aware design - Many types include the node name. This isn't just for completeness - it's what enables network transparency. A gen.PID tells you not just which process, but which node, allowing the framework to route messages appropriately.

For detailed API documentation of these interfaces and types, refer to the godoc comments in the source code.

Last updated

Was this helpful?