Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion PLUGIN_DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,24 @@ Test environment variables and YAML configuration:

```bash
DINGO_DATABASE_BLOB_MYPLUGIN_OPTION1=value ./dingo --blob myplugin
```
```

## Programmatic Option Overrides (for tests)

When writing tests or programmatically constructing database instances you can override plugin options
without importing plugin implementation packages directly by using the plugin registry helper:

```go
// Set data-dir for the blob plugin to a per-test temp directory
plugin.SetPluginOption(plugin.PluginTypeBlob, "badger", "data-dir", t.TempDir())

// Set data-dir for the metadata plugin
plugin.SetPluginOption(plugin.PluginTypeMetadata, "sqlite", "data-dir", t.TempDir())
```

The helper sets the plugin option's destination variable in the registry before plugin instantiation.
If the requested option is not defined by the targeted plugin the call is non-fatal and returns nil,
allowing tests to run regardless of which plugin implementation is selected.

Using `t.TempDir()` guarantees each test uses its own on-disk path and prevents concurrent tests from
colliding on shared directories (for example the default `.dingo` Badger directory).
6 changes: 3 additions & 3 deletions connmanager/connection_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const (

type connectionInfo struct {
conn *ouroboros.Connection
isInbound bool
peerAddr string
isInbound bool
}

type peerConnectionState struct {
Expand All @@ -47,19 +47,19 @@ type peerConnectionState struct {

type ConnectionManager struct {
connections map[ouroboros.ConnectionId]*connectionInfo
metrics *connectionManagerMetrics
config ConnectionManagerConfig
connectionsMutex sync.Mutex
metrics *connectionManagerMetrics
}

type ConnectionManagerConfig struct {
PromRegistry prometheus.Registerer
Logger *slog.Logger
EventBus *event.EventBus
ConnClosedFunc ConnectionManagerConnClosedFunc
Listeners []ListenerConfig
OutboundConnOpts []ouroboros.ConnectionOptionFunc
OutboundSourcePort uint
PromRegistry prometheus.Registerer
}

type connectionManagerMetrics struct {
Expand Down
25 changes: 24 additions & 1 deletion database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io"
"log/slog"

"github.com/blinklabs-io/dingo/database/plugin"
"github.com/blinklabs-io/dingo/database/plugin/blob"
"github.com/blinklabs-io/dingo/database/plugin/metadata"
"github.com/prometheus/client_golang/prometheus"
Expand Down Expand Up @@ -119,6 +120,7 @@ func (d *Database) init() error {
func New(
config *Config,
) (*Database, error) {
var err error
if config == nil {
config = DefaultConfig
}
Expand All @@ -132,10 +134,31 @@ func New(
if configCopy.MetadataPlugin == "" {
configCopy.MetadataPlugin = DefaultConfig.MetadataPlugin
}
// DataDir defaulting behavior:
// Handle DataDir configuration for plugins:
// - nil config → DefaultConfig.DataDir (".dingo" for persistence)
// - empty DataDir → in-memory storage
// - non-empty DataDir → persistent storage at specified path
// NOTE: SetPluginOption mutates global plugin state, so DataDir is effectively
// process-wide and not concurrency-safe. Multiple Database instances in the
// same process will share and overwrite these options.
err = plugin.SetPluginOption(
plugin.PluginTypeBlob,
configCopy.BlobPlugin,
"data-dir",
configCopy.DataDir,
)
if err != nil {
return nil, err
}
err = plugin.SetPluginOption(
plugin.PluginTypeMetadata,
configCopy.MetadataPlugin,
"data-dir",
configCopy.DataDir,
)
if err != nil {
return nil, err
}
blobDb, err := blob.New(
configCopy.BlobPlugin,
)
Expand Down
2 changes: 1 addition & 1 deletion database/plugin/blob/aws/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ type BlobStoreS3 struct {
startupCtx context.Context
logger *S3Logger
client *s3.Client
startupCancel context.CancelFunc
bucket string
prefix string
region string
startupCancel context.CancelFunc
timeout time.Duration
}

Expand Down
10 changes: 5 additions & 5 deletions database/plugin/blob/badger/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ type BlobStoreBadger struct {
promRegistry prometheus.Registerer
db *badger.DB
logger *slog.Logger
dataDir string
gcEnabled bool
blockCacheSize uint64
indexCacheSize uint64
gcTicker *time.Ticker
gcStopCh chan struct{}
dataDir string
gcWg sync.WaitGroup
blockCacheSize uint64
indexCacheSize uint64
gcEnabled bool
}

// New creates a new database
Expand Down Expand Up @@ -76,7 +76,7 @@ func New(opts ...BlobStoreBadgerOptionFunc) (*BlobStoreBadger, error) {
return nil, fmt.Errorf("failed to read data dir: %w", err)
}
// Create data directory
if err := os.MkdirAll(db.dataDir, fs.ModePerm); err != nil {
if err := os.MkdirAll(db.dataDir, 0o755); err != nil {
return nil, fmt.Errorf("failed to create data dir: %w", err)
}
}
Expand Down
3 changes: 2 additions & 1 deletion database/plugin/blob/badger/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func initCmdlineOptions() {
cmdlineOptions.blockCacheSize = DefaultBlockCacheSize
cmdlineOptions.indexCacheSize = DefaultIndexCacheSize
cmdlineOptions.gcEnabled = true
cmdlineOptions.dataDir = ".dingo"
}

// Register plugin
Expand All @@ -59,7 +60,7 @@ func init() {
Name: "data-dir",
Type: plugin.PluginOptionTypeString,
Description: "Data directory for badger storage",
DefaultValue: "",
DefaultValue: ".dingo",
Dest: &(cmdlineOptions.dataDir),
},
{
Expand Down
2 changes: 1 addition & 1 deletion database/plugin/blob/gcs/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ func TestCredentialValidation(t *testing.T) {
tests := []struct {
name string
credentialsFile string
expectError bool
errorMessage string
expectError bool
}{
{
name: "valid credentials file",
Expand Down
4 changes: 2 additions & 2 deletions database/plugin/metadata/sqlite/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ type MetadataStoreSqlite struct {
db *gorm.DB
logger *slog.Logger
timerVacuum *time.Timer
timerMutex sync.Mutex
dataDir string
timerMutex sync.Mutex
closed bool
}

Expand Down Expand Up @@ -159,7 +159,7 @@ func (d *MetadataStoreSqlite) Start() error {
return fmt.Errorf("failed to read data dir: %w", err)
}
// Create data directory
if err := os.MkdirAll(d.dataDir, fs.ModePerm); err != nil {
if err := os.MkdirAll(d.dataDir, 0o755); err != nil {
return fmt.Errorf("failed to create data dir: %w", err)
}
}
Expand Down
4 changes: 2 additions & 2 deletions database/plugin/metadata/sqlite/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var (
func initCmdlineOptions() {
cmdlineOptionsMutex.Lock()
defer cmdlineOptionsMutex.Unlock()
cmdlineOptions.dataDir = ""
cmdlineOptions.dataDir = ".dingo"
}

// Register plugin
Expand All @@ -48,7 +48,7 @@ func init() {
Name: "data-dir",
Type: plugin.PluginOptionTypeString,
Description: "Data directory for sqlite storage",
DefaultValue: "",
DefaultValue: ".dingo",
Dest: &(cmdlineOptions.dataDir),
},
},
Expand Down
170 changes: 170 additions & 0 deletions database/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,173 @@ func StartPlugin(pluginType PluginType, pluginName string) (Plugin, error) {

return p, nil
}

// SetPluginOption sets the value of a named option for a plugin entry. This
// is used by callers that need to programmatically override plugin defaults
// (for example to set data-dir before starting a plugin). It returns an error
// if the plugin or option is not found or if the value type is incompatible.
// NOTE: This function accesses the global pluginEntries slice without
// synchronization. It should only be called during initialization or in
// single-threaded contexts to avoid race conditions.
// NOTE: This function writes directly to plugin option destinations (e.g.,
// cmdlineOptions fields) without acquiring the plugin's cmdlineOptionsMutex.
// It must be called before any plugin instantiation to avoid data races with
// concurrent reads in NewFromCmdlineOptions.
func SetPluginOption(
pluginType PluginType,
pluginName string,
optionName string,
value any,
) error {
for i := range pluginEntries {
p := &pluginEntries[i]
if p.Type != pluginType || p.Name != pluginName {
continue
}
for _, opt := range p.Options {
if opt.Name != optionName {
continue
}
// Perform a type-checked assignment into the Dest pointer
switch opt.Type {
case PluginOptionTypeString:
v, ok := value.(string)
if !ok {
return fmt.Errorf(
"invalid type for option %s: expected string",
optionName,
)
}
if opt.Dest == nil {
return fmt.Errorf(
"nil destination for option %s",
optionName,
)
}
dest, ok := opt.Dest.(*string)
if !ok {
return fmt.Errorf(
"invalid destination type for option %s: expected *string",
optionName,
)
}
if dest == nil {
return fmt.Errorf(
"nil destination pointer for option %s",
optionName,
)
}
*dest = v
return nil
case PluginOptionTypeBool:
v, ok := value.(bool)
if !ok {
return fmt.Errorf(
"invalid type for option %s: expected bool",
optionName,
)
}
if opt.Dest == nil {
return fmt.Errorf(
"nil destination for option %s",
optionName,
)
}
dest, ok := opt.Dest.(*bool)
if !ok {
return fmt.Errorf(
"invalid destination type for option %s: expected *bool",
optionName,
)
}
if dest == nil {
return fmt.Errorf(
"nil destination pointer for option %s",
optionName,
)
}
*dest = v
return nil
case PluginOptionTypeInt:
v, ok := value.(int)
if !ok {
return fmt.Errorf(
"invalid type for option %s: expected int",
optionName,
)
}
if opt.Dest == nil {
return fmt.Errorf(
"nil destination for option %s",
optionName,
)
}
dest, ok := opt.Dest.(*int)
if !ok {
return fmt.Errorf(
"invalid destination type for option %s: expected *int",
optionName,
)
}
if dest == nil {
return fmt.Errorf(
"nil destination pointer for option %s",
optionName,
)
}
*dest = v
return nil
case PluginOptionTypeUint:
// accept uint64 or int
switch tv := value.(type) {
case uint64:
if opt.Dest == nil {
return fmt.Errorf("nil destination for option %s", optionName)
}
dest, ok := opt.Dest.(*uint64)
if !ok {
return fmt.Errorf("invalid destination type for option %s: expected *uint64", optionName)
}
if dest == nil {
return fmt.Errorf("nil destination pointer for option %s", optionName)
}
*dest = tv
return nil
case int:
if tv < 0 {
return fmt.Errorf("invalid value for option %s: negative int", optionName)
}
if opt.Dest == nil {
return fmt.Errorf("nil destination for option %s", optionName)
}
dest, ok := opt.Dest.(*uint64)
if !ok {
return fmt.Errorf("invalid destination type for option %s: expected *uint64", optionName)
}
if dest == nil {
return fmt.Errorf("nil destination pointer for option %s", optionName)
}
*dest = uint64(tv)
return nil
default:
return fmt.Errorf("invalid type for option %s: expected uint64 or int", optionName)
}
default:
return fmt.Errorf(
"unknown plugin option type %d for option %s",
opt.Type,
optionName,
)
}
}
// Option not found for this plugin: treat as non-fatal. This allows
// callers to attempt to set options that may not exist for all
// implementations (for example `data-dir` may not be relevant).
return nil
}
return fmt.Errorf(
"plugin %s of type %s not found",
pluginName,
PluginTypeName(pluginType),
)
}
Loading
Loading