feat(core): implement cordis configuration extension and migrate all plugins

This commit is contained in:
ryan
2026-08-29 10:53:53 +08:00
parent b43c429544
commit ed8491addf
74 changed files with 1354 additions and 1145 deletions
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package cache
// RedisConfig declares the configuration read by the Redis cache provider.
type RedisConfig struct {
Enabled bool `config:"enabled" env:"REDIS_ENABLED" default:"false" autoEnable:"REDIS_ADDR"`
Addrs []string `config:"addrs" env:"REDIS_ADDR"`
Username string `config:"username" env:"REDIS_USERNAME"`
Password string `config:"password" env:"REDIS_PASSWORD" secret:"true"`
DB int `config:"db" env:"REDIS_DB"`
ClusterMode bool `config:"cluster_mode" env:"REDIS_CLUSTER_MODE"`
MasterName string `config:"master_name" env:"REDIS_MASTER_NAME"`
KeyPrefix string `config:"key_prefix" env:"REDIS_KEY_PREFIX"`
PoolSize int `config:"pool_size" env:"REDIS_POOL_SIZE"`
MinIdleConn int `config:"min_idle_conn" env:"REDIS_MIN_IDLE_CONN"`
DialTimeout int `config:"dial_timeout" env:"REDIS_DIAL_TIMEOUT"`
ReadTimeout int `config:"read_timeout" env:"REDIS_READ_TIMEOUT"`
WriteTimeout int `config:"write_timeout" env:"REDIS_WRITE_TIMEOUT"`
MaxRetries int `config:"max_retries" env:"REDIS_MAX_RETRIES"`
PoolTimeout int `config:"pool_timeout" env:"REDIS_POOL_TIMEOUT"`
ConnMaxIdleTime int `config:"conn_max_idle_time" env:"REDIS_CONN_MAX_IDLE_TIME"`
MaintNotifications bool `config:"maint_notifications" env:"REDIS_MAINT_NOTIFICATIONS" default:"false"`
}
+18 -1
View File
@@ -77,13 +77,30 @@ func (p *Plugin) Name() string {
return "cache"
}
// DeclareConfig declares the configuration bindings consumed by this plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "redis", Target: &RedisConfig{}},
}
}
// ConfigEnabled gates plugin activation based on whether Redis is enabled.
func (p *Plugin) ConfigEnabled(view core.ConfigView) bool {
return view.Bool("redis.enabled", false)
}
// Apply mounts the multi-layer cache service into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var cfg RedisConfig
if err := ctx.Config().Bind("redis", &cfg); err != nil {
return err
}
redisClient := p.redisClient
if redisClient == nil {
if Redis == nil {
var err error
redisClient, err = InitRedis()
redisClient, err = InitRedisWithConfig(cfg)
if err != nil {
return err
}
+4 -2
View File
@@ -32,9 +32,11 @@ func TestCachePluginOperations(t *testing.T) {
cache.WithKeyPrefix("app:"),
cache.WithRAMCapacity(500),
)
assert.Equal(t, "cache", p.Name())
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(map[string]any{
"redis.enabled": true,
}))
require.NoError(t, ctx.Config().Resolve())
require.NoError(t, p.Apply(ctx))
svc, err := core.Inject[contracts.CacheService](ctx)
+26 -7
View File
@@ -4,12 +4,12 @@
package cache
import (
"Wavelet/pkg/config"
"context"
"encoding/json"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/redis/go-redis/extra/redisotel/v9"
@@ -18,18 +18,37 @@ import (
"go.opentelemetry.io/otel/attribute"
)
// Redis 全局 Redis 客户端实例
var Redis redis.UniversalClient
var (
// Redis 全局 Redis 客户端实例
Redis redis.UniversalClient
// InitRedis 初始化全局/默认 Redis 客户端实例
func InitRedis() (redis.UniversalClient, error) {
cfg := config.Config.Redis
keyPrefixMu sync.RWMutex
keyPrefix string
)
// SetKeyPrefix sets the package-level key prefix.
func SetKeyPrefix(prefix string) {
keyPrefixMu.Lock()
defer keyPrefixMu.Unlock()
keyPrefix = prefix
}
// GetKeyPrefix returns the package-level key prefix.
func GetKeyPrefix() string {
keyPrefixMu.RLock()
defer keyPrefixMu.RUnlock()
return keyPrefix
}
// InitRedisWithConfig initializes the Redis client using the provided RedisConfig.
func InitRedisWithConfig(cfg RedisConfig) (redis.UniversalClient, error) {
if !cfg.Enabled {
log.Println("[Redis] is disabled, skipping Redis initialization")
return nil, nil
}
SetKeyPrefix(cfg.KeyPrefix)
var client redis.UniversalClient
if cfg.ClusterMode {
@@ -114,7 +133,7 @@ func redisMaintNotificationsConfig(enabled bool) *maintnotifications.Config {
// PrefixedKey 返回带前缀的 Key
func PrefixedKey(key string) string {
prefix := config.Config.Redis.KeyPrefix
prefix := GetKeyPrefix()
if prefix == "" {
return key
}
@@ -54,6 +54,23 @@ func (p *Plugin) Manifest() core.Manifest {
}
}
// redisGateConfig declares the Redis gate configuration for cache_memory.
type redisGateConfig struct {
Enabled bool `config:"enabled" env:"REDIS_ENABLED" default:"false" autoEnable:"REDIS_ADDR"`
}
// DeclareConfig declares the configuration bindings consumed by this plugin.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "redis", Target: &redisGateConfig{}},
}
}
// ConfigEnabled gates plugin activation when Redis is disabled.
func (p *Plugin) ConfigEnabled(view core.ConfigView) bool {
return !view.Bool("redis.enabled", false)
}
// Apply mounts the in-memory cache service into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
svc, err := newMemoryCacheService(p.capacity, ctx.Events())
+15 -19
View File
@@ -5,7 +5,6 @@
package database
import (
"Wavelet/pkg/config"
"context"
"fmt"
"log"
@@ -34,35 +33,35 @@ var (
chDB *gorm.DB
)
func init() {
if !config.Config.ClickHouse.Enabled {
return
// InitClickHouseWithConfig initializes the ClickHouse connection using the provided configuration.
func InitClickHouseWithConfig(cfg ClickHouseConfig) error {
if !cfg.Enabled {
return nil
}
cfg := config.Config.ClickHouse
if cfg.Database == "" {
log.Fatalf("[ClickHouse] database name is required (expected: wavelet)\n")
return fmt.Errorf("[ClickHouse] database name is required (expected: wavelet)")
}
opts := buildClickHouseOptions()
opts := buildClickHouseOptions(cfg)
var err error
ChConn, err = clickhouse.Open(opts)
if err != nil {
log.Fatalf("[ClickHouse] init connection failed: %v\n", err)
return fmt.Errorf("[ClickHouse] init connection failed: %w", err)
}
if err = ChConn.Ping(context.Background()); err != nil {
log.Fatalf("[ClickHouse] ping failed: %v\n", err)
return fmt.Errorf("[ClickHouse] ping failed: %w", err)
}
chDB, err = gorm.Open(clickhouseDriver.New(clickhouseDriver.Config{
DSN: buildClickHouseDSN(),
DSN: buildClickHouseDSN(cfg),
}), &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
log.Fatalf("[ClickHouse] init gorm connection failed: %v\n", err)
return fmt.Errorf("[ClickHouse] init gorm connection failed: %w", err)
}
if err = chDB.Use(
@@ -74,12 +73,12 @@ func init() {
),
),
); err != nil {
log.Fatalf("[ClickHouse] init trace failed: %v\n", err)
return fmt.Errorf("[ClickHouse] init trace failed: %w", err)
}
sqlDB, err := chDB.DB()
if err != nil {
log.Fatalf("[ClickHouse] load sql db failed: %v\n", err)
return fmt.Errorf("[ClickHouse] load sql db failed: %w", err)
}
sqlDB.SetMaxIdleConns(cfg.MaxIdleConn)
@@ -87,11 +86,10 @@ func init() {
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
log.Println("[ClickHouse] connection established successfully")
return nil
}
func buildClickHouseOptions() *clickhouse.Options {
cfg := config.Config.ClickHouse
func buildClickHouseOptions(cfg ClickHouseConfig) *clickhouse.Options {
return &clickhouse.Options{
Addr: cfg.Hosts,
Auth: clickhouse.Auth{
@@ -114,9 +112,7 @@ func buildClickHouseOptions() *clickhouse.Options {
}
}
func buildClickHouseDSN() string {
cfg := config.Config.ClickHouse
func buildClickHouseDSN(cfg ClickHouseConfig) string {
chURL := &url.URL{
Scheme: "clickhouse",
Host: strings.Join(cfg.Hosts, ","),
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package database
import "time"
// ReplicaConfig declares a single read-replica configuration for PostgreSQL.
type ReplicaConfig struct {
Host string `config:"host"`
Port int `config:"port"`
Username string `config:"username"`
Password string `config:"password" secret:"true"`
}
// Config declares PostgreSQL and SQLite database configuration.
type Config struct {
Enabled bool `config:"enabled" env:"DB_ENABLED" default:"false" autoEnable:"DB_HOST"`
SQLitePath string `config:"sqlite_path" env:"SQLITE_PATH"`
Host string `config:"host" env:"DB_HOST"`
Port int `config:"port" env:"DB_PORT" default:"5432"`
Username string `config:"username" env:"DB_USERNAME"`
Password string `config:"password" env:"DB_PASSWORD" secret:"true"`
Database string `config:"database" env:"DB_NAME" default:"wavelet"`
MaxIdleConn int `config:"max_idle_conn" env:"DB_MAX_IDLE_CONN" default:"10"`
MaxOpenConn int `config:"max_open_conn" env:"DB_MAX_OPEN_CONN" default:"100"`
ConnMaxLifetime int `config:"conn_max_lifetime" env:"DB_CONN_MAX_LIFETIME" default:"3600"`
ConnMaxIdleTime int `config:"conn_max_idle_time" env:"DB_CONN_MAX_IDLE_TIME" default:"600"`
LogLevel string `config:"log_level" env:"DB_LOG_LEVEL" default:"warn"`
SSLMode string `config:"ssl_mode" env:"DB_SSL_MODE" default:"disable"`
TimeZone string `config:"time_zone" env:"DB_TIMEZONE" default:"UTC"`
ApplicationName string `config:"application_name" env:"DB_APPLICATION_NAME" default:"wavelet"`
SearchPath string `config:"search_path" env:"DB_SEARCH_PATH" default:"public"`
PreferSimpleProtocol bool `config:"prefer_simple_protocol" env:"DB_PREFER_SIMPLE_PROTOCOL"`
StatementCacheCapacity int `config:"statement_cache_capacity" env:"DB_STATEMENT_CACHE_CAPACITY"`
DefaultQueryExecMode string `config:"default_query_exec_mode" env:"DB_DEFAULT_QUERY_EXEC_MODE"`
Replicas []ReplicaConfig `config:"replicas"`
SlowThreshold time.Duration `config:"slow_threshold" env:"DB_SLOW_THRESHOLD" default:"200ms"`
}
// ClickHouseConfig declares the configuration for ClickHouse analytical storage.
type ClickHouseConfig struct {
Enabled bool `config:"enabled" env:"CLICKHOUSE_ENABLED" default:"false" autoEnable:"CLICKHOUSE_HOST"`
Hosts []string `config:"hosts" env:"CLICKHOUSE_HOST"`
Username string `config:"username" env:"CLICKHOUSE_USERNAME"`
Password string `config:"password" env:"CLICKHOUSE_PASSWORD" secret:"true"`
Database string `config:"database" env:"CLICKHOUSE_NAME" default:"wavelet"`
MaxIdleConn int `config:"max_idle_conn" env:"CLICKHOUSE_MAX_IDLE_CONN" default:"10"`
MaxOpenConn int `config:"max_open_conn" env:"CLICKHOUSE_MAX_OPEN_CONN" default:"50"`
ConnMaxLifetime int `config:"conn_max_lifetime" env:"CLICKHOUSE_CONN_MAX_LIFETIME" default:"3600"`
DialTimeout int `config:"dial_timeout" env:"CLICKHOUSE_DIAL_TIMEOUT" default:"10"`
BlockBufferSize uint8 `config:"block_buffer_size" env:"CLICKHOUSE_BLOCK_BUFFER_SIZE" default:"10"`
}
type appEnvConfig struct {
Env string `config:"env" env:"APP_ENV" default:"development"`
}
+29 -1
View File
@@ -56,17 +56,45 @@ func (p *Plugin) Name() string {
return "database"
}
// DeclareConfig declares database configuration keys.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "database", Target: &Config{}},
{Prefix: "clickhouse", Target: &ClickHouseConfig{}},
{Prefix: "app", Target: &appEnvConfig{}},
}
}
// Apply mounts the database service into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var dbCfg Config
if err := ctx.Config().Bind("database", &dbCfg); err != nil {
return err
}
var chCfg ClickHouseConfig
if err := ctx.Config().Bind("clickhouse", &chCfg); err != nil {
return err
}
var appCfg appEnvConfig
_ = ctx.Config().Bind("app", &appCfg)
targetDB := p.db
if targetDB == nil {
var err error
targetDB, err = InitDB()
targetDB, err = InitDBWithConfig(dbCfg, appCfg.Env == "production" || appCfg.Env == "prod")
if err != nil {
return err
}
}
if chCfg.Enabled {
if err := InitClickHouseWithConfig(chCfg); err != nil {
return err
}
}
svc := &dbServiceImpl{
primary: targetDB,
namedDBs: p.namedDBs,
@@ -37,9 +37,11 @@ func TestDatabasePlugin(t *testing.T) {
database.WithDB(gdb),
database.WithNamedDB("analytics", namedDB),
)
assert.Equal(t, "database", p.Name())
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(map[string]any{
"database.enabled": false,
}))
require.NoError(t, ctx.Config().Resolve())
require.NoError(t, p.Apply(ctx))
svc, err := core.Inject[contracts.DBService](ctx)
+39 -40
View File
@@ -4,7 +4,6 @@
package database
import (
"Wavelet/pkg/config"
"context"
"fmt"
"log"
@@ -30,15 +29,19 @@ const sqliteDirMode = 0o750
// InitDB 初始化主数据库实例(支持 PostgreSQL / SQLite)
func InitDB() (*gorm.DB, error) {
if !config.Config.Database.Enabled {
return initSQLite()
}
return initPostgres()
return InitDBWithConfig(Config{}, false)
}
// initSQLite 初始化 SQLite 数据库(PostgreSQL 禁用时的后备方案)
func initSQLite() (*gorm.DB, error) {
sqlitePath := config.Config.Database.SQLitePath
// InitDBWithConfig initializes the main database with the provided config.
func InitDBWithConfig(cfg Config, isProd bool) (*gorm.DB, error) {
if !cfg.Enabled {
return initSQLiteWithConfig(cfg, isProd)
}
return initPostgresWithConfig(cfg, isProd)
}
func initSQLiteWithConfig(cfg Config, isProd bool) (*gorm.DB, error) {
sqlitePath := cfg.SQLitePath
if sqlitePath == "" {
sqlitePath = "./data/wavelet.db"
}
@@ -54,9 +57,9 @@ func initSQLite() (*gorm.DB, error) {
targetDB, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: &gormZapLogger{
logLevel: parseLogLevel(config.Config.Database.LogLevel),
slowThreshold: config.Config.Database.SlowThreshold,
ignoreRecordNotFoundError: config.Config.App.IsProduction(),
logLevel: parseLogLevel(cfg.LogLevel),
slowThreshold: cfg.SlowThreshold,
ignoreRecordNotFoundError: isProd,
},
})
if err != nil {
@@ -81,24 +84,21 @@ func initSQLite() (*gorm.DB, error) {
return targetDB, nil
}
// initPostgres 初始化 PostgreSQL 数据库
func initPostgres() (*gorm.DB, error) {
dbConfig := config.Config.Database
func initPostgresWithConfig(cfg Config, isProd bool) (*gorm.DB, error) {
// 构建主库 DSN 并连接
primaryDSN := buildDSN(dbConfig.Host, dbConfig.Port, dbConfig.Username, dbConfig.Password)
primaryDSN := buildDSN(cfg, cfg.Host, cfg.Port, cfg.Username, cfg.Password)
pgConfig := postgres.Config{
DSN: primaryDSN,
PreferSimpleProtocol: dbConfig.PreferSimpleProtocol,
PreferSimpleProtocol: cfg.PreferSimpleProtocol,
}
targetDB, err := gorm.Open(postgres.New(pgConfig), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: &gormZapLogger{
logLevel: parseLogLevel(config.Config.Database.LogLevel),
slowThreshold: config.Config.Database.SlowThreshold,
ignoreRecordNotFoundError: config.Config.App.IsProduction(),
logLevel: parseLogLevel(cfg.LogLevel),
slowThreshold: cfg.SlowThreshold,
ignoreRecordNotFoundError: isProd,
},
})
if err != nil {
@@ -110,9 +110,9 @@ func initPostgres() (*gorm.DB, error) {
tracing.NewPlugin(
tracing.WithoutMetrics(),
tracing.WithAttributes(
attribute.String("db.instance", dbConfig.Database),
attribute.String("db.ip", dbConfig.Host),
attribute.String("server.address", net.JoinHostPort(dbConfig.Host, strconv.Itoa(dbConfig.Port))),
attribute.String("db.instance", cfg.Database),
attribute.String("db.ip", cfg.Host),
attribute.String("server.address", net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))),
attribute.String("db.system", "PostgreSQL"),
),
),
@@ -120,21 +120,21 @@ func initPostgres() (*gorm.DB, error) {
return nil, err
}
if len(dbConfig.Replicas) > 0 {
if len(cfg.Replicas) > 0 {
var replicaDialectors []gorm.Dialector
for _, replica := range dbConfig.Replicas {
for _, replica := range cfg.Replicas {
username := replica.Username
if username == "" {
username = dbConfig.Username
username = cfg.Username
}
password := replica.Password
if password == "" {
password = dbConfig.Password
password = cfg.Password
}
replicaDSN := buildDSN(replica.Host, replica.Port, username, password)
replicaDSN := buildDSN(cfg, replica.Host, replica.Port, username, password)
replicaDialectors = append(replicaDialectors, postgres.New(postgres.Config{
DSN: replicaDSN,
PreferSimpleProtocol: dbConfig.PreferSimpleProtocol,
PreferSimpleProtocol: cfg.PreferSimpleProtocol,
}))
}
@@ -143,15 +143,15 @@ func initPostgres() (*gorm.DB, error) {
Policy: dbresolver.RandomPolicy{},
})
resolver.SetMaxIdleConns(dbConfig.MaxIdleConn).
SetMaxOpenConns(dbConfig.MaxOpenConn).
SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Second).
SetConnMaxIdleTime(time.Duration(dbConfig.ConnMaxIdleTime) * time.Second)
resolver.SetMaxIdleConns(cfg.MaxIdleConn).
SetMaxOpenConns(cfg.MaxOpenConn).
SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second).
SetConnMaxIdleTime(time.Duration(cfg.ConnMaxIdleTime) * time.Second)
if err = targetDB.Use(resolver); err != nil {
return nil, err
}
log.Printf("[PostgreSQL] initialized in Primary-Replica mode (%d replicas)\n", len(dbConfig.Replicas))
log.Printf("[PostgreSQL] initialized in Primary-Replica mode (%d replicas)\n", len(cfg.Replicas))
} else {
log.Println("[PostgreSQL] initialized in Standalone mode")
}
@@ -162,18 +162,17 @@ func initPostgres() (*gorm.DB, error) {
return nil, err
}
sqlDB.SetMaxIdleConns(dbConfig.MaxIdleConn)
sqlDB.SetMaxOpenConns(dbConfig.MaxOpenConn)
sqlDB.SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Second)
sqlDB.SetConnMaxIdleTime(time.Duration(dbConfig.ConnMaxIdleTime) * time.Second)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConn)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConn)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
sqlDB.SetConnMaxIdleTime(time.Duration(cfg.ConnMaxIdleTime) * time.Second)
db = targetDB
return targetDB, nil
}
// buildDSN 构建 PostgreSQL DSN
func buildDSN(host string, port int, username, password string) string {
cfg := config.Config.Database
func buildDSN(cfg Config, host string, port int, username, password string) string {
pqURL := &url.URL{
Scheme: "postgres",
Host: net.JoinHostPort(host, strconv.Itoa(port)),
+32 -11
View File
@@ -41,11 +41,25 @@ func setupTestDB(t *testing.T) *gorm.DB {
return db
}
func TestDatabasePlugin(t *testing.T) {
func prepareTestContext(values map[string]any, declarers ...core.Plugin) *core.Context {
ctx := core.NewContext(context.Background())
testDB := setupTestDB(t)
ctx.Config().SetSource(core.NewMapSource(values))
for _, p := range declarers {
if d, ok := p.(interface{ DeclareConfig() []core.ConfigBinding }); ok {
for _, b := range d.DeclareConfig() {
_ = ctx.Config().Declare(p.Name(), b)
}
}
}
_ = ctx.Config().Resolve()
return ctx
}
func TestDatabasePlugin(t *testing.T) {
testDB := setupTestDB(t)
p := database.New(database.WithDB(testDB))
ctx := prepareTestContext(nil, p)
require.Equal(t, "database", p.Name())
require.NoError(t, p.Apply(ctx))
@@ -70,8 +84,9 @@ func TestDatabasePlugin(t *testing.T) {
}
func TestCachePluginRAMOnly(t *testing.T) {
ctx := core.NewContext(context.Background())
p := cache.New()
ctx := prepareTestContext(nil, p)
require.Equal(t, "cache", p.Name())
require.NoError(t, p.Apply(ctx))
@@ -142,12 +157,12 @@ func TestCachePluginWithRedisAndPubSub(t *testing.T) {
})
defer func() { _ = rdb.Close() }()
ctx1 := core.NewContext(context.Background())
ctx2 := core.NewContext(context.Background())
p1 := cache.New(cache.WithRedis(rdb), cache.WithKeyPrefix("test:"))
p2 := cache.New(cache.WithRedis(rdb), cache.WithKeyPrefix("test:"))
ctx1 := prepareTestContext(map[string]any{"redis.enabled": true}, p1)
ctx2 := prepareTestContext(map[string]any{"redis.enabled": true}, p2)
require.NoError(t, p1.Apply(ctx1))
require.NoError(t, p2.Apply(ctx2))
@@ -294,14 +309,20 @@ func TestStoragePlugin(t *testing.T) {
}
func TestAllInfraPluginsCombined(t *testing.T) {
ctx := core.NewContext(context.Background())
testDB := setupTestDB(t)
memBackend := newMemoryBackend()
require.NoError(t, database.New(database.WithDB(testDB)).Apply(ctx))
require.NoError(t, cache.New().Apply(ctx))
require.NoError(t, logger.New().Apply(ctx))
require.NoError(t, storage.New(storage.WithBackend(memBackend)).Apply(ctx))
dbP := database.New(database.WithDB(testDB))
cacheP := cache.New()
logP := logger.New()
storageP := storage.New(storage.WithBackend(memBackend))
ctx := prepareTestContext(nil, dbP, cacheP, logP, storageP)
require.NoError(t, dbP.Apply(ctx))
require.NoError(t, cacheP.Apply(ctx))
require.NoError(t, logP.Apply(ctx))
require.NoError(t, storageP.Apply(ctx))
// Using3 to resolve dependencies concurrently
var resolved bool