Skip to content
Merged
7 changes: 7 additions & 0 deletions pluginkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
Releases of this module are tagged `pluginkit/vX.Y.Z`. Releases up to
0.5.1 were tagged `vX.Y.Z` in `github.com/gopherium/pluginkit`.

## Unreleased

- `PluginStore`, with `Records`, `Activate` and `Pause`, is an application's record of each plugin's state: not installed, inactive, active or paused, with the text of the failure that paused it.
- `NewBoot`, `Register`, `Boot.Pause` and `Boot.Started` carry one boot from the records to the start: a plugin seen for the first time is recorded active, inactive and not installed plugins are never registered, a plugin that fails to register, migrate or start is paused, and a paused plugin that starts again is active with no error.
- A boot whose context ends pauses nothing, and a store read or write that fails is returned to the application.
- `pluginstoretest.Run` checks any `PluginStore` against the contract, and `pluginstoretest.NewMemory` is a store in memory that keeps it.

## 0.12.0 - 2026-11-07

- `RegisterEach` calls each `Registration` with the dependencies `depsFor` builds for its id, each inside its own recover, goes on past an error or a panic, and answers a `RegisterOutcome` naming each failure at the new `StepRegister`.
Expand Down
106 changes: 106 additions & 0 deletions pluginkit/boot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0

package pluginkit

import (
"context"
"fmt"
)

// Boot is one start of the application's plugins, from their records to their start.
type Boot struct {
store PluginStore
records map[string]PluginRecord
}

// NewBoot reads every record of store once.
func NewBoot(ctx context.Context, store PluginStore) (*Boot, error) {
var records map[string]PluginRecord
err := guarded(ctx, "read the plugin records", func(ctx context.Context) error {
var readErr error
records, readErr = store.Records(ctx)
return readErr
})
if err != nil {
return nil, err
}
return &Boot{store: store, records: records}, nil
}

// Register records each new entry active, registers the active and paused ones, and pauses each that fails.
func Register[D, P any](ctx context.Context, boot *Boot, entries []Registration[D, P],
depsFor func(id string) D) (RegisterOutcome[P], error) {
wanted, err := admitted(ctx, boot, entries)
if err != nil {
return RegisterOutcome[P]{}, err
}
outcome := RegisterEach(wanted, depsFor)
return outcome, boot.pauseEach(ctx, outcome.Failed)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
}

// admitted returns the entries boot registers, in order, recording each new one active.
func admitted[D, P any](ctx context.Context, boot *Boot, entries []Registration[D, P]) ([]Registration[D, P], error) {
var wanted []Registration[D, P]
for _, entry := range entries {
registers, err := boot.admit(ctx, entry.ID)
if err != nil {
return nil, err
}
if registers {
wanted = append(wanted, entry)
}
}
return wanted, nil
}

// pauseEach pauses each failed plugin but one whose migration record failed, and none once ctx ended.
func (b *Boot) pauseEach(ctx context.Context, failures []Failure) error {
if err := ctx.Err(); err != nil {
return fmt.Errorf("pluginkit: the boot's context ended: %w", err)
}
for _, failure := range failures {
if failure.Step == StepRecord {
continue
}
if err := b.Pause(ctx, failure.ID, failure.Err); err != nil {
return err
}
}
return nil
}

// admit records the plugin id active when boot has never seen it, and reports whether a boot registers it.
func (b *Boot) admit(ctx context.Context, id string) (bool, error) {
if record, known := b.records[id]; known {
return record.State == PluginActive || record.State == PluginPaused, nil
}
return true, b.activate(ctx, id)
}

// Pause pauses the plugin id for failure, a step that failed or a reason of the application's own.
func (b *Boot) Pause(ctx context.Context, id string, failure error) error {
return guarded(ctx, "pause "+id, func(ctx context.Context) error {
return b.store.Pause(ctx, id, failure.Error())
})
}

// activate marks the plugin id active and clears its error.
func (b *Boot) activate(ctx context.Context, id string) error {
return guarded(ctx, "activate "+id, func(ctx context.Context) error { return b.store.Activate(ctx, id) })
}

// Started pauses each plugin that failed to migrate or start, and activates each paused plugin that started.
func (b *Boot) Started(ctx context.Context, outcome StartOutcome) error {
if err := b.pauseEach(ctx, outcome.Failed); err != nil {
return err
}
for _, started := range outcome.Started {
if b.records[started.ID].State != PluginPaused {
continue
}
if err := b.activate(ctx, started.ID); err != nil {
return err
}
}
return nil
}
Loading
Loading