gplugin

package module
v0.0.0-...-9197009 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 5 Imported by: 1

README

English | 简体中文

gplugin

The plugin mechanism shared by the framework and the protocol libraries — two small layers for two different problems, no third-party dependencies.

type Module[C any] interface {  // assembly: a capability that registers into C
    ID() string
    Register(C) error
}

type Plugin[H any] interface {  // host extension: an extension of a live host H
    Name() string
    Start(H) error
    Stop() error
}

C and H are always types the caller declares. This package knows no protocol, no framework and no container — which is what lets an application framework and an MQTT broker share one mechanism without sharing any semantics.

1. Assembly — Set, Select, Assembly

An application declares the capabilities it wants; the set resolves the dependency closure, orders it topologically and reports a manifest.

set := gplugin.NewSet[*gable.Container]()
set.Add(logCapability)                                                     // id "log"
set.Add(gplugin.Static("cache", "1.0.0", cacheCapability))                  // DependsOn: log
set.Add(gplugin.Static("web", "1.0.0", webCapability))                      // DependsOn: cache

assembly, err := set.Select("web")   // resolves log -> cache -> web
if err != nil {
    return err                       // unknown capability, missing dep, cycle
}
defer assembly.Stop(ctx)             // reverse order, failures joined
if err := assembly.Start(ctx, container); err != nil {
    return err                       // already-started capabilities were rolled back
}
for _, entry := range assembly.Manifest() {
    log.Printf("capability %s@%s needs %v", entry.ID, entry.Version, entry.DependsOn)
}

Guarantees:

  • Deterministic order. Dependencies first; ties break on id, so every process start wires the same order and a rolling restart cannot reorder anything.
  • Failures are startup errors. An unknown capability, an unselected dependency and a cycle are reported, never silently dropped from the assembly.
  • No package-level registry, no panic. Wiring is explicit and testable in isolation; a duplicate id is an error the caller sees.
  • Optional lifecycle. A wiring-only capability does not implement Start/Stop; it is not listed as started and a rollback has nothing to stop for it.

From(Spec[C]) adapts a module whose own Register takes a concrete container type or that has its own lifecycle interface, without renaming anything in its repository.

2. Host extensions — Plugin, Registry

A library owns a host object and lets named extensions attach to it — and switch on and off at run time.

reg := gplugin.NewRegistry(broker, gplugin.WithObserver(myStatusLight{}))
reg.Add(authPlugin)                 // Name() "auth"
reg.Add(metricsPlugin)              // Name() "metrics", DependsOn: []string{"auth"}

if err := reg.Start("metrics"); err != nil {   // starts auth first
    return err                                 // auth/metrics were rolled back
}
defer reg.StopAll()                            // newest first, failures joined

statuses, err := reg.Snapshot()     // what is wired in, in start order
for _, s := range statuses {
    log.Printf("%s %s %s", s.ID, s.State, s.Err)
}

States. StateRegistered (known, never started), StateEnabled (running) and StateStopped (ran, then stopped). The three are deliberately distinct: "switched off" and "never switched on" are different facts, and an operator needs both.

Observers. Observer{StateChanged(Event)} receives every transition with its From, To and, on a failure, the Err — so an application can print what started, light a status indicator grey/green/red, or count failures, without polling. Observers run synchronously after the transition is recorded, with no lock held: they must not block and must not call back into Start/Stop.

Guarantees.

  • Order is dependency order, ties break on name, so a rolling restart cannot reorder.
  • Start pulls its closure. Start("web") starts the dependencies too, and skips the ones already running, so a control call is retryable.
  • Stop cascades. Stop("cache") stops everything that depends on cache, newest first; a plugin whose Stop failed stays enabled with the reason recorded rather than pretending it stopped.
  • Failures are visible. A failed Start rolls back only what that call started, records the reason, emits an event carrying it and returns an error naming the plugin.
  • Duplicate names, empty names and unregistered names are errors; nothing is silently smaller.

See examples/lifecycle for a runnable status table whose light is grey, green or red — the colour lives in the application, not here.

3. What this package does not do

  • No dynamic plugins. No .so, no download, no service discovery. A plugin is compiled into the binary; this package decides what is wired and in which order, not what exists at run time.
  • No protocol semantics. Hooks, events and lifecycles stay in the library that owns them.
  • No presentation. This package reports a State; whether it becomes a colour, a word or a metric is the application's decision.
  • No global state. Nothing is registered at package initialization.

4. Naming

gplugin sits below both the framework layer and the protocol libraries: it may be imported by any of them and depends on none of them. See the dependency rules in the consuming repository.

Version

Version = "0.0.0-dev" — pre-1.0, no promises of API stability yet.

License

MIT, see LICENSE.

Documentation

Overview

Package gplugin is the plugin mechanism shared by the framework and the protocol libraries.

It answers two different questions with two small layers, because they are two different problems:

  • Assembly (Set / Select / Assembly): an application declares the capabilities it wants ("log", "cache", "web"), the set resolves the dependency closure, orders it topologically and produces a manifest. Capabilities register values into a container of the caller's type C.
  • Host extensions (Plugin / Registry): a library owns a host object H (a broker, a server) and lets named plugins attach to it. A plugin starts against the host, stops in reverse order, and reports a state — registered, enabled or stopped — that an Observer can turn into whatever the application needs: a log line, a status light, a metric.

Neither layer knows any protocol, framework or container: C and H are always types the caller declares. That is what lets an application framework and an MQTT broker share one mechanism without sharing any semantics.

Deliberately not supported: dynamic plugins (no .so, no download, no service discovery). A plugin is compiled into the binary; the mechanism decides what is wired, in which order, and whether it is currently switched on — not what exists at run time.

Index

Constants

View Source
const Version = "0.0.0-dev"

Version is the semantic version of this library.

Variables

View Source
var ErrAlreadyRegistered = errors.New("gplugin: already registered")

ErrAlreadyRegistered means an id or name was registered twice.

View Source
var ErrPermanent = errors.New("gplugin: permanent error")

ErrPermanent marks wiring mistakes: retrying cannot help.

Functions

This section is empty.

Types

type Assembly

type Assembly[C any] struct {
	// contains filtered or unexported fields
}

Assembly is a selected, ordered set of capabilities.

func (*Assembly[C]) IDs

func (a *Assembly[C]) IDs() []string

IDs returns the capability ids in start order.

func (*Assembly[C]) Manifest

func (a *Assembly[C]) Manifest() []Entry

Manifest reports what the assembly contains, in start order.

func (*Assembly[C]) Register

func (a *Assembly[C]) Register(container C) error

Register runs every capability's registration in dependency order.

func (*Assembly[C]) Start

func (a *Assembly[C]) Start(ctx context.Context, container C) error

Start registers and then starts the capabilities in order.

A failure stops the ones already started, in reverse order, so a half-started application never keeps serving.

func (*Assembly[C]) Stop

func (a *Assembly[C]) Stop(ctx context.Context) error

Stop shuts the capabilities down in reverse order, joining every failure so one broken capability cannot mask the rest.

type Container

type Container interface {
	Set(key string, value any)
	Get(key string) (any, bool)
}

Container carries values between capabilities during Register.

It is the smallest untyped store a capability needs. A framework with its own container satisfies it without adapters, and an application without one can use a map.

type Dependent

type Dependent interface {
	DependsOn() []string
}

Dependent declares the ids that must come first, for both layers of this package.

type Entry

type Entry struct {
	ID        string
	Version   string
	DependsOn []string
}

Entry is one row of a manifest: what is wired, at which version, on what it depends.

type Event

type Event struct {
	ID   string
	From State
	To   State
	Err  error
}

Event is one observed state transition.

A failed transition reports From == To with Err set, so an observer can log the failure from the event alone instead of querying the registry again.

type Module

type Module[C any] interface {
	ID() string
	Register(C) error
}

Module is an assembled capability: it registers its values into the container and may have a lifecycle.

C is the container type of the caller. An application framework passes its own container type; a test passes a map; this package never assumes either.

func From

func From[C any](spec Spec[C]) (Module[C], error)

From turns a Spec into a Module.

No description is inferred: everything the manifest shows comes from the Spec, so an adapted module's version and dependencies must be stated rather than guessed. A spec without hooks becomes a wiring-only module — it does not satisfy Starter, so selection never lists it as started and a rollback has nothing to stop for it.

func Static

func Static[C any](id, version string, module Module[C]) Module[C]

Static wraps an already-built capability under an explicit id and version.

It exists so an application can adopt selection and manifests without rewriting its modules: the wrapped module keeps its own Register and lifecycle.

type Observer

type Observer interface {
	StateChanged(Event)
}

Observer receives every state transition.

It is how an application learns what happened without polling: printing what started, turning a status light grey, green or red, counting failures. Observers are called synchronously, after the transition is already recorded and with no lock held; an implementation must not block and must not call back into the registry's Start or Stop.

type Option

type Option func(*options)

Option configures a Registry.

func WithObserver

func WithObserver(observer Observer) Option

WithObserver registers an observer. Passing it several times registers several.

type Plugin

type Plugin[H any] interface {
	// Name identifies the plugin. It is unique within a Registry and is what status
	// output and error messages report.
	Name() string
	// Start attaches the plugin to the host. Returning an error leaves the plugin
	// stopped, records the reason and rolls back the plugins this call started.
	Start(H) error
	// Stop detaches the plugin. It is called in reverse start order and must tolerate
	// being called when Start never succeeded.
	Stop() error
}

Plugin is a named extension of a host object.

H is the host the plugin attaches to — a broker, a server, anything the owning library decides. Start receives the live host, so a plugin can register callbacks, start goroutines and publish through the host's own API; Stop must undo that.

type Registry

type Registry[H any] struct {
	// contains filtered or unexported fields
}

Registry is the ordered set of plugins a library offers and controls at run time.

Order comes from the optional Dependent interface and breaks ties on name, so a process start is deterministic. A duplicate or empty name is a wiring error rather than a silent replacement.

func NewRegistry

func NewRegistry[H any](host H, opts ...Option) *Registry[H]

NewRegistry returns a registry bound to host.

The host is fixed for the registry's lifetime: it is what every plugin's Start receives, whether the plugin starts at boot or later from a control call.

func (*Registry[H]) Add

func (r *Registry[H]) Add(plugin Plugin[H]) error

Add registers a plugin without starting it. Its state becomes StateRegistered.

func (*Registry[H]) Enabled

func (r *Registry[H]) Enabled() []string

Enabled lists the running plugin names, in start order.

func (*Registry[H]) Names

func (r *Registry[H]) Names() []string

Names lists the registered plugin names, sorted.

func (*Registry[H]) Snapshot

func (r *Registry[H]) Snapshot() ([]Status, error)

Snapshot reports every registered plugin in start order, with its state and the reason for its last failure.

It has no side effects: nothing is started and no event is emitted, so it is safe to call at any point — which is what makes "print what the process enabled" a one-liner for the application.

func (*Registry[H]) Start

func (r *Registry[H]) Start(names ...string) error

Start starts the named plugins and everything they depend on, in dependency order.

An already running plugin is left alone and produces no event: the operation is idempotent so a control call can be retried. A dependency that is not registered is an error, never a silently smaller start. When a plugin fails to start, the plugins this call started are stopped again in reverse order, the failure is recorded and observed, and the error names the plugin that failed.

func (*Registry[H]) StartAll

func (r *Registry[H]) StartAll() error

StartAll starts every registered plugin, in dependency order.

func (*Registry[H]) State

func (r *Registry[H]) State(name string) (State, bool)

State reports where one plugin stands. The second result is false when the name is not registered.

func (*Registry[H]) Stop

func (r *Registry[H]) Stop(names ...string) error

Stop stops the named plugins and every running plugin that depends on them, newest first.

Stopping something that is not running is a no-op. A failure does not abort the operation — the remaining plugins are still stopped and the failures are joined — because a half-stopped dependency graph is more dangerous than a logged error. A plugin whose Stop failed stays enabled with the reason recorded: it did not confirm that it stopped, and pretending otherwise would hide a half-detached plugin.

func (*Registry[H]) StopAll

func (r *Registry[H]) StopAll() error

StopAll stops every running plugin, newest first.

type Set

type Set[C any] struct {
	// contains filtered or unexported fields
}

Set is the registry of capabilities available to this binary.

It is explicit on purpose: no package-level registry, no panic on duplicates. Wiring must be testable in isolation, so a duplicate id is an error the caller sees.

func NewSet

func NewSet[C any]() *Set[C]

NewSet returns an empty set.

func (*Set[C]) Add

func (s *Set[C]) Add(module Module[C]) error

Add registers a capability.

A duplicate or empty id is a wiring error: silently replacing a capability would make the running binary differ from its manifest.

func (*Set[C]) IDs

func (s *Set[C]) IDs() []string

IDs lists the available capability ids, sorted.

func (*Set[C]) Select

func (s *Set[C]) Select(ids ...string) (*Assembly[C], error)

Select resolves the requested ids, pulls in their transitive dependencies and orders the result topologically.

Every failure is a startup error rather than a silently smaller assembly: an unknown capability, a dependency that was not selected, or a cycle all mean the binary does not match the declaration.

type Spec

type Spec[C any] struct {
	// ID is the capability name used for selection.
	ID string
	// Version is reported in the manifest; empty means the module does not declare one.
	Version string
	// DependsOn lists the capabilities that must start first.
	DependsOn []string
	// Register wires the capability. The container is this package's interface; a module
	// whose Register takes its own concrete type adapts it inside this closure.
	Register func(C) error
	// Start and Stop are optional; nil means the capability has no lifecycle.
	Start func(context.Context) error
	Stop  func(context.Context) error
}

Spec describes a capability by closures instead of a type.

It exists for modules that already have their own shape — a module whose Register takes a concrete container type, or that has its own lifecycle interface, cannot satisfy this package's interfaces by signature. Describing such a module with closures lets it join selection and manifests without renaming anything in its own repository.

type Starter

type Starter interface {
	Start(context.Context) error
}

Starter is the optional lifecycle hook of a Module.

type State

type State int

State is the lifecycle position of one entry.

The three values are what an operator needs to tell apart: known but never started, running, and stopped again. An application renders them however it likes — a colour, a word, a metric — because presentation is the application's decision, not this package's.

const (
	// StateRegistered means the entry is known and has not started yet.
	StateRegistered State = iota
	// StateEnabled means the entry is running.
	StateEnabled
	// StateStopped means the entry ran and was stopped. It is deliberately distinct from
	// StateRegistered: "switched off" and "never switched on" are different facts.
	StateStopped
)

func (State) String

func (s State) String() string

String returns the state as a lower-case word, for logs and status output.

type Status

type Status struct {
	ID        string
	Version   string
	DependsOn []string
	// State is the current position.
	State State
	// Err is the last start or stop failure, nil after a clean transition. It never
	// changes State: a plugin whose start failed is still not running, and pretending
	// otherwise would hide the reason.
	Err error
}

Status is one row of a snapshot: what is registered, and where it stands.

type Stopper

type Stopper interface {
	Stop(context.Context) error
}

Stopper is the reverse-order shutdown hook of a Module.

type Versioned

type Versioned interface {
	Version() string
}

Versioned reports a module's or plugin's version, shown in manifests.

It is optional so an existing type keeps working: an unversioned module reports an empty version, which is honest — nothing is ever invented.

Directories

Path Synopsis
examples
lifecycle command
Command lifecycle shows what an application does with gplugin's lifecycle hooks: start a few plugins, observe every transition, and print a status table whose light is grey (registered), green (enabled) or red (stopped).
Command lifecycle shows what an application does with gplugin's lifecycle hooks: start a few plugins, observe every transition, and print a status table whose light is grey (registered), green (enabled) or red (stopped).

Jump to

Keyboard shortcuts

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