feat: 004/phase 1
This commit is contained in:
32
internal/hubmodule/README.md
Normal file
32
internal/hubmodule/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# hubmodule
|
||||
|
||||
集中定义和实现 Any-Hub 的“代理 + 缓存”模块体系。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
internal/hubmodule/
|
||||
├── doc.go # 包级说明与约束
|
||||
├── README.md # 本文件
|
||||
├── registry.go # 模块注册/发现入口(后续任务)
|
||||
└── <module-key>/ # 各仓类型模块,例如 legacy、npm、docker
|
||||
```
|
||||
|
||||
## 模块约束
|
||||
- **单一接口**:每个模块需要同时实现代理与缓存接口,避免跨包耦合。
|
||||
- **注册流程**:在模块 `init()` 中调用 `hubmodule.Register(ModuleMetadata{...})`,注册失败必须 panic 以阻止启动。
|
||||
- **缓存布局**:一律使用 `StoragePath/<Hub>/<path>.body`,如需附加目录需在 `ModuleMetadata` 中声明迁移策略。
|
||||
- **配置注入**:模块仅通过依赖注入获取 `HubConfigEntry` 和全局参数,禁止直接读取文件或环境变量。
|
||||
- **可观测性**:所有模块必须输出 `module_key`、命中/回源状态等日志字段,并在返回错误时附带 Hub 名称。
|
||||
|
||||
## 开发流程
|
||||
1. 复制 `internal/hubmodule/template/`(由 T010 提供)作为起点。
|
||||
2. 填写模块特有逻辑与缓存策略,并确保包含中文注释解释设计。
|
||||
3. 在模块目录添加 `module_test.go`,使用 `httptest.Server` 与 `t.TempDir()` 复现真实流量。
|
||||
4. 运行 `make modules-test` 验证模块单元测试。
|
||||
5. 更新 `config.toml` 中对应 `[[Hub]].Module` 字段,验证集成测试后再提交。
|
||||
|
||||
## 术语
|
||||
- **Module Key**:模块唯一标识(如 `legacy`、`npm-tarball`)。
|
||||
- **Cache Strategy Profile**:定义 TTL、验证策略、磁盘布局等策略元数据。
|
||||
- **Legacy Adapter**:包装当前共享实现,确保迁移期间仍可运行。
|
||||
9
internal/hubmodule/doc.go
Normal file
9
internal/hubmodule/doc.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Package hubmodule 聚合任意仓类型的代理 + 缓存模块,并提供统一的注册入口。
|
||||
//
|
||||
// 模块作者需要:
|
||||
// 1. 在 internal/hubmodule/<module-key>/ 目录下实现代理与缓存接口;
|
||||
// 2. 通过本包暴露的 Register 函数在 init() 中注册模块元数据;
|
||||
// 3. 保证缓存写入仍遵循 StoragePath/<Hub>/<path>.body 布局,并补充中文注释说明实现细节。
|
||||
//
|
||||
// 该包同时负责提供模块发现、可观测信息以及迁移状态的对外查询能力。
|
||||
package hubmodule
|
||||
44
internal/hubmodule/interfaces.go
Normal file
44
internal/hubmodule/interfaces.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package hubmodule
|
||||
|
||||
import "time"
|
||||
|
||||
// MigrationState 描述模块上线阶段,方便观测端区分 legacy/beta/ga。
|
||||
type MigrationState string
|
||||
|
||||
const (
|
||||
MigrationStateLegacy MigrationState = "legacy"
|
||||
MigrationStateBeta MigrationState = "beta"
|
||||
MigrationStateGA MigrationState = "ga"
|
||||
)
|
||||
|
||||
// ValidationMode 描述缓存再验证的默认策略。
|
||||
type ValidationMode string
|
||||
|
||||
const (
|
||||
ValidationModeETag ValidationMode = "etag"
|
||||
ValidationModeLastModified ValidationMode = "last-modified"
|
||||
ValidationModeNever ValidationMode = "never"
|
||||
)
|
||||
|
||||
// CacheStrategyProfile 描述模块的缓存读写策略及其默认值。
|
||||
type CacheStrategyProfile struct {
|
||||
TTLHint time.Duration
|
||||
ValidationMode ValidationMode
|
||||
DiskLayout string
|
||||
RequiresMetadataFile bool
|
||||
SupportsStreamingWrite bool
|
||||
}
|
||||
|
||||
// ModuleMetadata 记录一个模块的静态信息,供配置校验和诊断端使用。
|
||||
type ModuleMetadata struct {
|
||||
Key string
|
||||
Description string
|
||||
MigrationState MigrationState
|
||||
SupportedProtocols []string
|
||||
CacheStrategy CacheStrategyProfile
|
||||
}
|
||||
|
||||
// DefaultModuleKey 返回内置 legacy 模块的键值。
|
||||
func DefaultModuleKey() string {
|
||||
return defaultModuleKey
|
||||
}
|
||||
20
internal/hubmodule/legacy/legacy_module.go
Normal file
20
internal/hubmodule/legacy/legacy_module.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package legacy
|
||||
|
||||
import "github.com/any-hub/any-hub/internal/hubmodule"
|
||||
|
||||
// 模块描述:包装当前共享的代理 + 缓存实现,供未迁移的 Hub 使用。
|
||||
func init() {
|
||||
hubmodule.MustRegister(hubmodule.ModuleMetadata{
|
||||
Key: hubmodule.DefaultModuleKey(),
|
||||
Description: "Legacy proxy + cache implementation bundled with any-hub",
|
||||
MigrationState: hubmodule.MigrationStateLegacy,
|
||||
SupportedProtocols: []string{
|
||||
"docker", "npm", "go", "pypi",
|
||||
},
|
||||
CacheStrategy: hubmodule.CacheStrategyProfile{
|
||||
DiskLayout: ".body",
|
||||
ValidationMode: hubmodule.ValidationModeETag,
|
||||
SupportsStreamingWrite: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
117
internal/hubmodule/registry.go
Normal file
117
internal/hubmodule/registry.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package hubmodule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const defaultModuleKey = "legacy"
|
||||
|
||||
var globalRegistry = newRegistry()
|
||||
|
||||
type registry struct {
|
||||
mu sync.RWMutex
|
||||
modules map[string]ModuleMetadata
|
||||
}
|
||||
|
||||
func newRegistry() *registry {
|
||||
return ®istry{modules: make(map[string]ModuleMetadata)}
|
||||
}
|
||||
|
||||
// Register 将模块元数据加入全局注册表,重复键会返回错误。
|
||||
func Register(meta ModuleMetadata) error {
|
||||
return globalRegistry.register(meta)
|
||||
}
|
||||
|
||||
// MustRegister 在注册失败时 panic,适合模块 init() 中调用。
|
||||
func MustRegister(meta ModuleMetadata) {
|
||||
if err := Register(meta); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve 返回指定键的模块元数据。
|
||||
func Resolve(key string) (ModuleMetadata, bool) {
|
||||
return globalRegistry.resolve(key)
|
||||
}
|
||||
|
||||
// List 返回按键排序的模块元数据列表。
|
||||
func List() []ModuleMetadata {
|
||||
return globalRegistry.list()
|
||||
}
|
||||
|
||||
// Keys 返回所有已注册模块的键值,供调试或诊断使用。
|
||||
func Keys() []string {
|
||||
items := List()
|
||||
result := make([]string, len(items))
|
||||
for i, meta := range items {
|
||||
result[i] = meta.Key
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *registry) normalizeKey(key string) string {
|
||||
return strings.ToLower(strings.TrimSpace(key))
|
||||
}
|
||||
|
||||
func (r *registry) register(meta ModuleMetadata) error {
|
||||
if meta.Key == "" {
|
||||
return fmt.Errorf("module key is required")
|
||||
}
|
||||
key := r.normalizeKey(meta.Key)
|
||||
if key == "" {
|
||||
return fmt.Errorf("module key is required")
|
||||
}
|
||||
meta.Key = key
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, exists := r.modules[key]; exists {
|
||||
return fmt.Errorf("module %s already registered", key)
|
||||
}
|
||||
r.modules[key] = meta
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) mustRegister(meta ModuleMetadata) {
|
||||
if err := r.register(meta); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) resolve(key string) (ModuleMetadata, bool) {
|
||||
if key == "" {
|
||||
return ModuleMetadata{}, false
|
||||
}
|
||||
normalized := r.normalizeKey(key)
|
||||
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
meta, ok := r.modules[normalized]
|
||||
return meta, ok
|
||||
}
|
||||
|
||||
func (r *registry) list() []ModuleMetadata {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if len(r.modules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(r.modules))
|
||||
for key := range r.modules {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
result := make([]ModuleMetadata, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
result = append(result, r.modules[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
49
internal/hubmodule/registry_test.go
Normal file
49
internal/hubmodule/registry_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package hubmodule
|
||||
|
||||
import "testing"
|
||||
|
||||
func replaceRegistry(t *testing.T) func() {
|
||||
t.Helper()
|
||||
prev := globalRegistry
|
||||
globalRegistry = newRegistry()
|
||||
return func() { globalRegistry = prev }
|
||||
}
|
||||
|
||||
func TestRegisterResolveAndList(t *testing.T) {
|
||||
cleanup := replaceRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := Register(ModuleMetadata{Key: "beta", MigrationState: MigrationStateBeta}); err != nil {
|
||||
t.Fatalf("register beta failed: %v", err)
|
||||
}
|
||||
if err := Register(ModuleMetadata{Key: "gamma", MigrationState: MigrationStateGA}); err != nil {
|
||||
t.Fatalf("register gamma failed: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := Resolve("beta"); !ok {
|
||||
t.Fatalf("expected beta to resolve")
|
||||
}
|
||||
if _, ok := Resolve("BETA"); !ok {
|
||||
t.Fatalf("resolve should be case-insensitive")
|
||||
}
|
||||
|
||||
list := List()
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("list length mismatch: %d", len(list))
|
||||
}
|
||||
if list[0].Key != "beta" || list[1].Key != "gamma" {
|
||||
t.Fatalf("unexpected order: %+v", list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterDuplicateFails(t *testing.T) {
|
||||
cleanup := replaceRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := Register(ModuleMetadata{Key: "legacy"}); err != nil {
|
||||
t.Fatalf("first registration should succeed: %v", err)
|
||||
}
|
||||
if err := Register(ModuleMetadata{Key: "legacy"}); err == nil {
|
||||
t.Fatalf("duplicate registration should fail")
|
||||
}
|
||||
}
|
||||
21
internal/hubmodule/strategy.go
Normal file
21
internal/hubmodule/strategy.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package hubmodule
|
||||
|
||||
import "time"
|
||||
|
||||
// StrategyOptions 描述来自 Hub Config 的 override。
|
||||
type StrategyOptions struct {
|
||||
TTLOverride time.Duration
|
||||
ValidationOverride ValidationMode
|
||||
}
|
||||
|
||||
// ResolveStrategy 将模块的默认策略与 hub 级覆盖合并。
|
||||
func ResolveStrategy(meta ModuleMetadata, opts StrategyOptions) CacheStrategyProfile {
|
||||
strategy := meta.CacheStrategy
|
||||
if opts.TTLOverride > 0 {
|
||||
strategy.TTLHint = opts.TTLOverride
|
||||
}
|
||||
if opts.ValidationOverride != "" {
|
||||
strategy.ValidationMode = opts.ValidationOverride
|
||||
}
|
||||
return strategy
|
||||
}
|
||||
13
internal/hubmodule/template/module.go
Normal file
13
internal/hubmodule/template/module.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package template
|
||||
|
||||
import "github.com/any-hub/any-hub/internal/hubmodule"
|
||||
|
||||
// Package template 提供编写新模块时可复制的骨架示例。
|
||||
//
|
||||
// 使用方式:复制整个目录到 internal/hubmodule/<module-key>/ 并替换字段。
|
||||
// - 将 TemplateModule 重命名为实际模块类型。
|
||||
// - 在 init() 中调用 hubmodule.MustRegister,注册新的 ModuleMetadata。
|
||||
// - 在模块目录中实现自定义代理/缓存逻辑,然后在 main 中调用 proxy.RegisterModuleHandler。
|
||||
//
|
||||
// 注意:本文件仅示例 metadata 注册写法,不会参与编译。
|
||||
var _ = hubmodule.ModuleMetadata{}
|
||||
Reference in New Issue
Block a user