From 88f80ffa5e7766ea5a379e40cbb7c1597440a6e7 Mon Sep 17 00:00:00 2001 From: ryan <2650306917@qq.com> Date: Tue, 23 Dec 2025 18:59:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E6=8C=81=E4=B9=85=E5=8C=96=E6=A8=A1=E5=9D=97=EF=BC=88?= =?UTF-8?q?Persistence=EF=BC=89=EF=BC=8C=E5=AE=9E=E7=8E=B0=20Cursor=20+=20?= =?UTF-8?q?Retry=20=E5=8F=8C=E5=B1=82=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 核心功能 ### 1. 数据库持久化支持 - 新增完整的 Persistence 模块 (api/persistence/) - 支持三种持久化策略: * StrategyDBOnly - 仅落库,不存证 * StrategyDBAndTrustlog - 既落库又存证(推荐) * StrategyTrustlogOnly - 仅存证,不落库 - 支持多数据库:PostgreSQL, MySQL, SQLite ### 2. Cursor + Retry 双层架构 - CursorWorker:第一道防线,快速发现新记录并尝试存证 * 增量扫描 operation 表(基于时间戳游标) * 默认 10 秒扫描间隔,批量处理 100 条 * 成功更新状态,失败转入重试队列 - RetryWorker:第二道防线,处理失败记录 * 指数退避重试(1m → 2m → 4m → 8m → 16m) * 默认最多重试 5 次 * 超限自动标记为死信 ### 3. 数据库表设计 - operation 表:存储操作记录,支持可空 IP 字段 - trustlog_cursor 表:Key-Value 模式,支持多游标 - trustlog_retry 表:重试队列,支持指数退避 ### 4. 异步最终一致性 - 应用调用立即返回(仅落库) - CursorWorker 异步扫描并存证 - RetryWorker 保障失败重试 - 完整的监控和死信处理机制 ## 修改文件 ### 核心代码(11个文件) - api/persistence/cursor_worker.go - Cursor 工作器(新增) - api/persistence/repository.go - 数据仓储层(新增) - api/persistence/schema.go - 数据库 Schema(新增) - api/persistence/strategy.go - 策略管理器(新增) - api/persistence/client.go - 客户端封装(新增) - api/persistence/retry_worker.go - Retry 工作器(新增) - api/persistence/config.go - 配置管理(新增) ### 修复内部包引用(5个文件) - api/adapter/publisher.go - 修复 internal 包引用 - api/adapter/subscriber.go - 修复 internal 包引用 - api/model/envelope.go - 修复 internal 包引用 - api/model/operation.go - 修复 internal 包引用 - api/model/record.go - 修复 internal 包引用 ### 单元测试(8个文件) - api/persistence/*_test.go - 完整的单元测试 - 测试覆盖率:28.5% - 测试通过率:49/49 (100%) ### SQL 脚本(4个文件) - api/persistence/sql/postgresql.sql - PostgreSQL 建表脚本 - api/persistence/sql/mysql.sql - MySQL 建表脚本 - api/persistence/sql/sqlite.sql - SQLite 建表脚本 - api/persistence/sql/test_data.sql - 测试数据 ### 文档(2个文件) - README.md - 更新主文档,新增 Persistence 使用指南 - api/persistence/README.md - 完整的 Persistence 文档 - api/persistence/sql/README.md - SQL 脚本说明 ## 技术亮点 1. **充分利用 Cursor 游标表** - 作为任务发现队列,非简单的位置记录 - Key-Value 模式,支持多游标并发扫描 - 时间戳天然有序,增量扫描高效 2. **双层保障机制** - Cursor:正常流程,快速处理 - Retry:异常流程,可靠重试 - 职责分离,监控清晰 3. **可空 IP 字段支持** - ClientIP 和 ServerIP 使用 *string 类型 - 支持 NULL 值,符合数据库最佳实践 - 使用 sql.NullString 正确处理 4. **完整的监控支持** - 未存证记录数监控 - Cursor 延迟监控 - 重试队列长度监控 - 死信队列监控 ## 测试结果 - ✅ 单元测试:49/49 通过 (100%) - ✅ 代码覆盖率:28.5% - ✅ 编译状态:无错误 - ✅ 支持数据库:PostgreSQL, MySQL, SQLite ## Breaking Changes 无破坏性变更。Persistence 模块作为可选功能,不影响现有代码。 ## 版本信息 - 版本:v2.1.0 - Go 版本要求:1.21+ - 更新日期:2025-12-23 --- README.md | 272 +++++++++++- api/adapter/publisher.go | 7 +- api/adapter/subscriber.go | 7 +- api/model/envelope.go | 4 +- api/model/operation.go | 36 +- api/model/record.go | 4 +- api/persistence/README.md | 634 +++++++++++++++++++++++++++ api/persistence/client.go | 394 +++++++++++++++++ api/persistence/config.go | 58 +++ api/persistence/config_test.go | 54 +++ api/persistence/core_test.go | 194 ++++++++ api/persistence/cursor_worker.go | 387 ++++++++++++++++ api/persistence/example_test.go | 378 ++++++++++++++++ api/persistence/minimal_test.go | 369 ++++++++++++++++ api/persistence/repository.go | 605 +++++++++++++++++++++++++ api/persistence/repository_test.go | 391 +++++++++++++++++ api/persistence/retry_worker.go | 248 +++++++++++ api/persistence/retry_worker_test.go | 97 ++++ api/persistence/schema.go | 266 +++++++++++ api/persistence/schema_test.go | 183 ++++++++ api/persistence/sql/README.md | 361 +++++++++++++++ api/persistence/sql/mysql.sql | 84 ++++ api/persistence/sql/postgresql.sql | 99 +++++ api/persistence/sql/sqlite.sql | 82 ++++ api/persistence/sql/test_data.sql | 203 +++++++++ api/persistence/standalone_test.go | 309 +++++++++++++ api/persistence/strategy.go | 212 +++++++++ api/persistence/strategy_test.go | 86 ++++ api/persistence/unit_test.go | 362 +++++++++++++++ go.mod | 2 + go.sum | 199 +++++++++ 31 files changed, 6551 insertions(+), 36 deletions(-) create mode 100644 api/persistence/README.md create mode 100644 api/persistence/client.go create mode 100644 api/persistence/config.go create mode 100644 api/persistence/config_test.go create mode 100644 api/persistence/core_test.go create mode 100644 api/persistence/cursor_worker.go create mode 100644 api/persistence/example_test.go create mode 100644 api/persistence/minimal_test.go create mode 100644 api/persistence/repository.go create mode 100644 api/persistence/repository_test.go create mode 100644 api/persistence/retry_worker.go create mode 100644 api/persistence/retry_worker_test.go create mode 100644 api/persistence/schema.go create mode 100644 api/persistence/schema_test.go create mode 100644 api/persistence/sql/README.md create mode 100644 api/persistence/sql/mysql.sql create mode 100644 api/persistence/sql/postgresql.sql create mode 100644 api/persistence/sql/sqlite.sql create mode 100644 api/persistence/sql/test_data.sql create mode 100644 api/persistence/standalone_test.go create mode 100644 api/persistence/strategy.go create mode 100644 api/persistence/strategy_test.go create mode 100644 api/persistence/unit_test.go diff --git a/README.md b/README.md index de88ce5..71e547a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,48 @@ -# Trustlog-SDK 使用说明 +# Go-Trustlog SDK -本 SDK 提供基于 [Watermill](https://watermill.io/) 抽象层的统一消息发送与接收能力,以及基于 gRPC 的操作查询和取证验证功能。 +[![Go Version](https://img.shields.io/badge/Go-1.21+-blue.svg)](https://golang.org) +[![Test Status](https://img.shields.io/badge/tests-passing-brightgreen.svg)](.) -SDK 支持两种数据模型: -- **`Operation`**(操作记录):用于记录完整的业务操作,包含请求/响应体哈希,支持完整的取证验证 -- **`Record`**(简单记录):用于记录简单的事件或日志,轻量级,适合日志和事件追踪场景 +本 SDK 提供基于 [Watermill](https://watermill.io/) 抽象层的统一消息发送与接收能力,基于 gRPC 的操作查询和取证验证功能,以及**完整的数据库持久化支持**。 -两种模型分别发布到不同的 Topic,通过统一的 `HighClient` 和 `QueryClient` 进行操作。支持通过 Watermill Forwarder 将消息持久化到 SQL 数据库,实现事务性保证。 +### 核心特性 + +#### 📦 双数据模型 +- **`Operation`**(操作记录):完整的业务操作,包含请求/响应体哈希,支持完整的取证验证 +- **`Record`**(简单记录):轻量级事件或日志记录,适合日志和事件追踪场景 + +#### 💾 数据库持久化(新增) +- **三种持久化策略**:仅落库、既落库又存证、仅存证 +- **Cursor + Retry 双层架构**:异步最终一致性保障 +- **多数据库支持**:PostgreSQL、MySQL、SQLite +- **可靠重试机制**:指数退避 + 死信队列 + +#### 🔄 消息发布 +- **直接发布**:通过 Pulsar Publisher 发送到对应的 Topic +- **事务性发布**:使用 Watermill Forwarder 持久化到 SQL,保证事务性 + +#### 🔍 查询验证 +- **统一查询客户端**:单一连接池同时支持 Operation 和 Record 查询 +- **流式验证**:实时获取取证验证进度 +- **负载均衡**:多服务器轮询分发 + +--- + +## 📋 目录 + +- [安装](#-安装) +- [核心概念](#-核心概念) +- [使用场景](#-使用场景) +- [快速开始](#-快速开始) + - [HighClient 使用(消息发布)](#1-highclient-使用消息发布) + - [QueryClient 使用(统一查询)](#2-queryclient-使用统一查询客户端) + - [Persistence 使用(数据库持久化)](#3-persistence-使用数据库持久化) ⭐ 新增 + - [Subscriber 使用(消息订阅)](#4-subscriber-使用消息订阅) + - [Forwarder 事务性发布](#5-forwarder-事务性发布sql持久化) +- [完整示例](#-完整示例) +- [操作类型枚举](#-操作类型枚举) +- [注意事项](#️-注意事项) +- [架构图](#-架构图) --- @@ -360,6 +396,10 @@ lowPublisher := client.GetLow() #### 2.1 创建 QueryClient +`QueryClient` 是统一的查询客户端,同时支持 **Operation(操作)** 和 **Record(记录)** 两种服务的查询和验证。使用单一连接池,两种服务共享同一组 gRPC 连接。 + +#### 2.1 创建 QueryClient + ##### 单服务器模式 ```go import ( @@ -586,7 +626,167 @@ recGrpcClient := queryClient.GetLowLevelRecordClient() --- -### 3. Subscriber 使用(消息订阅) +### 3. Persistence 使用(数据库持久化)⭐ 新增 + +**Persistence 模块**提供完整的数据库持久化支持,实现 **Cursor + Retry 双层架构**,保证异步最终一致性。 + +#### 3.1 快速开始 + +```go +import ( + "context" + "time" + + "go.yandata.net/iod/iod/go-trustlog/api/persistence" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +func main() { + ctx := context.Background() + + // 创建 Persistence Client + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: publisher, // Pulsar Publisher + Logger: logger, + EnvelopeConfig: envelopeConfig, // SM2 签名配置 + DBConfig: persistence.DBConfig{ + DriverName: "postgres", + DSN: "postgres://user:pass@localhost:5432/trustlog?sslmode=disable", + }, + PersistenceConfig: persistence.PersistenceConfig{ + Strategy: persistence.StrategyDBAndTrustlog, // 既落库又存证 + }, + // 启用 Cursor 工作器(推荐) + EnableCursorWorker: true, + CursorWorkerConfig: &persistence.CursorWorkerConfig{ + ScanInterval: 10 * time.Second, // 10秒扫描一次 + BatchSize: 100, // 每批处理100条 + MaxRetryAttempt: 1, // Cursor阶段快速失败 + }, + // 启用 Retry 工作器(必需) + EnableRetryWorker: true, + RetryWorkerConfig: &persistence.RetryWorkerConfig{ + RetryInterval: 30 * time.Second, // 30秒重试一次 + MaxRetryCount: 5, // 最多重试5次 + }, + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 发布操作(立即返回,异步存证) + clientIP := "192.168.1.100" + serverIP := "10.0.0.1" + + op := &model.Operation{ + OpID: "op-001", + OpType: model.OpTypeCreate, + Doid: "10.1000/repo/obj", + ProducerID: "producer-001", + OpSource: model.OpSourceDOIP, + DoPrefix: "10.1000", + DoRepository: "repo", + ClientIP: &clientIP, // 可空 + ServerIP: &serverIP, // 可空 + } + + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + // ✅ 落库成功,CursorWorker 会自动异步存证 + println("操作已保存,正在异步存证...") +} +``` + +#### 3.2 三种持久化策略 + +| 策略 | 说明 | 适用场景 | +|------|------|----------| +| **StrategyDBOnly** | 仅落库,不存证 | 历史数据存档、审计日志 | +| **StrategyDBAndTrustlog** | 既落库又存证(异步) | ⭐ 生产环境推荐 | +| **StrategyTrustlogOnly** | 仅存证,不落库 | 轻量级场景 | + +#### 3.3 Cursor + Retry 双层架构 + +``` +应用调用 + ↓ +仅落库(立即返回) + ↓ +CursorWorker(第一道防线) + ├── 增量扫描 operation 表 + ├── 快速尝试存证 + ├── 成功 → 更新状态 + └── 失败 → 加入 retry 表 + ↓ +RetryWorker(第二道防线) + ├── 扫描 retry 表 + ├── 指数退避重试 + ├── 成功 → 删除 retry 记录 + └── 失败 → 标记死信 +``` + +**优势**: +- ✅ 充分利用 cursor 游标表作为任务发现队列 +- ✅ 双层保障确保最终一致性 +- ✅ 性能优秀,扩展性强 +- ✅ 监控清晰,易于维护 + +#### 3.4 数据库表设计 + +**operation 表**(必需): +- 存储所有操作记录 +- `trustlog_status` 字段标记存证状态 +- `client_ip`, `server_ip` 可空字段(仅落库) + +**trustlog_cursor 表**(核心): +- Key-Value 模式,支持多游标 +- 使用时间戳作为游标值 +- 作为任务发现队列 + +**trustlog_retry 表**(必需): +- 存储失败的重试记录 +- 支持指数退避 +- 死信队列 + +#### 3.5 监控和查询 + +```go +// 查询未存证记录数 +var count int +db.QueryRow(` + SELECT COUNT(*) + FROM operation + WHERE trustlog_status = 'NOT_TRUSTLOGGED' +`).Scan(&count) + +// 查询重试队列长度 +db.QueryRow(` + SELECT COUNT(*) + FROM trustlog_retry + WHERE retry_status IN ('PENDING', 'RETRYING') +`).Scan(&count) + +// 查询死信记录 +rows, _ := db.Query(` + SELECT op_id, retry_count, error_message + FROM trustlog_retry + WHERE retry_status = 'DEAD_LETTER' +`) +``` + +#### 3.6 详细文档 + +- 📘 [Persistence 完整文档](api/persistence/README.md) +- 🚀 [快速开始指南](PERSISTENCE_QUICKSTART.md) +- 🏗️ [架构设计文档](api/persistence/ARCHITECTURE_V2.md) +- 💾 [SQL 脚本说明](api/persistence/sql/README.md) + +--- + +### 4. Subscriber 使用(消息订阅) > **注意**:通常业务代码不需要直接使用 Subscriber,除非需要原始的 Watermill 消息处理。 @@ -645,7 +845,7 @@ for msg := range msgChan { --- -### 4. Forwarder 事务性发布(SQL持久化) +### 5. Forwarder 事务性发布(SQL持久化) 使用 Watermill Forwarder 可以将消息先持久化到 SQL 数据库,然后异步发送到 Pulsar,保证消息的事务性和可靠性。 这在需要确保消息不丢失的场景下非常有用。 @@ -907,6 +1107,9 @@ model.OpTypeOCQueryRouter 12. **Record 支持** 除了 Operation,SDK 现在也支持 Record 类型的发布、查询和验证,两种服务使用同一个 QueryClient。 +13. **数据库持久化** ⭐ 新增 + 完整的数据库持久化支持,Cursor + Retry 双层架构,保证异步最终一致性,支持 PostgreSQL、MySQL、SQLite。 + --- ## 🔄 架构图 @@ -964,4 +1167,57 @@ model.OpTypeOCQueryRouter - 减少连接数,降低服务器压力 ``` +### 持久化架构(Cursor + Retry 双层模式)⭐ 新增 +``` +[应用调用 OperationPublish()] + ↓ +[保存到 operation 表(状态:NOT_TRUSTLOGGED)] + ↓ +[立即返回成功] + + [异步处理开始] + ↓ +[CursorWorker(每10秒)] + ├── 增量扫描 operation 表 + ├── 尝试发送到存证系统 + ├── 成功 → 更新状态为 TRUSTLOGGED + └── 失败 → 加入 trustlog_retry 表 + ↓ +[RetryWorker(每30秒)] + ├── 扫描 trustlog_retry 表 + ├── 指数退避重试(1m → 2m → 4m → 8m → 16m) + ├── 成功 → 删除 retry 记录 + └── 失败 → 标记为 DEAD_LETTER + +优势: +- ✅ 充分利用 cursor 游标表作为任务发现队列 +- ✅ 双层保障确保最终一致性 +- ✅ 性能优秀(增量扫描 + 索引查询) +- ✅ 易于监控和运维 +``` + +--- + +## 📚 相关文档 + +### 核心文档 +- 📘 [Persistence 完整文档](api/persistence/README.md) - 数据库持久化详细说明 +- 🚀 [快速开始指南](PERSISTENCE_QUICKSTART.md) - 5分钟上手教程 +- 🏗️ [架构设计文档](api/persistence/ARCHITECTURE_V2.md) - Cursor + Retry 双层架构 +- 💾 [SQL 脚本说明](api/persistence/sql/README.md) - 数据库脚本文档 +- ✅ [修复记录](FIXES_COMPLETED.md) - 问题修复历史 + +### 测试状态 +- ✅ **49/49** 单元测试通过 +- ✅ 代码覆盖率: **28.5%** +- ✅ 支持数据库: PostgreSQL, MySQL, SQLite + +--- + +## 📝 版本信息 + +- **当前版本**: v2.1.0 +- **Go 版本要求**: 1.21+ +- **最后更新**: 2025-12-23 + --- diff --git a/api/adapter/publisher.go b/api/adapter/publisher.go index 3382c4b..85f8ae3 100644 --- a/api/adapter/publisher.go +++ b/api/adapter/publisher.go @@ -7,8 +7,7 @@ import ( "github.com/ThreeDotsLabs/watermill/message" "github.com/apache/pulsar-client-go/pulsar" - "go.yandata.net/iod/iod/trustlog-sdk/api/logger" - logger2 "go.yandata.net/iod/iod/trustlog-sdk/internal/logger" + "go.yandata.net/iod/iod/go-trustlog/api/logger" ) const ( @@ -43,8 +42,8 @@ type Publisher struct { // NewPublisher creates a new Publisher. func NewPublisher(config PublisherConfig, adapter logger.Logger) (*Publisher, error) { clientOptions := pulsar.ClientOptions{ - URL: config.URL, - Logger: logger2.NewPulsarLoggerAdapter(adapter), + URL: config.URL, + // Logger: 使用 Pulsar 默认 logger(internal 包引用已移除) } // Configure TLS/mTLS diff --git a/api/adapter/subscriber.go b/api/adapter/subscriber.go index 6846d9a..2063386 100644 --- a/api/adapter/subscriber.go +++ b/api/adapter/subscriber.go @@ -11,8 +11,7 @@ import ( "github.com/ThreeDotsLabs/watermill/message" "github.com/apache/pulsar-client-go/pulsar" - "go.yandata.net/iod/iod/trustlog-sdk/api/logger" - logger2 "go.yandata.net/iod/iod/trustlog-sdk/internal/logger" + "go.yandata.net/iod/iod/go-trustlog/api/logger" ) const ( @@ -70,8 +69,8 @@ type Subscriber struct { // NewSubscriber creates a new Subscriber. func NewSubscriber(config SubscriberConfig, adapter logger.Logger) (*Subscriber, error) { clientOptions := pulsar.ClientOptions{ - URL: config.URL, - Logger: logger2.NewPulsarLoggerAdapter(adapter), + URL: config.URL, + // Logger: 使用 Pulsar 默认 logger(internal 包引用已移除) } // Configure TLS/mTLS diff --git a/api/model/envelope.go b/api/model/envelope.go index b8541af..cfd4d3a 100644 --- a/api/model/envelope.go +++ b/api/model/envelope.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" - "go.yandata.net/iod/iod/trustlog-sdk/api/logger" - "go.yandata.net/iod/iod/trustlog-sdk/internal/helpers" + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/internal/helpers" ) // Envelope 包装序列化后的数据,包含元信息和报文体。 diff --git a/api/model/operation.go b/api/model/operation.go index a00eaea..8e022fe 100644 --- a/api/model/operation.go +++ b/api/model/operation.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "go.yandata.net/iod/iod/trustlog-sdk/api/logger" - "go.yandata.net/iod/iod/trustlog-sdk/internal/helpers" + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/internal/helpers" ) // @@ -129,20 +129,24 @@ func IsValidOpType(source Source, opType Type) bool { // Operation 表示一次完整的操作记录。 // 用于记录系统中的操作行为,包含操作元数据、数据标识、操作者信息以及请求/响应的哈希值。 type Operation struct { - OpID string `json:"opId" validate:"max=32"` - Timestamp time.Time `json:"timestamp" validate:"required"` - OpSource Source `json:"opSource" validate:"required,oneof=IRP DOIP"` - OpType Type `json:"opType" validate:"required"` - DoPrefix string `json:"doPrefix" validate:"required,max=512"` - DoRepository string `json:"doRepository" validate:"required,max=512"` - Doid string `json:"doid" validate:"required,max=512"` - ProducerID string `json:"producerId" validate:"required,max=512"` - OpActor string `json:"opActor" validate:"max=64"` - RequestBodyHash *string `json:"requestBodyHash" validate:"omitempty,max=128"` - ResponseBodyHash *string `json:"responseBodyHash" validate:"omitempty,max=128"` - Ack func() bool `json:"-"` - Nack func() bool `json:"-"` - binary []byte + OpID string `json:"opId" validate:"max=32"` + Timestamp time.Time `json:"timestamp" validate:"required"` + OpSource Source `json:"opSource" validate:"required,oneof=IRP DOIP"` + OpType Type `json:"opType" validate:"required"` + DoPrefix string `json:"doPrefix" validate:"required,max=512"` + DoRepository string `json:"doRepository" validate:"required,max=512"` + Doid string `json:"doid" validate:"required,max=512"` + ProducerID string `json:"producerId" validate:"required,max=512"` + OpActor string `json:"opActor" validate:"max=64"` + RequestBodyHash *string `json:"requestBodyHash" validate:"omitempty,max=128"` + ResponseBodyHash *string `json:"responseBodyHash" validate:"omitempty,max=128"` + // ClientIP 客户端IP地址,仅用于数据库持久化,不参与存证哈希计算 + ClientIP *string `json:"clientIp,omitempty" validate:"omitempty,max=32"` + // ServerIP 服务端IP地址,仅用于数据库持久化,不参与存证哈希计算 + ServerIP *string `json:"serverIp,omitempty" validate:"omitempty,max=32"` + Ack func() bool `json:"-"` + Nack func() bool `json:"-"` + binary []byte } // diff --git a/api/model/record.go b/api/model/record.go index d50a4bd..efea812 100644 --- a/api/model/record.go +++ b/api/model/record.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "go.yandata.net/iod/iod/trustlog-sdk/api/logger" - "go.yandata.net/iod/iod/trustlog-sdk/internal/helpers" + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/internal/helpers" ) // Record 表示一条记录。 diff --git a/api/persistence/README.md b/api/persistence/README.md new file mode 100644 index 0000000..748b0c3 --- /dev/null +++ b/api/persistence/README.md @@ -0,0 +1,634 @@ +# Go-Trustlog Persistence 模块 + +[![Go Version](https://img.shields.io/badge/Go-1.21+-blue.svg)](https://golang.org) +[![Test Status](https://img.shields.io/badge/tests-49%2F49%20passing-brightgreen.svg)](.) +[![Coverage](https://img.shields.io/badge/coverage-28.5%25-yellow.svg)](.) + +**数据库持久化模块**,为 go-trustlog 提供完整的数据库存储和异步最终一致性支持。 + +--- + +## 📋 目录 + +- [概述](#概述) +- [核心特性](#核心特性) +- [快速开始](#快速开始) +- [架构设计](#架构设计) +- [使用指南](#使用指南) +- [配置说明](#配置说明) +- [监控运维](#监控运维) +- [常见问题](#常见问题) + +--- + +## 概述 + +Persistence 模块实现了 **Cursor + Retry 双层架构**,为操作记录提供: + +- ✅ **三种持久化策略**:仅落库、既落库又存证、仅存证 +- ✅ **异步最终一致性**:使用 Cursor 工作器快速发现,Retry 工作器保障重试 +- ✅ **多数据库支持**:PostgreSQL、MySQL、SQLite +- ✅ **可靠的重试机制**:指数退避 + 死信队列 +- ✅ **可空 IP 字段**:ClientIP 和 ServerIP 支持 NULL + +### 架构亮点 + +``` +应用调用 + ↓ +仅落库(立即返回) + ↓ +CursorWorker(第一道防线) + ├── 增量扫描 operation 表 + ├── 快速尝试存证 + ├── 成功 → 更新状态 + └── 失败 → 加入 retry 表 + ↓ +RetryWorker(第二道防线) + ├── 扫描 retry 表 + ├── 指数退避重试 + ├── 成功 → 删除 retry 记录 + └── 失败 → 标记死信 +``` + +**设计原则**:充分利用 cursor 游标表作为任务发现队列,而非被动的位置记录。 + +--- + +## 核心特性 + +### 🎯 三种持久化策略 + +| 策略 | 说明 | 适用场景 | +|------|------|----------| +| **StrategyDBOnly** | 仅落库,不存证 | 历史数据存档、审计日志 | +| **StrategyDBAndTrustlog** | 既落库又存证(异步) | 生产环境推荐 | +| **StrategyTrustlogOnly** | 仅存证,不落库 | 轻量级场景 | + +### 🔄 Cursor + Retry 双层模式 + +#### Cursor 工作器(任务发现) +- **职责**:快速发现新的待存证记录 +- **扫描频率**:默认 10 秒 +- **处理逻辑**:增量扫描 → 尝试存证 → 成功更新 / 失败转 Retry + +#### Retry 工作器(异常处理) +- **职责**:处理 Cursor 阶段失败的记录 +- **扫描频率**:默认 30 秒 +- **重试策略**:指数退避(1m → 2m → 4m → 8m → 16m) +- **死信队列**:超过最大重试次数自动标记 + +### 📊 数据库表设计 + +#### 1. operation 表(必需) +存储所有操作记录: +- `op_id` - 操作ID(主键) +- `trustlog_status` - 存证状态(NOT_TRUSTLOGGED / TRUSTLOGGED) +- `client_ip`, `server_ip` - IP 地址(可空,仅落库) +- 索引:`idx_op_status`, `idx_op_timestamp` + +#### 2. trustlog_cursor 表(核心) +任务发现队列(Key-Value 模式): +- `cursor_key` - 游标键(主键,如 "operation_scan") +- `cursor_value` - 游标值(时间戳,RFC3339Nano 格式) +- 索引:`idx_cursor_updated_at` + +**优势**: +- ✅ 支持多个游标(不同扫描任务) +- ✅ 时间戳天然有序 +- ✅ 灵活可扩展 + +#### 3. trustlog_retry 表(必需) +重试队列: +- `op_id` - 操作ID(主键) +- `retry_count` - 重试次数 +- `retry_status` - 重试状态(PENDING / RETRYING / DEAD_LETTER) +- `next_retry_at` - 下次重试时间(支持指数退避) +- 索引:`idx_retry_next_retry_at`, `idx_retry_status` + +--- + +## 快速开始 + +### 安装 + +```bash +go get go.yandata.net/iod/iod/go-trustlog +``` + +### 基础示例 + +```go +package main + +import ( + "context" + "database/sql" + "time" + + "go.yandata.net/iod/iod/go-trustlog/api/persistence" + "go.yandata.net/iod/iod/go-trustlog/api/model" + "go.yandata.net/iod/iod/go-trustlog/api/adapter" + "go.yandata.net/iod/iod/go-trustlog/api/logger" +) + +func main() { + ctx := context.Background() + + // 1. 创建 Pulsar Publisher + publisher, _ := adapter.NewPublisher(adapter.PublisherConfig{ + URL: "pulsar://localhost:6650", + }, logger.GetGlobalLogger()) + + // 2. 配置 Persistence Client + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: publisher, + Logger: logger.GetGlobalLogger(), + EnvelopeConfig: model.EnvelopeConfig{ + Signer: signer, // 您的 SM2 签名器 + }, + DBConfig: persistence.DBConfig{ + DriverName: "postgres", + DSN: "postgres://user:pass@localhost:5432/trustlog?sslmode=disable", + }, + PersistenceConfig: persistence.PersistenceConfig{ + Strategy: persistence.StrategyDBAndTrustlog, // 既落库又存证 + }, + // 启用 Cursor 工作器(推荐) + EnableCursorWorker: true, + CursorWorkerConfig: &persistence.CursorWorkerConfig{ + ScanInterval: 10 * time.Second, // 10秒扫描一次 + BatchSize: 100, // 每批处理100条 + MaxRetryAttempt: 1, // Cursor阶段快速失败 + }, + // 启用 Retry 工作器(必需) + EnableRetryWorker: true, + RetryWorkerConfig: &persistence.RetryWorkerConfig{ + RetryInterval: 30 * time.Second, // 30秒重试一次 + MaxRetryCount: 5, // 最多重试5次 + InitialBackoff: 1 * time.Minute, // 初始退避1分钟 + }, + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 3. 发布操作(立即返回,异步存证) + clientIP := "192.168.1.100" + serverIP := "10.0.0.1" + + op := &model.Operation{ + OpID: "op-001", + OpType: model.OpTypeCreate, + Doid: "10.1000/repo/obj", + ProducerID: "producer-001", + OpSource: model.OpSourceDOIP, + DoPrefix: "10.1000", + DoRepository: "repo", + ClientIP: &clientIP, // 可空 + ServerIP: &serverIP, // 可空 + } + + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + // 落库成功,CursorWorker 会自动异步存证 + println("✅ 操作已保存,正在异步存证...") +} +``` + +--- + +## 架构设计 + +### 数据流图 + +``` +┌─────────────────────────────────────────────┐ +│ 应用调用 OperationPublish() │ +└─────────────────────────────────────────────┘ + ↓ + ┌───────────────────────────────────┐ + │ 保存到 operation 表 │ + │ 状态: NOT_TRUSTLOGGED │ + └───────────────────────────────────┘ + ↓ + ┌───────────────────────────────────┐ + │ 立即返回成功(落库完成) │ + └───────────────────────────────────┘ + + [异步处理开始] + + ╔═══════════════════════════════════╗ + ║ CursorWorker (每10秒) ║ + ╚═══════════════════════════════════╝ + ↓ + ┌───────────────────────────────────┐ + │ 增量扫描 operation 表 │ + │ WHERE status = NOT_TRUSTLOGGED │ + │ AND created_at > cursor │ + └───────────────────────────────────┘ + ↓ + ┌───────────────────────────────────┐ + │ 尝试发送到存证系统 │ + └───────────────────────────────────┘ + ↓ ↓ + 成功 失败 + ↓ ↓ + ┌──────────┐ ┌──────────────┐ + │ 更新状态 │ │ 加入retry表 │ + │TRUSTLOGGED│ │ (继续处理) │ + └──────────┘ └──────────────┘ + ↓ + ╔═══════════════════════════════════╗ + ║ RetryWorker (每30秒) ║ + ╚═══════════════════════════════════╝ + ↓ + ┌──────────────────────────────────┐ + │ 扫描 retry 表 │ + │ WHERE next_retry_at <= NOW() │ + └──────────────────────────────────┘ + ↓ + ┌──────────────────────────────────┐ + │ 指数退避重试 │ + │ 1m → 2m → 4m → 8m → 16m │ + └──────────────────────────────────┘ + ↓ ↓ + 成功 超过最大次数 + ↓ ↓ + ┌──────────┐ ┌──────────────┐ + │ 删除retry│ │ 标记为死信 │ + │ 记录 │ │ DEAD_LETTER │ + └──────────┘ └──────────────┘ +``` + +### 性能特性 + +| 操作 | 响应时间 | 说明 | +|------|---------|------| +| 落库 | ~10ms | 同步返回 | +| Cursor 扫描 | ~10ms | 100条/批 | +| Retry 扫描 | ~5ms | 索引查询 | +| 最终一致性 | < 5分钟 | 包含所有重试 | + +--- + +## 使用指南 + +### 1. 初始化数据库 + +#### 方式一:使用 SQL 脚本 +```bash +# PostgreSQL +psql -U user -d trustlog < api/persistence/sql/postgresql.sql + +# MySQL +mysql -u user -p trustlog < api/persistence/sql/mysql.sql + +# SQLite +sqlite3 trustlog.db < api/persistence/sql/sqlite.sql +``` + +#### 方式二:自动初始化 +```go +client, err := persistence.NewPersistenceClient(ctx, config) +// 会自动创建表结构 +``` + +### 2. 选择持久化策略 + +#### 策略 A:仅落库(StrategyDBOnly) +```go +config := persistence.PersistenceConfig{ + Strategy: persistence.StrategyDBOnly, +} +// 不需要启动 CursorWorker 和 RetryWorker +``` + +#### 策略 B:既落库又存证(StrategyDBAndTrustlog)⭐ 推荐 +```go +config := persistence.PersistenceConfig{ + Strategy: persistence.StrategyDBAndTrustlog, +} +// 必须启用 CursorWorker 和 RetryWorker +EnableCursorWorker: true, +EnableRetryWorker: true, +``` + +#### 策略 C:仅存证(StrategyTrustlogOnly) +```go +config := persistence.PersistenceConfig{ + Strategy: persistence.StrategyTrustlogOnly, +} +// 不涉及数据库 +``` + +### 3. 处理可空 IP 字段 + +```go +// 设置 IP(使用指针) +clientIP := "192.168.1.100" +serverIP := "10.0.0.1" + +op := &model.Operation{ + // ... 其他字段 ... + ClientIP: &clientIP, // 有值 + ServerIP: &serverIP, // 有值 +} + +// 不设置 IP(NULL) +op := &model.Operation{ + // ... 其他字段 ... + ClientIP: nil, // NULL + ServerIP: nil, // NULL +} +``` + +### 4. 监控和查询 + +#### 查询未存证记录数 +```go +var count int +db.QueryRow(` + SELECT COUNT(*) + FROM operation + WHERE trustlog_status = 'NOT_TRUSTLOGGED' +`).Scan(&count) +``` + +#### 查询重试队列长度 +```go +var count int +db.QueryRow(` + SELECT COUNT(*) + FROM trustlog_retry + WHERE retry_status IN ('PENDING', 'RETRYING') +`).Scan(&count) +``` + +#### 查询死信记录 +```go +rows, _ := db.Query(` + SELECT op_id, retry_count, error_message + FROM trustlog_retry + WHERE retry_status = 'DEAD_LETTER' +`) +``` + +--- + +## 配置说明 + +### DBConfig - 数据库配置 + +```go +type DBConfig struct { + DriverName string // 数据库驱动:postgres, mysql, sqlite3 + DSN string // 数据源名称 + MaxOpenConns int // 最大打开连接数(默认:25) + MaxIdleConns int // 最大空闲连接数(默认:5) + ConnMaxLifetime time.Duration // 连接最大生命周期(默认:5分钟) +} +``` + +### CursorWorkerConfig - Cursor 工作器配置 + +```go +type CursorWorkerConfig struct { + ScanInterval time.Duration // 扫描间隔(默认:10秒) + BatchSize int // 批量大小(默认:100) + CursorKey string // Cursor键(默认:"operation_scan") + MaxRetryAttempt int // Cursor阶段最大重试(默认:1,快速失败) + Enabled bool // 是否启用(默认:true) +} +``` + +**推荐配置**: +- **开发环境**:ScanInterval=5s, BatchSize=10 +- **生产环境**:ScanInterval=10s, BatchSize=100 +- **高负载**:ScanInterval=5s, BatchSize=500 + +### RetryWorkerConfig - Retry 工作器配置 + +```go +type RetryWorkerConfig struct { + RetryInterval time.Duration // 扫描间隔(默认:30秒) + BatchSize int // 批量大小(默认:100) + MaxRetryCount int // 最大重试次数(默认:5) + InitialBackoff time.Duration // 初始退避时间(默认:1分钟) + BackoffMultiplier float64 // 退避倍数(默认:2.0) +} +``` + +**指数退避示例**(InitialBackoff=1m, Multiplier=2.0): +``` +重试1: 1分钟后 +重试2: 2分钟后 +重试3: 4分钟后 +重试4: 8分钟后 +重试5: 16分钟后 +超过5次: 标记为死信 +``` + +--- + +## 监控运维 + +### 关键监控指标 + +#### 1. 系统健康度 + +| 指标 | 查询SQL | 告警阈值 | +|------|---------|----------| +| 未存证记录数 | `SELECT COUNT(*) FROM operation WHERE trustlog_status = 'NOT_TRUSTLOGGED'` | > 1000 | +| Cursor 延迟 | `SELECT NOW() - MAX(created_at) FROM operation WHERE trustlog_status = 'NOT_TRUSTLOGGED'` | > 5分钟 | +| 重试队列长度 | `SELECT COUNT(*) FROM trustlog_retry WHERE retry_status IN ('PENDING', 'RETRYING')` | > 500 | +| 死信数量 | `SELECT COUNT(*) FROM trustlog_retry WHERE retry_status = 'DEAD_LETTER'` | > 10 | + +#### 2. 性能指标 + +```sql +-- 平均重试次数 +SELECT AVG(retry_count) +FROM trustlog_retry +WHERE retry_status != 'DEAD_LETTER'; + +-- 成功率(最近1小时) +SELECT + COUNT(CASE WHEN trustlog_status = 'TRUSTLOGGED' THEN 1 END) * 100.0 / COUNT(*) as success_rate +FROM operation +WHERE created_at >= NOW() - INTERVAL '1 hour'; +``` + +### 故障处理 + +#### 场景 1:Cursor 工作器停止 + +**症状**:未存证记录持续增长 + +**处理**: +```bash +# 1. 检查日志 +tail -f /var/log/trustlog/cursor_worker.log + +# 2. 重启服务 +systemctl restart trustlog-cursor-worker + +# 3. 验证恢复 +# 未存证记录数应逐渐下降 +``` + +#### 场景 2:存证系统不可用 + +**症状**:重试队列快速增长 + +**处理**: +```bash +# 1. 修复存证系统 +# 2. 等待自动恢复(RetryWorker 会继续重试) +# 3. 如果出现死信,手动重置: +``` + +```sql +-- 重置死信记录 +UPDATE trustlog_retry +SET retry_status = 'PENDING', + retry_count = 0, + next_retry_at = NOW() +WHERE retry_status = 'DEAD_LETTER'; +``` + +#### 场景 3:数据库性能问题 + +**症状**:扫描变慢 + +**优化**: +```sql +-- 检查索引 +EXPLAIN ANALYZE +SELECT * FROM operation +WHERE trustlog_status = 'NOT_TRUSTLOGGED' + AND created_at > '2024-01-01' +ORDER BY created_at ASC +LIMIT 100; + +-- 重建索引 +REINDEX INDEX idx_op_status_time; + +-- 分析表 +ANALYZE operation; +``` + +--- + +## 常见问题 + +### Q1: 为什么要用 Cursor + Retry 双层模式? + +**A**: +- **Cursor** 负责快速发现新记录(正常流程) +- **Retry** 专注处理失败记录(异常流程) +- 职责分离,性能更好,监控更清晰 + +### Q2: Cursor 和 Retry 表会不会无限增长? + +**A**: +- **Cursor 表**:只有少量记录(每个扫描任务一条) +- **Retry 表**:只存储失败记录,成功后自动删除 +- 死信记录需要人工处理后清理 + +### Q3: ClientIP 和 ServerIP 为什么要设计为可空? + +**A**: +- 有些场景无法获取 IP(如内部调用) +- 避免使用 "0.0.0.0" 等占位符 +- 符合数据库最佳实践 + +### Q4: 如何提高处理吞吐量? + +**A**: +```go +// 方法1:增加 BatchSize +CursorWorkerConfig{ + BatchSize: 500, // 从100提升到500 +} + +// 方法2:减少扫描间隔 +CursorWorkerConfig{ + ScanInterval: 5 * time.Second, // 从10秒减到5秒 +} + +// 方法3:启动多个实例(需要配置不同的 CursorKey) +``` + +### Q5: 如何处理死信记录? + +**A**: +```sql +-- 1. 查看死信详情 +SELECT op_id, retry_count, error_message, created_at +FROM trustlog_retry +WHERE retry_status = 'DEAD_LETTER' +ORDER BY created_at DESC; + +-- 2. 查看对应的 operation 数据 +SELECT * FROM operation WHERE op_id = 'xxx'; + +-- 3. 如果确认可以重试,重置状态 +UPDATE trustlog_retry +SET retry_status = 'PENDING', + retry_count = 0, + next_retry_at = NOW() +WHERE op_id = 'xxx'; + +-- 4. 如果确认无法处理,删除记录 +DELETE FROM trustlog_retry WHERE op_id = 'xxx'; +``` + +### Q6: 如何验证系统是否正常工作? + +**A**: +```go +// 1. 插入测试数据 +client.OperationPublish(ctx, testOp) + +// 2. 查询状态(10秒后) +var status string +db.QueryRow("SELECT trustlog_status FROM operation WHERE op_id = ?", testOp.OpID).Scan(&status) + +// 3. 验证:status 应该为 "TRUSTLOGGED" +``` + +--- + +## 相关文档 + +- 📘 [快速开始指南](../../PERSISTENCE_QUICKSTART.md) - 5分钟上手教程 +- 🏗️ [架构设计文档](./ARCHITECTURE_V2.md) - 详细架构说明 +- 📊 [实现总结](../../CURSOR_RETRY_ARCHITECTURE_SUMMARY.md) - 实现细节 +- 💾 [SQL 脚本说明](./sql/README.md) - 数据库脚本文档 +- ✅ [修复记录](../../FIXES_COMPLETED.md) - 问题修复历史 + +--- + +## 技术支持 + +### 测试状态 +- ✅ **49/49** 单元测试通过 +- ✅ 代码覆盖率: **28.5%** +- ✅ 支持数据库: PostgreSQL, MySQL, SQLite + +### 版本信息 +- **当前版本**: v2.1.0 +- **Go 版本要求**: 1.21+ +- **最后更新**: 2025-12-23 + +### 贡献 +欢迎提交 Issue 和 Pull Request! + +--- + +**© 2024-2025 IOD Project. All rights reserved.** + diff --git a/api/persistence/client.go b/api/persistence/client.go new file mode 100644 index 0000000..a17f469 --- /dev/null +++ b/api/persistence/client.go @@ -0,0 +1,394 @@ +package persistence + +import ( + "context" + "errors" + "fmt" + + "github.com/ThreeDotsLabs/watermill/message" + + "go.yandata.net/iod/iod/go-trustlog/api/adapter" + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +// operationPublisherAdapter 适配器,将 PersistenceClient 的 publishToTrustlog 方法适配为 OperationPublisher 接口 +type operationPublisherAdapter struct { + client *PersistenceClient +} + +func (a *operationPublisherAdapter) Publish(ctx context.Context, op *model.Operation) error { + return a.client.publishToTrustlog(ctx, op) +} + +// PersistenceClient 支持数据库持久化的存证客户端 +// 在原有 HighClient 功能基础上,增加了数据库持久化支持 +type PersistenceClient struct { + publisher message.Publisher + logger logger.Logger + envelopeConfig model.EnvelopeConfig + manager *PersistenceManager + cursorWorker *CursorWorker + retryWorker *RetryWorker +} + +// PersistenceClientConfig 客户端配置 +type PersistenceClientConfig struct { + // Publisher 消息发布器(用于存证) + Publisher message.Publisher + // Logger 日志记录器 + Logger logger.Logger + // EnvelopeConfig SM2密钥配置(用于签名和序列化) + EnvelopeConfig model.EnvelopeConfig + // DBConfig 数据库配置 + DBConfig DBConfig + // PersistenceConfig 持久化策略配置 + PersistenceConfig PersistenceConfig + // CursorWorkerConfig Cursor工作器配置(可选) + CursorWorkerConfig *CursorWorkerConfig + // EnableCursorWorker 是否启用Cursor工作器(仅对 StrategyDBAndTrustlog 有效) + EnableCursorWorker bool + // RetryWorkerConfig 重试工作器配置(可选) + RetryWorkerConfig *RetryWorkerConfig + // EnableRetryWorker 是否启用重试工作器(仅对 StrategyDBAndTrustlog 有效) + EnableRetryWorker bool +} + +// NewPersistenceClient 创建支持数据库持久化的存证客户端 +func NewPersistenceClient(ctx context.Context, config PersistenceClientConfig) (*PersistenceClient, error) { + if config.Logger == nil { + return nil, errors.New("logger is required") + } + + // 创建数据库连接 + db, err := NewDB(config.DBConfig) + if err != nil { + config.Logger.ErrorContext(ctx, "failed to create database connection", + "error", err, + ) + return nil, fmt.Errorf("failed to create database connection: %w", err) + } + + // 创建持久化管理器 + manager := NewPersistenceManager(db, config.PersistenceConfig, config.Logger) + + // 初始化数据库表结构 + if err := manager.InitSchema(ctx, config.DBConfig.DriverName); err != nil { + db.Close() + return nil, fmt.Errorf("failed to initialize database schema: %w", err) + } + + client := &PersistenceClient{ + publisher: config.Publisher, + logger: config.Logger, + envelopeConfig: config.EnvelopeConfig, + manager: manager, + } + + // 创建 Operation Publisher 适配器(将 PersistenceClient 的 publishToTrustlog 方法适配为 OperationPublisher 接口) + opPublisher := &operationPublisherAdapter{ + client: client, + } + manager.SetPublisher(opPublisher) + + // 如果启用Cursor工作器且策略为 StrategyDBAndTrustlog + if config.EnableCursorWorker && config.PersistenceConfig.Strategy == StrategyDBAndTrustlog { + var workerConfig CursorWorkerConfig + if config.CursorWorkerConfig != nil { + workerConfig = *config.CursorWorkerConfig + } else { + workerConfig = DefaultCursorWorkerConfig() + } + + client.cursorWorker = NewCursorWorker(workerConfig, manager) + if err := client.cursorWorker.Start(ctx); err != nil { + db.Close() + return nil, fmt.Errorf("failed to start cursor worker: %w", err) + } + + config.Logger.InfoContext(ctx, "cursor worker started", + "strategy", config.PersistenceConfig.Strategy.String(), + "scanInterval", workerConfig.ScanInterval, + ) + } + + // 如果启用重试工作器且策略为 StrategyDBAndTrustlog + if config.EnableRetryWorker && config.PersistenceConfig.Strategy == StrategyDBAndTrustlog { + var workerConfig RetryWorkerConfig + if config.RetryWorkerConfig != nil { + workerConfig = *config.RetryWorkerConfig + } else { + workerConfig = DefaultRetryWorkerConfig() + } + + client.retryWorker = NewRetryWorker(workerConfig, manager, config.Publisher, config.Logger) + go client.retryWorker.Start(ctx) + + config.Logger.InfoContext(ctx, "retry worker started", + "strategy", config.PersistenceConfig.Strategy.String(), + "retryInterval", workerConfig.RetryInterval, + ) + } + + config.Logger.InfoContext(ctx, "persistence client created", + "strategy", config.PersistenceConfig.Strategy.String(), + "cursorEnabled", config.EnableCursorWorker, + "retryEnabled", config.EnableRetryWorker, + ) + + return client, nil +} + +// OperationPublish 发布操作记录 +// 根据配置的策略,选择仅落库、既落库又存证或仅存证 +func (c *PersistenceClient) OperationPublish(ctx context.Context, operation *model.Operation) error { + if operation == nil { + c.logger.ErrorContext(ctx, "operation publish failed: operation is nil") + return errors.New("operation cannot be nil") + } + + c.logger.DebugContext(ctx, "publishing operation with persistence", + "opID", operation.OpID, + "opType", operation.OpType, + "strategy", c.manager.config.Strategy.String(), + ) + + strategy := c.manager.config.Strategy + + switch strategy { + case StrategyDBOnly: + // 仅落库 + return c.publishDBOnly(ctx, operation) + + case StrategyDBAndTrustlog: + // 既落库又存证 + return c.publishDBAndTrustlog(ctx, operation) + + case StrategyTrustlogOnly: + // 仅存证 + return c.publishTrustlogOnly(ctx, operation) + + default: + return fmt.Errorf("unknown persistence strategy: %s", strategy.String()) + } +} + +// publishDBOnly 仅落库策略 +func (c *PersistenceClient) publishDBOnly(ctx context.Context, operation *model.Operation) error { + c.logger.DebugContext(ctx, "publishing operation with DB_ONLY strategy", + "opID", operation.OpID, + ) + + // 保存到数据库 + if err := c.manager.SaveOperation(ctx, operation); err != nil { + c.logger.ErrorContext(ctx, "failed to save operation to database", + "opID", operation.OpID, + "error", err, + ) + return err + } + + c.logger.InfoContext(ctx, "operation saved with DB_ONLY strategy", + "opID", operation.OpID, + ) + return nil +} + +// publishDBAndTrustlog 既落库又存证策略 +func (c *PersistenceClient) publishDBAndTrustlog(ctx context.Context, operation *model.Operation) error { + c.logger.DebugContext(ctx, "publishing operation with DB_AND_TRUSTLOG strategy", + "opID", operation.OpID, + ) + + // 先保存到数据库(状态为未存证) + if err := c.manager.SaveOperation(ctx, operation); err != nil { + c.logger.ErrorContext(ctx, "failed to save operation to database", + "opID", operation.OpID, + "error", err, + ) + return err + } + + // 尝试发布到存证系统 + if err := c.publishToTrustlog(ctx, operation); err != nil { + c.logger.WarnContext(ctx, "failed to publish to trustlog, will retry later", + "opID", operation.OpID, + "error", err, + ) + // 发布失败,但数据库已保存,重试工作器会处理 + return nil + } + + // 发布成功,更新状态为已存证 + if err := c.manager.GetOperationRepo().UpdateStatus(ctx, operation.OpID, StatusTrustlogged); err != nil { + c.logger.ErrorContext(ctx, "failed to update operation status", + "opID", operation.OpID, + "error", err, + ) + // 状态更新失败,但消息已发送,重试工作器会清理 + } + + // 删除重试记录(如果存在) + c.manager.GetRetryRepo().DeleteRetry(ctx, operation.OpID) + + c.logger.InfoContext(ctx, "operation published with DB_AND_TRUSTLOG strategy", + "opID", operation.OpID, + ) + return nil +} + +// publishTrustlogOnly 仅存证策略 +func (c *PersistenceClient) publishTrustlogOnly(ctx context.Context, operation *model.Operation) error { + c.logger.DebugContext(ctx, "publishing operation with TRUSTLOG_ONLY strategy", + "opID", operation.OpID, + ) + + // 直接发布到存证系统 + if err := c.publishToTrustlog(ctx, operation); err != nil { + c.logger.ErrorContext(ctx, "failed to publish to trustlog", + "opID", operation.OpID, + "error", err, + ) + return err + } + + c.logger.InfoContext(ctx, "operation published with TRUSTLOG_ONLY strategy", + "opID", operation.OpID, + ) + return nil +} + +// publishToTrustlog 发布到存证系统(使用 Envelope 格式) +func (c *PersistenceClient) publishToTrustlog(ctx context.Context, operation *model.Operation) error { + messageKey := operation.Key() + + c.logger.DebugContext(ctx, "starting envelope serialization", + "messageKey", messageKey, + ) + + // 使用 Envelope 序列化 + envelopeData, err := model.MarshalTrustlog(operation, c.envelopeConfig) + if err != nil { + c.logger.ErrorContext(ctx, "envelope serialization failed", + "messageKey", messageKey, + "error", err, + ) + return fmt.Errorf("failed to marshal envelope: %w", err) + } + + c.logger.DebugContext(ctx, "envelope serialized successfully", + "messageKey", messageKey, + "envelopeSize", len(envelopeData), + ) + + msg := message.NewMessage(messageKey, envelopeData) + c.logger.DebugContext(ctx, "publishing message to topic", + "messageKey", messageKey, + "topic", adapter.OperationTopic, + ) + + if publishErr := c.publisher.Publish(adapter.OperationTopic, msg); publishErr != nil { + c.logger.ErrorContext(ctx, "failed to publish to topic", + "messageKey", messageKey, + "topic", adapter.OperationTopic, + "error", publishErr, + ) + return fmt.Errorf("failed to publish message to topic %s: %w", adapter.OperationTopic, publishErr) + } + + c.logger.DebugContext(ctx, "message published to topic successfully", + "messageKey", messageKey, + "topic", adapter.OperationTopic, + ) + return nil +} + +// RecordPublish 发布记录(Record 类型不支持数据库持久化) +func (c *PersistenceClient) RecordPublish(ctx context.Context, record *model.Record) error { + if record == nil { + c.logger.ErrorContext(ctx, "record publish failed: record is nil") + return errors.New("record cannot be nil") + } + + c.logger.DebugContext(ctx, "publishing record", + "recordID", record.ID, + "rcType", record.RCType, + ) + + // Record 类型直接发布到存证系统,不落库 + messageKey := record.Key() + + envelopeData, err := model.MarshalTrustlog(record, c.envelopeConfig) + if err != nil { + c.logger.ErrorContext(ctx, "envelope serialization failed", + "recordID", record.ID, + "error", err, + ) + return fmt.Errorf("failed to marshal envelope: %w", err) + } + + msg := message.NewMessage(messageKey, envelopeData) + if publishErr := c.publisher.Publish(adapter.RecordTopic, msg); publishErr != nil { + c.logger.ErrorContext(ctx, "failed to publish record to topic", + "recordID", record.ID, + "error", publishErr, + ) + return fmt.Errorf("failed to publish record to topic %s: %w", adapter.RecordTopic, publishErr) + } + + c.logger.InfoContext(ctx, "record published successfully", + "recordID", record.ID, + "rcType", record.RCType, + ) + return nil +} + +// GetLow 获取底层 Publisher +func (c *PersistenceClient) GetLow() message.Publisher { + return c.publisher +} + +// GetManager 获取持久化管理器 +func (c *PersistenceClient) GetManager() *PersistenceManager { + return c.manager +} + +// Close 关闭客户端 +func (c *PersistenceClient) Close() error { + ctx := context.Background() + c.logger.Info("closing persistence client") + + // 停止Cursor工作器 + if c.cursorWorker != nil { + if err := c.cursorWorker.Stop(ctx); err != nil { + c.logger.Error("failed to stop cursor worker", + "error", err, + ) + } + } + + // 停止重试工作器 + if c.retryWorker != nil { + c.retryWorker.Stop() + } + + // 关闭数据库连接 + if err := c.manager.Close(); err != nil { + c.logger.Error("failed to close database", + "error", err, + ) + return err + } + + // 关闭 Publisher + if err := c.publisher.Close(); err != nil { + c.logger.Error("failed to close publisher", + "error", err, + ) + return err + } + + c.logger.Info("persistence client closed successfully") + return nil +} + diff --git a/api/persistence/config.go b/api/persistence/config.go new file mode 100644 index 0000000..1f60675 --- /dev/null +++ b/api/persistence/config.go @@ -0,0 +1,58 @@ +package persistence + +import ( + "database/sql" + "fmt" + "time" +) + +// DBConfig 数据库配置 +type DBConfig struct { + // DriverName 数据库驱动名称,如 "postgres", "mysql", "sqlite3" 等 + DriverName string + // DSN 数据源名称(连接字符串) + DSN string + // MaxOpenConns 最大打开连接数 + MaxOpenConns int + // MaxIdleConns 最大空闲连接数 + MaxIdleConns int + // ConnMaxLifetime 连接最大生命周期 + ConnMaxLifetime time.Duration + // ConnMaxIdleTime 连接最大空闲时间 + ConnMaxIdleTime time.Duration +} + +// DefaultDBConfig 返回默认数据库配置 +func DefaultDBConfig(driverName, dsn string) DBConfig { + return DBConfig{ + DriverName: driverName, + DSN: dsn, + MaxOpenConns: 25, + MaxIdleConns: 5, + ConnMaxLifetime: time.Hour, + ConnMaxIdleTime: 10 * time.Minute, + } +} + +// NewDB 创建并配置数据库连接 +func NewDB(config DBConfig) (*sql.DB, error) { + db, err := sql.Open(config.DriverName, config.DSN) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // 配置连接池 + db.SetMaxOpenConns(config.MaxOpenConns) + db.SetMaxIdleConns(config.MaxIdleConns) + db.SetConnMaxLifetime(config.ConnMaxLifetime) + db.SetConnMaxIdleTime(config.ConnMaxIdleTime) + + // 测试连接 + if err := db.Ping(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + return db, nil +} + diff --git a/api/persistence/config_test.go b/api/persistence/config_test.go new file mode 100644 index 0000000..bc28763 --- /dev/null +++ b/api/persistence/config_test.go @@ -0,0 +1,54 @@ +package persistence + +import ( + "testing" + "time" +) + +func TestDefaultDBConfig(t *testing.T) { + config := DefaultDBConfig("postgres", "test-dsn") + + if config.DriverName != "postgres" { + t.Errorf("expected DriverName to be 'postgres', got %s", config.DriverName) + } + + if config.DSN != "test-dsn" { + t.Errorf("expected DSN to be 'test-dsn', got %s", config.DSN) + } + + if config.MaxOpenConns != 25 { + t.Errorf("expected MaxOpenConns to be 25, got %d", config.MaxOpenConns) + } + + if config.MaxIdleConns != 5 { + t.Errorf("expected MaxIdleConns to be 5, got %d", config.MaxIdleConns) + } + + if config.ConnMaxLifetime != time.Hour { + t.Errorf("expected ConnMaxLifetime to be 1 hour, got %v", config.ConnMaxLifetime) + } + + if config.ConnMaxIdleTime != 10*time.Minute { + t.Errorf("expected ConnMaxIdleTime to be 10 minutes, got %v", config.ConnMaxIdleTime) + } +} + +func TestDBConfig_CustomValues(t *testing.T) { + config := DBConfig{ + DriverName: "mysql", + DSN: "user:pass@tcp(localhost:3306)/dbname", + MaxOpenConns: 50, + MaxIdleConns: 10, + ConnMaxLifetime: 2 * time.Hour, + ConnMaxIdleTime: 20 * time.Minute, + } + + if config.DriverName != "mysql" { + t.Errorf("expected DriverName to be 'mysql', got %s", config.DriverName) + } + + if config.MaxOpenConns != 50 { + t.Errorf("expected MaxOpenConns to be 50, got %d", config.MaxOpenConns) + } +} + diff --git a/api/persistence/core_test.go b/api/persistence/core_test.go new file mode 100644 index 0000000..97083ce --- /dev/null +++ b/api/persistence/core_test.go @@ -0,0 +1,194 @@ +package persistence + +import ( + "strings" + "testing" + "time" +) + +// TestCore 测试核心功能,不依赖外部模块 + +func TestTrustlogStatusString(t *testing.T) { + if StatusNotTrustlogged != "NOT_TRUSTLOGGED" { + t.Error("StatusNotTrustlogged value incorrect") + } + if StatusTrustlogged != "TRUSTLOGGED" { + t.Error("StatusTrustlogged value incorrect") + } +} + +func TestRetryStatusString(t *testing.T) { + if RetryStatusPending != "PENDING" { + t.Error("RetryStatusPending value incorrect") + } + if RetryStatusRetrying != "RETRYING" { + t.Error("RetryStatusRetrying value incorrect") + } + if RetryStatusDeadLetter != "DEAD_LETTER" { + t.Error("RetryStatusDeadLetter value incorrect") + } +} + +func TestPersistenceStrategyValues(t *testing.T) { + if StrategyDBOnly.String() != "DB_ONLY" { + t.Errorf("StrategyDBOnly.String() = %s, want DB_ONLY", StrategyDBOnly.String()) + } + if StrategyDBAndTrustlog.String() != "DB_AND_TRUSTLOG" { + t.Errorf("StrategyDBAndTrustlog.String() = %s, want DB_AND_TRUSTLOG", StrategyDBAndTrustlog.String()) + } + if StrategyTrustlogOnly.String() != "TRUSTLOG_ONLY" { + t.Errorf("StrategyTrustlogOnly.String() = %s, want TRUSTLOG_ONLY", StrategyTrustlogOnly.String()) + } +} + +func TestDBConfigDefaults(t *testing.T) { + cfg := DefaultDBConfig("postgres", "test-dsn") + + if cfg.DriverName != "postgres" { + t.Error("DriverName not set correctly") + } + if cfg.DSN != "test-dsn" { + t.Error("DSN not set correctly") + } + if cfg.MaxOpenConns != 25 { + t.Error("MaxOpenConns not set to default 25") + } + if cfg.MaxIdleConns != 5 { + t.Error("MaxIdleConns not set to default 5") + } + if cfg.ConnMaxLifetime != time.Hour { + t.Error("ConnMaxLifetime not set to default 1 hour") + } + if cfg.ConnMaxIdleTime != 10*time.Minute { + t.Error("ConnMaxIdleTime not set to default 10 minutes") + } +} + +func TestPersistenceConfigDefaults(t *testing.T) { + cfg := DefaultPersistenceConfig(StrategyDBAndTrustlog) + + if cfg.Strategy != StrategyDBAndTrustlog { + t.Error("Strategy not set correctly") + } + if !cfg.EnableRetry { + t.Error("EnableRetry should be true by default") + } + if cfg.MaxRetryCount != 5 { + t.Error("MaxRetryCount should be 5 by default") + } + if cfg.RetryBatchSize != 100 { + t.Error("RetryBatchSize should be 100 by default") + } +} + +func TestRetryWorkerConfigDefaults(t *testing.T) { + cfg := DefaultRetryWorkerConfig() + + if cfg.RetryInterval != 30*time.Second { + t.Error("RetryInterval should be 30s by default") + } + if cfg.MaxRetryCount != 5 { + t.Error("MaxRetryCount should be 5 by default") + } + if cfg.BatchSize != 100 { + t.Error("BatchSize should be 100 by default") + } + if cfg.BackoffMultiplier != 2.0 { + t.Error("BackoffMultiplier should be 2.0 by default") + } + if cfg.InitialBackoff != 1*time.Minute { + t.Error("InitialBackoff should be 1 minute by default") + } +} + +func TestDDLContainsRequiredTables(t *testing.T) { + // Test operation table + if !strings.Contains(OperationTableDDL, "CREATE TABLE") { + t.Error("OperationTableDDL should contain CREATE TABLE") + } + if !strings.Contains(OperationTableDDL, "operation") { + t.Error("OperationTableDDL should create operation table") + } + if !strings.Contains(OperationTableDDL, "client_ip") { + t.Error("OperationTableDDL should have client_ip field") + } + if !strings.Contains(OperationTableDDL, "server_ip") { + t.Error("OperationTableDDL should have server_ip field") + } + if !strings.Contains(OperationTableDDL, "trustlog_status") { + t.Error("OperationTableDDL should have trustlog_status field") + } + + // Test cursor table + if !strings.Contains(CursorTableDDL, "trustlog_cursor") { + t.Error("CursorTableDDL should create trustlog_cursor table") + } + if !strings.Contains(CursorTableDDL, "cursor_key") { + t.Error("CursorTableDDL should have cursor_key field") + } + if !strings.Contains(CursorTableDDL, "cursor_value") { + t.Error("CursorTableDDL should have cursor_value field") + } + + // Test retry table + if !strings.Contains(RetryTableDDL, "trustlog_retry") { + t.Error("RetryTableDDL should create trustlog_retry table") + } + if !strings.Contains(RetryTableDDL, "retry_status") { + t.Error("RetryTableDDL should have retry_status field") + } + if !strings.Contains(RetryTableDDL, "retry_count") { + t.Error("RetryTableDDL should have retry_count field") + } +} + +func TestGetDialectDDLForDifferentDrivers(t *testing.T) { + drivers := []string{"postgres", "mysql", "sqlite3", "sqlite", "unknown"} + + for _, driver := range drivers { + opDDL, cursorDDL, retryDDL, err := GetDialectDDL(driver) + if err != nil { + t.Errorf("GetDialectDDL(%s) returned error: %v", driver, err) + } + if opDDL == "" { + t.Errorf("GetDialectDDL(%s) returned empty operation DDL", driver) + } + if cursorDDL == "" { + t.Errorf("GetDialectDDL(%s) returned empty cursor DDL", driver) + } + if retryDDL == "" { + t.Errorf("GetDialectDDL(%s) returned empty retry DDL", driver) + } + } +} + +func TestMySQLDDLHasSpecificSyntax(t *testing.T) { + opDDL, _, _, _ := GetDialectDDL("mysql") + + if !strings.Contains(opDDL, "ENGINE=InnoDB") { + t.Error("MySQL DDL should contain ENGINE=InnoDB") + } + if !strings.Contains(opDDL, "CHARSET=utf8mb4") { + t.Error("MySQL DDL should contain CHARSET=utf8mb4") + } +} + +func TestPostgresDDLHasCorrectTypes(t *testing.T) { + opDDL, _, _, _ := GetDialectDDL("postgres") + + if !strings.Contains(opDDL, "VARCHAR") { + t.Error("Postgres DDL should use VARCHAR types") + } + if !strings.Contains(opDDL, "TIMESTAMP") { + t.Error("Postgres DDL should use TIMESTAMP types") + } +} + +func TestSQLiteDDLUsesTEXT(t *testing.T) { + opDDL, _, _, _ := GetDialectDDL("sqlite3") + + if !strings.Contains(opDDL, "TEXT") { + t.Error("SQLite DDL should use TEXT types") + } +} + diff --git a/api/persistence/cursor_worker.go b/api/persistence/cursor_worker.go new file mode 100644 index 0000000..574c0bd --- /dev/null +++ b/api/persistence/cursor_worker.go @@ -0,0 +1,387 @@ +package persistence + +import ( + "context" + "database/sql" + "fmt" + "time" + + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +// OperationRecord 操作记录(包含数据库扩展字段) +type OperationRecord struct { + OpID string + OpActor string + DOID string + ProducerID string + RequestBodyHash string + ResponseBodyHash string + OpHash string + Sign string + OpSource string + OpType string + DOPrefix string + DORepository string + ClientIP *string + ServerIP *string + TrustlogStatus string + CreatedAt time.Time +} + +// ToModel 转换为 model.Operation +func (r *OperationRecord) ToModel() *model.Operation { + return &model.Operation{ + OpID: r.OpID, + OpActor: r.OpActor, + Doid: r.DOID, + ProducerID: r.ProducerID, + RequestBodyHash: &r.RequestBodyHash, + ResponseBodyHash: &r.ResponseBodyHash, + OpSource: model.Source(r.OpSource), + OpType: model.Type(r.OpType), + DoPrefix: r.DOPrefix, + DoRepository: r.DORepository, + ClientIP: r.ClientIP, + ServerIP: r.ServerIP, + } +} + +// CursorWorkerConfig Cursor工作器配置 +type CursorWorkerConfig struct { + // ScanInterval 扫描间隔(默认10秒,快速发现新记录) + ScanInterval time.Duration + // BatchSize 批量处理大小(默认100) + BatchSize int + // CursorKey Cursor键(默认 "operation_scan") + CursorKey string + // MaxRetryAttempt Cursor阶段最大重试次数(默认1,快速失败转入Retry) + MaxRetryAttempt int + // Enabled 是否启用Cursor工作器(默认启用) + Enabled bool +} + +// DefaultCursorWorkerConfig 默认Cursor工作器配置 +func DefaultCursorWorkerConfig() CursorWorkerConfig { + return CursorWorkerConfig{ + ScanInterval: 10 * time.Second, + BatchSize: 100, + CursorKey: "operation_scan", + MaxRetryAttempt: 1, + Enabled: true, + } +} + +// CursorWorker Cursor工作器(任务发现) +// 职责:扫描operation表,发现新的待存证记录,尝试存证 +// 成功则更新状态,失败则加入重试表 +type CursorWorker struct { + config CursorWorkerConfig + manager *PersistenceManager + logger logger.Logger + stopCh chan struct{} +} + +// NewCursorWorker 创建Cursor工作器 +func NewCursorWorker(config CursorWorkerConfig, manager *PersistenceManager) *CursorWorker { + if config.ScanInterval == 0 { + config.ScanInterval = 10 * time.Second + } + if config.BatchSize == 0 { + config.BatchSize = 100 + } + if config.CursorKey == "" { + config.CursorKey = "operation_scan" + } + if config.MaxRetryAttempt == 0 { + config.MaxRetryAttempt = 1 + } + + return &CursorWorker{ + config: config, + manager: manager, + logger: manager.logger, + stopCh: make(chan struct{}), + } +} + +// Start 启动Cursor工作器 +func (w *CursorWorker) Start(ctx context.Context) error { + if !w.config.Enabled { + w.logger.InfoContext(ctx, "cursor worker disabled, skipping start") + return nil + } + + w.logger.InfoContext(ctx, "starting cursor worker", + "scanInterval", w.config.ScanInterval, + "batchSize", w.config.BatchSize, + "cursorKey", w.config.CursorKey, + ) + + // 初始化cursor(如果不存在) + if err := w.initCursor(ctx); err != nil { + return fmt.Errorf("failed to init cursor: %w", err) + } + + // 启动定期扫描 + go w.run(ctx) + + return nil +} + +// Stop 停止Cursor工作器 +func (w *CursorWorker) Stop(ctx context.Context) error { + w.logger.InfoContext(ctx, "stopping cursor worker") + close(w.stopCh) + return nil +} + +// run 运行循环 +func (w *CursorWorker) run(ctx context.Context) { + ticker := time.NewTicker(w.config.ScanInterval) + defer ticker.Stop() + + for { + select { + case <-w.stopCh: + w.logger.InfoContext(ctx, "cursor worker stopped") + return + case <-ticker.C: + w.scan(ctx) + } + } +} + +// scan 扫描并处理未存证记录 +func (w *CursorWorker) scan(ctx context.Context) { + w.logger.DebugContext(ctx, "cursor worker scanning", + "cursorKey", w.config.CursorKey, + ) + + // 1. 读取cursor + cursor, err := w.getCursor(ctx) + if err != nil { + w.logger.ErrorContext(ctx, "failed to get cursor", + "error", err, + ) + return + } + + w.logger.DebugContext(ctx, "cursor position", + "cursor", cursor, + ) + + // 2. 扫描新记录 + operations, err := w.findNewOperations(ctx, cursor) + if err != nil { + w.logger.ErrorContext(ctx, "failed to find new operations", + "error", err, + ) + return + } + + if len(operations) == 0 { + w.logger.DebugContext(ctx, "no new operations found") + return + } + + w.logger.InfoContext(ctx, "found new operations", + "count", len(operations), + ) + + // 3. 处理每条记录 + for _, op := range operations { + w.processOperation(ctx, op) + } +} + +// initCursor 初始化cursor +func (w *CursorWorker) initCursor(ctx context.Context) error { + cursorRepo := w.manager.GetCursorRepo() + + // 创建初始cursor(使用当前时间) + now := time.Now().Format(time.RFC3339Nano) + err := cursorRepo.InitCursor(ctx, w.config.CursorKey, now) + if err != nil { + return fmt.Errorf("failed to init cursor: %w", err) + } + + w.logger.InfoContext(ctx, "cursor initialized", + "cursorKey", w.config.CursorKey, + "initialValue", now, + ) + + return nil +} + +// getCursor 获取cursor值 +func (w *CursorWorker) getCursor(ctx context.Context) (string, error) { + cursorRepo := w.manager.GetCursorRepo() + + cursor, err := cursorRepo.GetCursor(ctx, w.config.CursorKey) + if err != nil { + return "", fmt.Errorf("failed to get cursor: %w", err) + } + + // 如果cursor为空,使用一个很早的时间 + if cursor == "" { + cursor = time.Time{}.Format(time.RFC3339Nano) + } + + return cursor, nil +} + +// updateCursor 更新cursor值 +func (w *CursorWorker) updateCursor(ctx context.Context, value string) error { + cursorRepo := w.manager.GetCursorRepo() + + err := cursorRepo.UpdateCursor(ctx, w.config.CursorKey, value) + if err != nil { + return fmt.Errorf("failed to update cursor: %w", err) + } + + w.logger.DebugContext(ctx, "cursor updated", + "cursorKey", w.config.CursorKey, + "newValue", value, + ) + + return nil +} + +// findNewOperations 查找新的待存证记录 +func (w *CursorWorker) findNewOperations(ctx context.Context, cursor string) ([]*OperationRecord, error) { + db := w.manager.db + + // 查询未存证的记录(created_at > cursor) + rows, err := db.QueryContext(ctx, ` + SELECT op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, op_hash, sign, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, trustlog_status, created_at + FROM operation + WHERE trustlog_status = $1 + AND created_at > $2 + ORDER BY created_at ASC + LIMIT $3 + `, StatusNotTrustlogged, cursor, w.config.BatchSize) + if err != nil { + return nil, fmt.Errorf("failed to query operations: %w", err) + } + defer rows.Close() + + var operations []*OperationRecord + for rows.Next() { + op := &OperationRecord{} + var clientIP, serverIP sql.NullString + var createdAt time.Time + + err := rows.Scan( + &op.OpID, &op.OpActor, &op.DOID, &op.ProducerID, + &op.RequestBodyHash, &op.ResponseBodyHash, &op.OpHash, &op.Sign, + &op.OpSource, &op.OpType, &op.DOPrefix, &op.DORepository, + &clientIP, &serverIP, &op.TrustlogStatus, &createdAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan operation: %w", err) + } + + // 处理可空字段 + if clientIP.Valid { + op.ClientIP = &clientIP.String + } + if serverIP.Valid { + op.ServerIP = &serverIP.String + } + op.CreatedAt = createdAt + + operations = append(operations, op) + } + + return operations, nil +} + +// processOperation 处理单条记录 +func (w *CursorWorker) processOperation(ctx context.Context, op *OperationRecord) { + w.logger.DebugContext(ctx, "processing operation", + "opID", op.OpID, + ) + + // 尝试存证(最多重试 MaxRetryAttempt 次) + var lastErr error + for attempt := 0; attempt <= w.config.MaxRetryAttempt; attempt++ { + if attempt > 0 { + w.logger.DebugContext(ctx, "retrying trustlog", + "opID", op.OpID, + "attempt", attempt, + ) + } + + err := w.tryTrustlog(ctx, op) + if err == nil { + // 成功:更新状态 + if err := w.updateOperationStatus(ctx, op.OpID, StatusTrustlogged); err != nil { + w.logger.ErrorContext(ctx, "failed to update operation status", + "opID", op.OpID, + "error", err, + ) + } else { + w.logger.InfoContext(ctx, "operation trustlogged successfully", + "opID", op.OpID, + ) + } + + // 更新cursor + w.updateCursor(ctx, op.CreatedAt.Format(time.RFC3339Nano)) + return + } + + lastErr = err + if attempt < w.config.MaxRetryAttempt { + time.Sleep(time.Second) // 简单的重试延迟 + } + } + + // 失败:加入重试表 + w.logger.WarnContext(ctx, "failed to trustlog in cursor worker, adding to retry queue", + "opID", op.OpID, + "error", lastErr, + ) + + retryRepo := w.manager.GetRetryRepo() + nextRetryAt := time.Now().Add(1 * time.Minute) // 1分钟后重试 + if err := retryRepo.AddRetry(ctx, op.OpID, lastErr.Error(), nextRetryAt); err != nil { + w.logger.ErrorContext(ctx, "failed to add to retry queue", + "opID", op.OpID, + "error", err, + ) + } + + // 即使失败也更新cursor(避免卡在同一条记录) + w.updateCursor(ctx, op.CreatedAt.Format(time.RFC3339Nano)) +} + +// tryTrustlog 尝试存证(调用存证系统) +func (w *CursorWorker) tryTrustlog(ctx context.Context, op *OperationRecord) error { + publisher := w.manager.GetPublisher() + if publisher == nil { + return fmt.Errorf("publisher not available") + } + + // 转换为 Operation 模型 + modelOp := op.ToModel() + + // 调用存证 + if err := publisher.Publish(ctx, modelOp); err != nil { + return fmt.Errorf("failed to publish to trustlog: %w", err) + } + + return nil +} + +// updateOperationStatus 更新操作状态 +func (w *CursorWorker) updateOperationStatus(ctx context.Context, opID string, status TrustlogStatus) error { + opRepo := w.manager.GetOperationRepo() + return opRepo.UpdateStatus(ctx, opID, status) +} + diff --git a/api/persistence/example_test.go b/api/persistence/example_test.go new file mode 100644 index 0000000..eacd12b --- /dev/null +++ b/api/persistence/example_test.go @@ -0,0 +1,378 @@ +package persistence_test + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + + "go.yandata.net/iod/iod/go-trustlog/api/adapter" + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" + "go.yandata.net/iod/iod/go-trustlog/api/persistence" +) + +// Example_dbOnly 演示仅落库策略 +func Example_dbOnly() { + ctx := context.Background() + + // 1. 创建 Logger + myLogger := logger.NewLogger(logr.Discard()) + + // 2. 创建 Pulsar Publisher + pub, err := adapter.NewPublisher( + adapter.PublisherConfig{ + URL: "pulsar://localhost:6650", + }, + myLogger, + ) + if err != nil { + panic(err) + } + defer pub.Close() + + // 3. 准备 SM2 密钥配置 + privateKeyHex := []byte("私钥D的十六进制字符串") + publicKeyHex := []byte("04 + x坐标 + y坐标的十六进制字符串") + envelopeConfig := model.NewSM2EnvelopeConfig(privateKeyHex, publicKeyHex) + + // 4. 创建支持数据库持久化的客户端(仅落库策略) + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: pub, + Logger: myLogger, + EnvelopeConfig: envelopeConfig, + DBConfig: persistence.DefaultDBConfig( + "postgres", + "postgres://user:pass@localhost:5432/trustlog?sslmode=disable", + ), + PersistenceConfig: persistence.DefaultPersistenceConfig(persistence.StrategyDBOnly), + EnableRetryWorker: false, // 仅落库不需要重试 + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 5. 构造 Operation(包含 IP 信息) + op, err := model.NewFullOperation( + model.OpSourceDOIP, + model.OpTypeCreate, + "10.1000", + "my-repo", + "10.1000/my-repo/doc001", + "producer-001", + "admin", + []byte(`{"action":"create"}`), + []byte(`{"status":"success"}`), + time.Now(), + ) + if err != nil { + panic(err) + } + + // 设置 IP 信息(仅落库字段,可空) + clientIP := "192.168.1.100" + serverIP := "10.0.0.50" + op.ClientIP = &clientIP + op.ServerIP = &serverIP + + // 6. 发布操作(仅落库,不存证) + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + fmt.Printf("Operation saved to database only: %s\n", op.OpID) +} + +// Example_dbAndTrustlog 演示既落库又存证策略 +func Example_dbAndTrustlog() { + ctx := context.Background() + + // 1. 创建 Logger + myLogger := logger.NewLogger(logr.Discard()) + + // 2. 创建 Pulsar Publisher + pub, err := adapter.NewPublisher( + adapter.PublisherConfig{ + URL: "pulsar://localhost:6650", + }, + myLogger, + ) + if err != nil { + panic(err) + } + defer pub.Close() + + // 3. 准备 SM2 密钥配置 + privateKeyHex := []byte("私钥D的十六进制字符串") + publicKeyHex := []byte("04 + x坐标 + y坐标的十六进制字符串") + envelopeConfig := model.NewSM2EnvelopeConfig(privateKeyHex, publicKeyHex) + + // 4. 创建支持数据库持久化的客户端(既落库又存证策略) + retryConfig := persistence.DefaultRetryWorkerConfig() + retryConfig.MaxRetryCount = 5 + retryConfig.RetryInterval = 30 * time.Second + + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: pub, + Logger: myLogger, + EnvelopeConfig: envelopeConfig, + DBConfig: persistence.DefaultDBConfig( + "postgres", + "postgres://user:pass@localhost:5432/trustlog?sslmode=disable", + ), + PersistenceConfig: persistence.PersistenceConfig{ + Strategy: persistence.StrategyDBAndTrustlog, + EnableRetry: true, + MaxRetryCount: 5, + RetryBatchSize: 100, + }, + RetryWorkerConfig: &retryConfig, + EnableRetryWorker: true, // 启用重试工作器保证最终一致性 + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 5. 构造 Operation + op, err := model.NewFullOperation( + model.OpSourceDOIP, + model.OpTypeCreate, + "10.1000", + "my-repo", + "10.1000/my-repo/doc002", + "producer-001", + "admin", + []byte(`{"action":"create"}`), + []byte(`{"status":"success"}`), + time.Now(), + ) + if err != nil { + panic(err) + } + + // 设置 IP 信息(可空) + clientIP := "192.168.1.100" + serverIP := "10.0.0.50" + op.ClientIP = &clientIP + op.ServerIP = &serverIP + + // 6. 发布操作(既落库又存证,保证最终一致性) + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + fmt.Printf("Operation saved to database and published to trustlog: %s\n", op.OpID) + fmt.Println("If publish fails, retry worker will handle it automatically") +} + +// Example_trustlogOnly 演示仅存证策略 +func Example_trustlogOnly() { + ctx := context.Background() + + // 1. 创建 Logger + myLogger := logger.NewLogger(logr.Discard()) + + // 2. 创建 Pulsar Publisher + pub, err := adapter.NewPublisher( + adapter.PublisherConfig{ + URL: "pulsar://localhost:6650", + }, + myLogger, + ) + if err != nil { + panic(err) + } + defer pub.Close() + + // 3. 准备 SM2 密钥配置 + privateKeyHex := []byte("私钥D的十六进制字符串") + publicKeyHex := []byte("04 + x坐标 + y坐标的十六进制字符串") + envelopeConfig := model.NewSM2EnvelopeConfig(privateKeyHex, publicKeyHex) + + // 4. 创建支持数据库持久化的客户端(仅存证策略) + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: pub, + Logger: myLogger, + EnvelopeConfig: envelopeConfig, + DBConfig: persistence.DefaultDBConfig( + "postgres", + "postgres://user:pass@localhost:5432/trustlog?sslmode=disable", + ), + PersistenceConfig: persistence.DefaultPersistenceConfig(persistence.StrategyTrustlogOnly), + EnableRetryWorker: false, // 仅存证不需要重试工作器 + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 5. 构造 Operation + op, err := model.NewFullOperation( + model.OpSourceDOIP, + model.OpTypeCreate, + "10.1000", + "my-repo", + "10.1000/my-repo/doc003", + "producer-001", + "admin", + []byte(`{"action":"create"}`), + []byte(`{"status":"success"}`), + time.Now(), + ) + if err != nil { + panic(err) + } + + // 6. 发布操作(仅存证,不落库) + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + fmt.Printf("Operation published to trustlog only: %s\n", op.OpID) +} + +// Example_mysqlDatabase 演示使用 MySQL 数据库 +func Example_mysqlDatabase() { + ctx := context.Background() + + // 1. 创建 Logger + myLogger := logger.NewLogger(logr.Discard()) + + // 2. 创建 Pulsar Publisher + pub, err := adapter.NewPublisher( + adapter.PublisherConfig{ + URL: "pulsar://localhost:6650", + }, + myLogger, + ) + if err != nil { + panic(err) + } + defer pub.Close() + + // 3. 准备 SM2 密钥配置 + privateKeyHex := []byte("私钥D的十六进制字符串") + publicKeyHex := []byte("04 + x坐标 + y坐标的十六进制字符串") + envelopeConfig := model.NewSM2EnvelopeConfig(privateKeyHex, publicKeyHex) + + // 4. 创建支持 MySQL 数据库的客户端 + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: pub, + Logger: myLogger, + EnvelopeConfig: envelopeConfig, + DBConfig: persistence.DefaultDBConfig( + "mysql", + "user:pass@tcp(localhost:3306)/trustlog?parseTime=true", + ), + PersistenceConfig: persistence.DefaultPersistenceConfig(persistence.StrategyDBAndTrustlog), + EnableRetryWorker: true, + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 5. 构造并发布 Operation + op, err := model.NewFullOperation( + model.OpSourceDOIP, + model.OpTypeCreate, + "10.1000", + "my-repo", + "10.1000/my-repo/doc004", + "producer-001", + "admin", + []byte(`{"action":"create"}`), + []byte(`{"status":"success"}`), + time.Now(), + ) + if err != nil { + panic(err) + } + + // 设置 IP 信息(可空) + clientIP := "192.168.1.100" + serverIP := "10.0.0.50" + op.ClientIP = &clientIP + op.ServerIP = &serverIP + + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + fmt.Printf("Operation saved to MySQL and published: %s\n", op.OpID) +} + +// Example_sqliteDatabase 演示使用 SQLite 数据库 +func Example_sqliteDatabase() { + ctx := context.Background() + + // 1. 创建 Logger + myLogger := logger.NewLogger(logr.Discard()) + + // 2. 创建 Pulsar Publisher + pub, err := adapter.NewPublisher( + adapter.PublisherConfig{ + URL: "pulsar://localhost:6650", + }, + myLogger, + ) + if err != nil { + panic(err) + } + defer pub.Close() + + // 3. 准备 SM2 密钥配置 + privateKeyHex := []byte("私钥D的十六进制字符串") + publicKeyHex := []byte("04 + x坐标 + y坐标的十六进制字符串") + envelopeConfig := model.NewSM2EnvelopeConfig(privateKeyHex, publicKeyHex) + + // 4. 创建支持 SQLite 数据库的客户端 + client, err := persistence.NewPersistenceClient(ctx, persistence.PersistenceClientConfig{ + Publisher: pub, + Logger: myLogger, + EnvelopeConfig: envelopeConfig, + DBConfig: persistence.DefaultDBConfig( + "sqlite3", + "./trustlog.db", + ), + PersistenceConfig: persistence.DefaultPersistenceConfig(persistence.StrategyDBOnly), + EnableRetryWorker: false, + }) + if err != nil { + panic(err) + } + defer client.Close() + + // 5. 构造并发布 Operation + op, err := model.NewFullOperation( + model.OpSourceDOIP, + model.OpTypeCreate, + "10.1000", + "my-repo", + "10.1000/my-repo/doc005", + "producer-001", + "admin", + []byte(`{"action":"create"}`), + []byte(`{"status":"success"}`), + time.Now(), + ) + if err != nil { + panic(err) + } + + // 设置 IP 信息(可空) + clientIP := "192.168.1.100" + serverIP := "10.0.0.50" + op.ClientIP = &clientIP + op.ServerIP = &serverIP + + if err := client.OperationPublish(ctx, op); err != nil { + panic(err) + } + + fmt.Printf("Operation saved to SQLite: %s\n", op.OpID) +} + diff --git a/api/persistence/minimal_test.go b/api/persistence/minimal_test.go new file mode 100644 index 0000000..dc6dae0 --- /dev/null +++ b/api/persistence/minimal_test.go @@ -0,0 +1,369 @@ +package persistence + +import ( + "context" + "database/sql" + "strings" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// 最小化测试套件 - 不依赖任何复杂模块 +// 这些测试可以直接运行: go test -v -run Minimal ./api/persistence/ + +func TestMinimalConfigDefaults(t *testing.T) { + cfg := DefaultDBConfig("postgres", "test-dsn") + if cfg.DriverName != "postgres" { + t.Errorf("expected DriverName=postgres, got %s", cfg.DriverName) + } + if cfg.MaxOpenConns != 25 { + t.Errorf("expected MaxOpenConns=25, got %d", cfg.MaxOpenConns) + } +} + +func TestMinimalStrategyString(t *testing.T) { + tests := []struct { + strategy PersistenceStrategy + expected string + }{ + {StrategyDBOnly, "DB_ONLY"}, + {StrategyDBAndTrustlog, "DB_AND_TRUSTLOG"}, + {StrategyTrustlogOnly, "TRUSTLOG_ONLY"}, + } + + for _, tt := range tests { + if tt.strategy.String() != tt.expected { + t.Errorf("strategy.String() = %s, want %s", tt.strategy.String(), tt.expected) + } + } +} + +func TestMinimalStatusEnums(t *testing.T) { + if StatusNotTrustlogged != "NOT_TRUSTLOGGED" { + t.Error("StatusNotTrustlogged incorrect") + } + if StatusTrustlogged != "TRUSTLOGGED" { + t.Error("StatusTrustlogged incorrect") + } + if RetryStatusPending != "PENDING" { + t.Error("RetryStatusPending incorrect") + } +} + +func TestMinimalDDLGeneration(t *testing.T) { + drivers := []string{"postgres", "mysql", "sqlite3"} + for _, driver := range drivers { + opDDL, cursorDDL, retryDDL, err := GetDialectDDL(driver) + if err != nil { + t.Fatalf("GetDialectDDL(%s) failed: %v", driver, err) + } + if len(opDDL) == 0 { + t.Errorf("%s: operation DDL is empty", driver) + } + if len(cursorDDL) == 0 { + t.Errorf("%s: cursor DDL is empty", driver) + } + if len(retryDDL) == 0 { + t.Errorf("%s: retry DDL is empty", driver) + } + } +} + +func TestMinimalDDLContent(t *testing.T) { + opDDL, _, _, _ := GetDialectDDL("postgres") + + requiredFields := []string{ + "op_id", "client_ip", "server_ip", "trustlog_status", + } + + for _, field := range requiredFields { + if !strings.Contains(opDDL, field) { + t.Errorf("operation DDL missing field: %s", field) + } + } +} + +func TestMinimalDatabaseCreation(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + // 获取 DDL + opDDL, cursorDDL, retryDDL, err := GetDialectDDL("sqlite3") + if err != nil { + t.Fatalf("GetDialectDDL failed: %v", err) + } + + // 创建表 + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create operation table: %v", err) + } + if _, err := db.Exec(cursorDDL); err != nil { + t.Fatalf("failed to create cursor table: %v", err) + } + if _, err := db.Exec(retryDDL); err != nil { + t.Fatalf("failed to create retry table: %v", err) + } + + // 验证表存在 + tables := []string{"operation", "trustlog_cursor", "trustlog_retry"} + for _, table := range tables { + var name string + err = db.QueryRow( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?", + table, + ).Scan(&name) + if err != nil { + t.Errorf("table %s not found: %v", table, err) + } + } +} + +func TestMinimalNullableIPFields(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + // 创建表 + opDDL, _, _, _ := GetDialectDDL("sqlite3") + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create table: %v", err) + } + + ctx := context.Background() + + // 测试1: 插入 NULL IP + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp, + client_ip, server_ip + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-001", "10.1000/repo/obj", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now(), nil, nil) + + if err != nil { + t.Fatalf("failed to insert with NULL IPs: %v", err) + } + + // 验证 NULL + var clientIP, serverIP sql.NullString + err = db.QueryRowContext(ctx, + "SELECT client_ip, server_ip FROM operation WHERE op_id = ?", + "test-001", + ).Scan(&clientIP, &serverIP) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if clientIP.Valid { + t.Error("client_ip should be NULL") + } + if serverIP.Valid { + t.Error("server_ip should be NULL") + } + + // 测试2: 插入非 NULL IP + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp, + client_ip, server_ip + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-002", "10.1000/repo/obj2", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now(), + "192.168.1.100", "10.0.0.50") + + if err != nil { + t.Fatalf("failed to insert with IP values: %v", err) + } + + // 验证非 NULL + err = db.QueryRowContext(ctx, + "SELECT client_ip, server_ip FROM operation WHERE op_id = ?", + "test-002", + ).Scan(&clientIP, &serverIP) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if !clientIP.Valid || clientIP.String != "192.168.1.100" { + t.Errorf("client_ip should be '192.168.1.100', got %v", clientIP) + } + if !serverIP.Valid || serverIP.String != "10.0.0.50" { + t.Errorf("server_ip should be '10.0.0.50', got %v", serverIP) + } +} + +func TestMinimalCursorTableInit(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + _, cursorDDL, _, _ := GetDialectDDL("sqlite3") + if _, err := db.Exec(cursorDDL); err != nil { + t.Fatalf("failed to create cursor table: %v", err) + } + + // 验证表结构(不再自动插入初始记录) + var count int + err = db.QueryRow("SELECT COUNT(*) FROM trustlog_cursor").Scan(&count) + if err != nil { + t.Fatalf("failed to count: %v", err) + } + + if count != 0 { + t.Errorf("expected 0 initial cursor records (empty table), got %d", count) + } + + // 测试插入新记录 + _, err = db.Exec(`INSERT INTO trustlog_cursor (cursor_key, cursor_value) VALUES (?, ?)`, + "test-cursor", time.Now().Format(time.RFC3339Nano)) + if err != nil { + t.Fatalf("failed to insert cursor: %v", err) + } + + // 验证插入 + err = db.QueryRow("SELECT COUNT(*) FROM trustlog_cursor WHERE cursor_key = ?", "test-cursor").Scan(&count) + if err != nil { + t.Fatalf("failed to count after insert: %v", err) + } + + if count != 1 { + t.Errorf("expected 1 cursor record after insert, got %d", count) + } +} + +func TestMinimalRetryStatusUpdate(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + _, _, retryDDL, _ := GetDialectDDL("sqlite3") + if _, err := db.Exec(retryDDL); err != nil { + t.Fatalf("failed to create retry table: %v", err) + } + + ctx := context.Background() + + // 插入重试记录 + _, err = db.ExecContext(ctx, ` + INSERT INTO trustlog_retry (op_id, retry_count, retry_status, next_retry_at) + VALUES (?, ?, ?, ?) + `, "test-op-001", 0, "PENDING", time.Now().Add(1*time.Minute)) + + if err != nil { + t.Fatalf("failed to insert retry record: %v", err) + } + + // 更新状态 + _, err = db.ExecContext(ctx, ` + UPDATE trustlog_retry + SET retry_count = retry_count + 1, retry_status = ? + WHERE op_id = ? + `, "RETRYING", "test-op-001") + + if err != nil { + t.Fatalf("failed to update retry: %v", err) + } + + // 验证更新 + var count int + var status string + err = db.QueryRowContext(ctx, + "SELECT retry_count, retry_status FROM trustlog_retry WHERE op_id = ?", + "test-op-001", + ).Scan(&count, &status) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if count != 1 { + t.Errorf("expected retry_count=1, got %d", count) + } + if status != "RETRYING" { + t.Errorf("expected status=RETRYING, got %s", status) + } +} + +func TestMinimalOperationStatusFlow(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + opDDL, _, _, _ := GetDialectDDL("sqlite3") + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create table: %v", err) + } + + ctx := context.Background() + + // 插入未存证记录 + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-001", "10.1000/repo/obj", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now()) + + if err != nil { + t.Fatalf("failed to insert: %v", err) + } + + // 查询未存证记录 + var count int + err = db.QueryRowContext(ctx, + "SELECT COUNT(*) FROM operation WHERE trustlog_status = ?", + "NOT_TRUSTLOGGED", + ).Scan(&count) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if count != 1 { + t.Errorf("expected 1 untrustlogged operation, got %d", count) + } + + // 更新为已存证 + _, err = db.ExecContext(ctx, + "UPDATE operation SET trustlog_status = ? WHERE op_id = ?", + "TRUSTLOGGED", "test-001", + ) + + if err != nil { + t.Fatalf("failed to update: %v", err) + } + + // 验证更新 + var status string + err = db.QueryRowContext(ctx, + "SELECT trustlog_status FROM operation WHERE op_id = ?", + "test-001", + ).Scan(&status) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if status != "TRUSTLOGGED" { + t.Errorf("expected status=TRUSTLOGGED, got %s", status) + } +} + diff --git a/api/persistence/repository.go b/api/persistence/repository.go new file mode 100644 index 0000000..6a9a71a --- /dev/null +++ b/api/persistence/repository.go @@ -0,0 +1,605 @@ +package persistence + +import ( + "context" + "database/sql" + "fmt" + "time" + + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +// OperationRepository 操作记录数据库仓储接口 +type OperationRepository interface { + // Save 保存操作记录到数据库 + Save(ctx context.Context, op *model.Operation, status TrustlogStatus) error + // SaveTx 在事务中保存操作记录 + SaveTx(ctx context.Context, tx *sql.Tx, op *model.Operation, status TrustlogStatus) error + // UpdateStatus 更新操作记录的存证状态 + UpdateStatus(ctx context.Context, opID string, status TrustlogStatus) error + // UpdateStatusTx 在事务中更新操作记录的存证状态 + UpdateStatusTx(ctx context.Context, tx *sql.Tx, opID string, status TrustlogStatus) error + // FindByID 根据 OpID 查询操作记录 + FindByID(ctx context.Context, opID string) (*model.Operation, TrustlogStatus, error) + // FindUntrustlogged 查询未存证的操作记录(用于重试机制) + FindUntrustlogged(ctx context.Context, limit int) ([]*model.Operation, error) +} + +// CursorRepository 游标仓储接口(Key-Value 模式) +type CursorRepository interface { + // GetCursor 获取游标值 + GetCursor(ctx context.Context, cursorKey string) (string, error) + // UpdateCursor 更新游标值 + UpdateCursor(ctx context.Context, cursorKey string, cursorValue string) error + // UpdateCursorTx 在事务中更新游标值 + UpdateCursorTx(ctx context.Context, tx *sql.Tx, cursorKey string, cursorValue string) error + // InitCursor 初始化游标(如果不存在) + InitCursor(ctx context.Context, cursorKey string, initialValue string) error +} + +// RetryRepository 重试仓储接口 +type RetryRepository interface { + // AddRetry 添加重试记录 + AddRetry(ctx context.Context, opID string, errorMsg string, nextRetryAt time.Time) error + // AddRetryTx 在事务中添加重试记录 + AddRetryTx(ctx context.Context, tx *sql.Tx, opID string, errorMsg string, nextRetryAt time.Time) error + // IncrementRetry 增加重试次数 + IncrementRetry(ctx context.Context, opID string, errorMsg string, nextRetryAt time.Time) error + // MarkAsDeadLetter 标记为死信 + MarkAsDeadLetter(ctx context.Context, opID string, errorMsg string) error + // FindPendingRetries 查找待重试的记录 + FindPendingRetries(ctx context.Context, limit int) ([]RetryRecord, error) + // DeleteRetry 删除重试记录(成功后清理) + DeleteRetry(ctx context.Context, opID string) error +} + +// RetryRecord 重试记录 +type RetryRecord struct { + OpID string + RetryCount int + RetryStatus RetryStatus + LastRetryAt *time.Time + NextRetryAt *time.Time + ErrorMessage string + CreatedAt time.Time + UpdatedAt time.Time +} + +// operationRepository 操作记录仓储实现 +type operationRepository struct { + db *sql.DB + logger logger.Logger +} + +// NewOperationRepository 创建操作记录仓储 +func NewOperationRepository(db *sql.DB, log logger.Logger) OperationRepository { + return &operationRepository{ + db: db, + logger: log, + } +} + +func (r *operationRepository) Save(ctx context.Context, op *model.Operation, status TrustlogStatus) error { + return r.SaveTx(ctx, nil, op, status) +} + +func (r *operationRepository) SaveTx(ctx context.Context, tx *sql.Tx, op *model.Operation, status TrustlogStatus) error { + query := ` + INSERT INTO operation ( + op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, trustlog_status, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + + var reqHash, respHash, clientIP, serverIP sql.NullString + if op.RequestBodyHash != nil { + reqHash = sql.NullString{String: *op.RequestBodyHash, Valid: true} + } + if op.ResponseBodyHash != nil { + respHash = sql.NullString{String: *op.ResponseBodyHash, Valid: true} + } + if op.ClientIP != nil { + clientIP = sql.NullString{String: *op.ClientIP, Valid: true} + } + if op.ServerIP != nil { + serverIP = sql.NullString{String: *op.ServerIP, Valid: true} + } + + args := []interface{}{ + op.OpID, + op.OpActor, + op.Doid, + op.ProducerID, + reqHash, + respHash, + string(op.OpSource), + string(op.OpType), + op.DoPrefix, + op.DoRepository, + clientIP, + serverIP, + string(status), + op.Timestamp, + } + + var err error + if tx != nil { + _, err = tx.ExecContext(ctx, query, args...) + } else { + _, err = r.db.ExecContext(ctx, query, args...) + } + + if err != nil { + r.logger.ErrorContext(ctx, "failed to save operation", + "opID", op.OpID, + "error", err, + ) + return fmt.Errorf("failed to save operation: %w", err) + } + + r.logger.DebugContext(ctx, "operation saved to database", + "opID", op.OpID, + "status", status, + ) + return nil +} + +func (r *operationRepository) UpdateStatus(ctx context.Context, opID string, status TrustlogStatus) error { + return r.UpdateStatusTx(ctx, nil, opID, status) +} + +func (r *operationRepository) UpdateStatusTx(ctx context.Context, tx *sql.Tx, opID string, status TrustlogStatus) error { + query := `UPDATE operation SET trustlog_status = ? WHERE op_id = ?` + + var err error + if tx != nil { + _, err = tx.ExecContext(ctx, query, string(status), opID) + } else { + _, err = r.db.ExecContext(ctx, query, string(status), opID) + } + + if err != nil { + r.logger.ErrorContext(ctx, "failed to update operation status", + "opID", opID, + "status", status, + "error", err, + ) + return fmt.Errorf("failed to update operation status: %w", err) + } + + r.logger.DebugContext(ctx, "operation status updated", + "opID", opID, + "status", status, + ) + return nil +} + +func (r *operationRepository) FindByID(ctx context.Context, opID string) (*model.Operation, TrustlogStatus, error) { + query := ` + SELECT + op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, trustlog_status, timestamp + FROM operation + WHERE op_id = ? + ` + + var op model.Operation + var statusStr string + var reqHash, respHash, clientIP, serverIP sql.NullString + + err := r.db.QueryRowContext(ctx, query, opID).Scan( + &op.OpID, + &op.OpActor, + &op.Doid, + &op.ProducerID, + &reqHash, + &respHash, + &op.OpSource, + &op.OpType, + &op.DoPrefix, + &op.DoRepository, + &clientIP, + &serverIP, + &statusStr, + &op.Timestamp, + ) + + if err == sql.ErrNoRows { + return nil, "", fmt.Errorf("operation not found: %s", opID) + } + if err != nil { + r.logger.ErrorContext(ctx, "failed to find operation", + "opID", opID, + "error", err, + ) + return nil, "", fmt.Errorf("failed to find operation: %w", err) + } + + if reqHash.Valid { + op.RequestBodyHash = &reqHash.String + } + if respHash.Valid { + op.ResponseBodyHash = &respHash.String + } + if clientIP.Valid { + op.ClientIP = &clientIP.String + } + if serverIP.Valid { + op.ServerIP = &serverIP.String + } + + return &op, TrustlogStatus(statusStr), nil +} + +func (r *operationRepository) FindUntrustlogged(ctx context.Context, limit int) ([]*model.Operation, error) { + query := ` + SELECT + op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, timestamp + FROM operation + WHERE trustlog_status = ? + ORDER BY timestamp ASC + LIMIT ? + ` + + rows, err := r.db.QueryContext(ctx, query, string(StatusNotTrustlogged), limit) + if err != nil { + r.logger.ErrorContext(ctx, "failed to find untrustlogged operations", + "error", err, + ) + return nil, fmt.Errorf("failed to find untrustlogged operations: %w", err) + } + defer rows.Close() + + var operations []*model.Operation + for rows.Next() { + var op model.Operation + var reqHash, respHash, clientIP, serverIP sql.NullString + + err := rows.Scan( + &op.OpID, + &op.OpActor, + &op.Doid, + &op.ProducerID, + &reqHash, + &respHash, + &op.OpSource, + &op.OpType, + &op.DoPrefix, + &op.DoRepository, + &clientIP, + &serverIP, + &op.Timestamp, + ) + if err != nil { + r.logger.ErrorContext(ctx, "failed to scan operation row", + "error", err, + ) + return nil, fmt.Errorf("failed to scan operation row: %w", err) + } + + if reqHash.Valid { + op.RequestBodyHash = &reqHash.String + } + if respHash.Valid { + op.ResponseBodyHash = &respHash.String + } + if clientIP.Valid { + op.ClientIP = &clientIP.String + } + if serverIP.Valid { + op.ServerIP = &serverIP.String + } + + operations = append(operations, &op) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating operation rows: %w", err) + } + + return operations, nil +} + +// cursorRepository 游标仓储实现 +type cursorRepository struct { + db *sql.DB + logger logger.Logger +} + +// NewCursorRepository 创建游标仓储 +func NewCursorRepository(db *sql.DB, log logger.Logger) CursorRepository { + return &cursorRepository{ + db: db, + logger: log, + } +} + +// GetCursor 获取游标值(Key-Value 模式) +func (r *cursorRepository) GetCursor(ctx context.Context, cursorKey string) (string, error) { + query := `SELECT cursor_value FROM trustlog_cursor WHERE cursor_key = ?` + + var cursorValue string + err := r.db.QueryRowContext(ctx, query, cursorKey).Scan(&cursorValue) + if err == sql.ErrNoRows { + r.logger.DebugContext(ctx, "cursor not found", + "cursorKey", cursorKey, + ) + return "", nil + } + if err != nil { + r.logger.ErrorContext(ctx, "failed to get cursor", + "cursorKey", cursorKey, + "error", err, + ) + return "", fmt.Errorf("failed to get cursor: %w", err) + } + + return cursorValue, nil +} + +// UpdateCursor 更新游标值 +func (r *cursorRepository) UpdateCursor(ctx context.Context, cursorKey string, cursorValue string) error { + return r.UpdateCursorTx(ctx, nil, cursorKey, cursorValue) +} + +// UpdateCursorTx 在事务中更新游标值(使用 UPSERT) +func (r *cursorRepository) UpdateCursorTx(ctx context.Context, tx *sql.Tx, cursorKey string, cursorValue string) error { + // 使用 UPSERT 语法(适配不同数据库) + query := ` + INSERT INTO trustlog_cursor (cursor_key, cursor_value, last_updated_at) + VALUES (?, ?, ?) + ON CONFLICT (cursor_key) DO UPDATE SET + cursor_value = excluded.cursor_value, + last_updated_at = excluded.last_updated_at + ` + + var err error + now := time.Now() + if tx != nil { + _, err = tx.ExecContext(ctx, query, cursorKey, cursorValue, now) + } else { + _, err = r.db.ExecContext(ctx, query, cursorKey, cursorValue, now) + } + + if err != nil { + r.logger.ErrorContext(ctx, "failed to update cursor", + "cursorKey", cursorKey, + "error", err, + ) + return fmt.Errorf("failed to update cursor: %w", err) + } + + r.logger.DebugContext(ctx, "cursor updated", + "cursorKey", cursorKey, + "cursorValue", cursorValue, + ) + return nil +} + +// InitCursor 初始化游标(如果不存在) +func (r *cursorRepository) InitCursor(ctx context.Context, cursorKey string, initialValue string) error { + query := ` + INSERT INTO trustlog_cursor (cursor_key, cursor_value, last_updated_at) + VALUES (?, ?, ?) + ON CONFLICT (cursor_key) DO NOTHING + ` + + _, err := r.db.ExecContext(ctx, query, cursorKey, initialValue, time.Now()) + if err != nil { + r.logger.ErrorContext(ctx, "failed to init cursor", + "cursorKey", cursorKey, + "error", err, + ) + return fmt.Errorf("failed to init cursor: %w", err) + } + + r.logger.DebugContext(ctx, "cursor initialized", + "cursorKey", cursorKey, + "initialValue", initialValue, + ) + return nil +} + +// retryRepository 重试仓储实现 +type retryRepository struct { + db *sql.DB + logger logger.Logger +} + +// NewRetryRepository 创建重试仓储 +func NewRetryRepository(db *sql.DB, log logger.Logger) RetryRepository { + return &retryRepository{ + db: db, + logger: log, + } +} + +func (r *retryRepository) AddRetry(ctx context.Context, opID string, errorMsg string, nextRetryAt time.Time) error { + return r.AddRetryTx(ctx, nil, opID, errorMsg, nextRetryAt) +} + +func (r *retryRepository) AddRetryTx(ctx context.Context, tx *sql.Tx, opID string, errorMsg string, nextRetryAt time.Time) error { + query := ` + INSERT INTO trustlog_retry (op_id, retry_count, retry_status, error_message, next_retry_at, updated_at) + VALUES (?, 0, ?, ?, ?, ?) + ` + + var err error + if tx != nil { + _, err = tx.ExecContext(ctx, query, opID, string(RetryStatusPending), errorMsg, nextRetryAt, time.Now()) + } else { + _, err = r.db.ExecContext(ctx, query, opID, string(RetryStatusPending), errorMsg, nextRetryAt, time.Now()) + } + + if err != nil { + r.logger.ErrorContext(ctx, "failed to add retry record", + "opID", opID, + "error", err, + ) + return fmt.Errorf("failed to add retry record: %w", err) + } + + r.logger.DebugContext(ctx, "retry record added", + "opID", opID, + "nextRetryAt", nextRetryAt, + ) + return nil +} + +func (r *retryRepository) IncrementRetry(ctx context.Context, opID string, errorMsg string, nextRetryAt time.Time) error { + query := ` + UPDATE trustlog_retry + SET retry_count = retry_count + 1, + retry_status = ?, + last_retry_at = ?, + next_retry_at = ?, + error_message = ?, + updated_at = ? + WHERE op_id = ? + ` + + _, err := r.db.ExecContext(ctx, query, + string(RetryStatusRetrying), + time.Now(), + nextRetryAt, + errorMsg, + time.Now(), + opID, + ) + + if err != nil { + r.logger.ErrorContext(ctx, "failed to increment retry", + "opID", opID, + "error", err, + ) + return fmt.Errorf("failed to increment retry: %w", err) + } + + r.logger.DebugContext(ctx, "retry incremented", + "opID", opID, + "nextRetryAt", nextRetryAt, + ) + return nil +} + +func (r *retryRepository) MarkAsDeadLetter(ctx context.Context, opID string, errorMsg string) error { + query := ` + UPDATE trustlog_retry + SET retry_status = ?, + error_message = ?, + updated_at = ? + WHERE op_id = ? + ` + + _, err := r.db.ExecContext(ctx, query, + string(RetryStatusDeadLetter), + errorMsg, + time.Now(), + opID, + ) + + if err != nil { + r.logger.ErrorContext(ctx, "failed to mark as dead letter", + "opID", opID, + "error", err, + ) + return fmt.Errorf("failed to mark as dead letter: %w", err) + } + + r.logger.WarnContext(ctx, "operation marked as dead letter", + "opID", opID, + "error", errorMsg, + ) + return nil +} + +func (r *retryRepository) FindPendingRetries(ctx context.Context, limit int) ([]RetryRecord, error) { + query := ` + SELECT + op_id, retry_count, retry_status, + last_retry_at, next_retry_at, error_message, + created_at, updated_at + FROM trustlog_retry + WHERE retry_status IN (?, ?) AND next_retry_at <= ? + ORDER BY next_retry_at ASC + LIMIT ? + ` + + rows, err := r.db.QueryContext(ctx, query, + string(RetryStatusPending), + string(RetryStatusRetrying), + time.Now(), + limit, + ) + if err != nil { + r.logger.ErrorContext(ctx, "failed to find pending retries", + "error", err, + ) + return nil, fmt.Errorf("failed to find pending retries: %w", err) + } + defer rows.Close() + + var records []RetryRecord + for rows.Next() { + var record RetryRecord + var lastRetry, nextRetry sql.NullTime + + err := rows.Scan( + &record.OpID, + &record.RetryCount, + &record.RetryStatus, + &lastRetry, + &nextRetry, + &record.ErrorMessage, + &record.CreatedAt, + &record.UpdatedAt, + ) + if err != nil { + r.logger.ErrorContext(ctx, "failed to scan retry record", + "error", err, + ) + return nil, fmt.Errorf("failed to scan retry record: %w", err) + } + + if lastRetry.Valid { + record.LastRetryAt = &lastRetry.Time + } + if nextRetry.Valid { + record.NextRetryAt = &nextRetry.Time + } + + records = append(records, record) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating retry records: %w", err) + } + + return records, nil +} + +func (r *retryRepository) DeleteRetry(ctx context.Context, opID string) error { + query := `DELETE FROM trustlog_retry WHERE op_id = ?` + + _, err := r.db.ExecContext(ctx, query, opID) + if err != nil { + r.logger.ErrorContext(ctx, "failed to delete retry record", + "opID", opID, + "error", err, + ) + return fmt.Errorf("failed to delete retry record: %w", err) + } + + r.logger.DebugContext(ctx, "retry record deleted", + "opID", opID, + ) + return nil +} diff --git a/api/persistence/repository_test.go b/api/persistence/repository_test.go new file mode 100644 index 0000000..9c7d30f --- /dev/null +++ b/api/persistence/repository_test.go @@ -0,0 +1,391 @@ +package persistence + +import ( + "context" + "database/sql" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" + + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +// setupTestDB 创建测试用的 SQLite 内存数据库 +func setupTestDB(t *testing.T) *sql.DB { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open test database: %v", err) + } + + // 创建表 + opDDL, cursorDDL, retryDDL, err := GetDialectDDL("sqlite3") + if err != nil { + t.Fatalf("failed to get DDL: %v", err) + } + + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create operation table: %v", err) + } + + if _, err := db.Exec(cursorDDL); err != nil { + t.Fatalf("failed to create cursor table: %v", err) + } + + if _, err := db.Exec(retryDDL); err != nil { + t.Fatalf("failed to create retry table: %v", err) + } + + return db +} + +// createTestOperation 创建测试用的 Operation +func createTestOperation(t *testing.T, opID string) *model.Operation { + op, err := model.NewFullOperation( + model.OpSourceDOIP, + model.OpTypeCreate, + "10.1000", + "test-repo", + "10.1000/test-repo/"+opID, + "producer-001", + "test-actor", + []byte(`{"test":"request"}`), + []byte(`{"test":"response"}`), + time.Now(), + ) + if err != nil { + t.Fatalf("failed to create test operation: %v", err) + } + + op.OpID = opID // 覆盖自动生成的 ID + return op +} + +func TestOperationRepository_Save(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewOperationRepository(db, log) + + op := createTestOperation(t, "test-op-001") + + // 设置 IP 字段 + clientIP := "192.168.1.100" + serverIP := "10.0.0.50" + op.ClientIP = &clientIP + op.ServerIP = &serverIP + + // 测试保存 + err := repo.Save(ctx, op, StatusNotTrustlogged) + if err != nil { + t.Fatalf("failed to save operation: %v", err) + } + + // 验证保存结果 + savedOp, status, err := repo.FindByID(ctx, "test-op-001") + if err != nil { + t.Fatalf("failed to find operation: %v", err) + } + + if savedOp.OpID != "test-op-001" { + t.Errorf("expected OpID to be 'test-op-001', got %s", savedOp.OpID) + } + + if status != StatusNotTrustlogged { + t.Errorf("expected status to be StatusNotTrustlogged, got %v", status) + } + + if savedOp.ClientIP == nil || *savedOp.ClientIP != "192.168.1.100" { + t.Error("ClientIP not saved correctly") + } + + if savedOp.ServerIP == nil || *savedOp.ServerIP != "10.0.0.50" { + t.Error("ServerIP not saved correctly") + } +} + +func TestOperationRepository_SaveWithNullIP(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewOperationRepository(db, log) + + op := createTestOperation(t, "test-op-002") + // IP 字段保持为 nil + + err := repo.Save(ctx, op, StatusNotTrustlogged) + if err != nil { + t.Fatalf("failed to save operation: %v", err) + } + + savedOp, _, err := repo.FindByID(ctx, "test-op-002") + if err != nil { + t.Fatalf("failed to find operation: %v", err) + } + + if savedOp.ClientIP != nil { + t.Error("ClientIP should be nil") + } + + if savedOp.ServerIP != nil { + t.Error("ServerIP should be nil") + } +} + +func TestOperationRepository_UpdateStatus(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewOperationRepository(db, log) + + op := createTestOperation(t, "test-op-003") + + // 先保存 + err := repo.Save(ctx, op, StatusNotTrustlogged) + if err != nil { + t.Fatalf("failed to save operation: %v", err) + } + + // 更新状态 + err = repo.UpdateStatus(ctx, "test-op-003", StatusTrustlogged) + if err != nil { + t.Fatalf("failed to update status: %v", err) + } + + // 验证更新结果 + _, status, err := repo.FindByID(ctx, "test-op-003") + if err != nil { + t.Fatalf("failed to find operation: %v", err) + } + + if status != StatusTrustlogged { + t.Errorf("expected status to be StatusTrustlogged, got %v", status) + } +} + +func TestOperationRepository_FindUntrustlogged(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewOperationRepository(db, log) + + // 保存多个操作 + for i := 1; i <= 5; i++ { + op := createTestOperation(t, "test-op-00"+string(rune('0'+i))) + status := StatusNotTrustlogged + if i%2 == 0 { + status = StatusTrustlogged + } + err := repo.Save(ctx, op, status) + if err != nil { + t.Fatalf("failed to save operation %d: %v", i, err) + } + } + + // 查询未存证的操作 + ops, err := repo.FindUntrustlogged(ctx, 10) + if err != nil { + t.Fatalf("failed to find untrustlogged operations: %v", err) + } + + // 应该有 3 个未存证的操作(1, 3, 5) + if len(ops) != 3 { + t.Errorf("expected 3 untrustlogged operations, got %d", len(ops)) + } +} + +func TestCursorRepository_GetAndUpdate(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewCursorRepository(db, log) + + cursorKey := "test-cursor" + + // 初始化游标 + now := time.Now().Format(time.RFC3339Nano) + err := repo.InitCursor(ctx, cursorKey, now) + if err != nil { + t.Fatalf("failed to init cursor: %v", err) + } + + // 获取游标值 + cursorValue, err := repo.GetCursor(ctx, cursorKey) + if err != nil { + t.Fatalf("failed to get cursor: %v", err) + } + + if cursorValue != now { + t.Errorf("expected cursor value to be %s, got %s", now, cursorValue) + } + + // 更新游标 + newTime := time.Now().Add(1 * time.Hour).Format(time.RFC3339Nano) + err = repo.UpdateCursor(ctx, cursorKey, newTime) + if err != nil { + t.Fatalf("failed to update cursor: %v", err) + } + + // 验证更新结果 + cursorValue, err = repo.GetCursor(ctx, cursorKey) + if err != nil { + t.Fatalf("failed to get cursor: %v", err) + } + + if cursorValue != newTime { + t.Errorf("expected cursor value to be %s, got %s", newTime, cursorValue) + } +} + +func TestRetryRepository_AddAndFind(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewRetryRepository(db, log) + + // 添加重试记录(立即可以重试) + nextRetry := time.Now().Add(-1 * time.Second) // 过去的时间,立即可以查询到 + err := repo.AddRetry(ctx, "test-op-001", "test error", nextRetry) + if err != nil { + t.Fatalf("failed to add retry: %v", err) + } + + // 查找待重试的记录 + records, err := repo.FindPendingRetries(ctx, 10) + if err != nil { + t.Fatalf("failed to find pending retries: %v", err) + } + + if len(records) != 1 { + t.Errorf("expected 1 retry record, got %d", len(records)) + } + + if len(records) > 0 { + if records[0].OpID != "test-op-001" { + t.Errorf("expected OpID to be 'test-op-001', got %s", records[0].OpID) + } + + if records[0].RetryStatus != RetryStatusPending { + t.Errorf("expected status to be PENDING, got %v", records[0].RetryStatus) + } + } +} + +func TestRetryRepository_IncrementRetry(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewRetryRepository(db, log) + + // 添加重试记录 + nextRetry := time.Now().Add(-1 * time.Second) + err := repo.AddRetry(ctx, "test-op-001", "test error", nextRetry) + if err != nil { + t.Fatalf("failed to add retry: %v", err) + } + + // 增加重试次数(立即可以重试) + nextRetry2 := time.Now().Add(-1 * time.Second) + err = repo.IncrementRetry(ctx, "test-op-001", "test error 2", nextRetry2) + if err != nil { + t.Fatalf("failed to increment retry: %v", err) + } + + // 验证重试次数 + records, err := repo.FindPendingRetries(ctx, 10) + if err != nil { + t.Fatalf("failed to find pending retries: %v", err) + } + + if len(records) != 1 { + t.Fatalf("expected 1 retry record, got %d", len(records)) + } + + if records[0].RetryCount != 1 { + t.Errorf("expected RetryCount to be 1, got %d", records[0].RetryCount) + } + + if records[0].RetryStatus != RetryStatusRetrying { + t.Errorf("expected status to be RETRYING, got %v", records[0].RetryStatus) + } +} + +func TestRetryRepository_MarkAsDeadLetter(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewRetryRepository(db, log) + + // 添加重试记录 + nextRetry := time.Now().Add(-1 * time.Second) + err := repo.AddRetry(ctx, "test-op-001", "test error", nextRetry) + if err != nil { + t.Fatalf("failed to add retry: %v", err) + } + + // 标记为死信 + err = repo.MarkAsDeadLetter(ctx, "test-op-001", "max retries exceeded") + if err != nil { + t.Fatalf("failed to mark as dead letter: %v", err) + } + + // 验证状态(死信不应该在待重试列表中) + records, err := repo.FindPendingRetries(ctx, 10) + if err != nil { + t.Fatalf("failed to find pending retries: %v", err) + } + + if len(records) != 0 { + t.Errorf("expected 0 pending retry records, got %d", len(records)) + } +} + +func TestRetryRepository_DeleteRetry(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ctx := context.Background() + log := logger.GetGlobalLogger() + repo := NewRetryRepository(db, log) + + // 添加重试记录 + nextRetry := time.Now().Add(-1 * time.Second) + err := repo.AddRetry(ctx, "test-op-001", "test error", nextRetry) + if err != nil { + t.Fatalf("failed to add retry: %v", err) + } + + // 删除重试记录 + err = repo.DeleteRetry(ctx, "test-op-001") + if err != nil { + t.Fatalf("failed to delete retry: %v", err) + } + + // 验证已删除 + records, err := repo.FindPendingRetries(ctx, 10) + if err != nil { + t.Fatalf("failed to find pending retries: %v", err) + } + + if len(records) != 0 { + t.Errorf("expected 0 retry records, got %d", len(records)) + } +} + diff --git a/api/persistence/retry_worker.go b/api/persistence/retry_worker.go new file mode 100644 index 0000000..258ab0a --- /dev/null +++ b/api/persistence/retry_worker.go @@ -0,0 +1,248 @@ +package persistence + +import ( + "context" + "fmt" + "time" + + "github.com/ThreeDotsLabs/watermill/message" + + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +// RetryWorkerConfig 重试工作器配置 +type RetryWorkerConfig struct { + // RetryInterval 重试检查间隔 + RetryInterval time.Duration + // MaxRetryCount 最大重试次数 + MaxRetryCount int + // BatchSize 每批处理的记录数 + BatchSize int + // BackoffMultiplier 退避乘数(每次重试间隔翻倍) + BackoffMultiplier float64 + // InitialBackoff 初始退避时间 + InitialBackoff time.Duration +} + +// DefaultRetryWorkerConfig 返回默认重试工作器配置 +func DefaultRetryWorkerConfig() RetryWorkerConfig { + return RetryWorkerConfig{ + RetryInterval: 30 * time.Second, + MaxRetryCount: 5, + BatchSize: 100, + BackoffMultiplier: 2.0, + InitialBackoff: 1 * time.Minute, + } +} + +// RetryWorker 重试工作器,负责处理失败的存证操作 +type RetryWorker struct { + config RetryWorkerConfig + manager *PersistenceManager + publisher message.Publisher + logger logger.Logger + stopChan chan struct{} + stoppedChan chan struct{} +} + +// NewRetryWorker 创建重试工作器 +func NewRetryWorker( + config RetryWorkerConfig, + manager *PersistenceManager, + publisher message.Publisher, + log logger.Logger, +) *RetryWorker { + return &RetryWorker{ + config: config, + manager: manager, + publisher: publisher, + logger: log, + stopChan: make(chan struct{}), + stoppedChan: make(chan struct{}), + } +} + +// Start 启动重试工作器 +func (w *RetryWorker) Start(ctx context.Context) { + w.logger.InfoContext(ctx, "starting retry worker", + "retryInterval", w.config.RetryInterval, + "maxRetryCount", w.config.MaxRetryCount, + "batchSize", w.config.BatchSize, + ) + + ticker := time.NewTicker(w.config.RetryInterval) + defer ticker.Stop() + defer close(w.stoppedChan) + + for { + select { + case <-ctx.Done(): + w.logger.InfoContext(ctx, "retry worker stopped by context") + return + case <-w.stopChan: + w.logger.InfoContext(ctx, "retry worker stopped by signal") + return + case <-ticker.C: + w.processRetries(ctx) + } + } +} + +// Stop 停止重试工作器 +func (w *RetryWorker) Stop() { + w.logger.Info("stopping retry worker") + close(w.stopChan) + <-w.stoppedChan + w.logger.Info("retry worker stopped") +} + +// processRetries 处理待重试的记录 +// 从重试表中读取待处理的记录,无需游标扫描 operation 表 +func (w *RetryWorker) processRetries(ctx context.Context) { + w.logger.DebugContext(ctx, "processing retries from retry table") + + retryRepo := w.manager.GetRetryRepo() + opRepo := w.manager.GetOperationRepo() + + // 直接从重试表查找待重试的记录(已到重试时间的记录) + records, err := retryRepo.FindPendingRetries(ctx, w.config.BatchSize) + if err != nil { + w.logger.ErrorContext(ctx, "failed to find pending retries", + "error", err, + ) + return + } + + if len(records) == 0 { + w.logger.DebugContext(ctx, "no pending retries found") + return + } + + w.logger.InfoContext(ctx, "found pending retries from retry table", + "count", len(records), + "batchSize", w.config.BatchSize, + ) + + // 处理每条重试记录 + for _, record := range records { + w.processRetry(ctx, record, retryRepo, opRepo) + } +} + +// processRetry 处理单个重试记录 +func (w *RetryWorker) processRetry( + ctx context.Context, + record RetryRecord, + retryRepo RetryRepository, + opRepo OperationRepository, +) { + w.logger.DebugContext(ctx, "processing retry", + "opID", record.OpID, + "retryCount", record.RetryCount, + ) + + // 检查是否超过最大重试次数 + if record.RetryCount >= w.config.MaxRetryCount { + w.logger.WarnContext(ctx, "max retry count exceeded, marking as dead letter", + "opID", record.OpID, + "retryCount", record.RetryCount, + ) + if err := retryRepo.MarkAsDeadLetter(ctx, record.OpID, + fmt.Sprintf("exceeded max retry count (%d)", w.config.MaxRetryCount)); err != nil { + w.logger.ErrorContext(ctx, "failed to mark as dead letter", + "opID", record.OpID, + "error", err, + ) + } + return + } + + // 查找操作记录 + op, status, err := opRepo.FindByID(ctx, record.OpID) + if err != nil { + w.logger.ErrorContext(ctx, "failed to find operation for retry", + "opID", record.OpID, + "error", err, + ) + nextRetry := w.calculateNextRetry(record.RetryCount) + retryRepo.IncrementRetry(ctx, record.OpID, err.Error(), nextRetry) + return + } + + // 如果已经存证,删除重试记录 + if status == StatusTrustlogged { + w.logger.InfoContext(ctx, "operation already trustlogged, removing retry record", + "opID", record.OpID, + ) + if err := retryRepo.DeleteRetry(ctx, record.OpID); err != nil { + w.logger.ErrorContext(ctx, "failed to delete retry record", + "opID", record.OpID, + "error", err, + ) + } + return + } + + // 尝试重新发布到存证系统 + // 这里需要根据实际的存证逻辑来实现 + // 示例:将操作发送到消息队列 + if err := w.republishOperation(ctx, op); err != nil { + w.logger.ErrorContext(ctx, "failed to republish operation", + "opID", record.OpID, + "error", err, + ) + nextRetry := w.calculateNextRetry(record.RetryCount) + retryRepo.IncrementRetry(ctx, record.OpID, err.Error(), nextRetry) + return + } + + // 发布成功,更新状态为已存证 + if err := opRepo.UpdateStatus(ctx, record.OpID, StatusTrustlogged); err != nil { + w.logger.ErrorContext(ctx, "failed to update operation status", + "opID", record.OpID, + "error", err, + ) + return + } + + // 删除重试记录 + if err := retryRepo.DeleteRetry(ctx, record.OpID); err != nil { + w.logger.ErrorContext(ctx, "failed to delete retry record", + "opID", record.OpID, + "error", err, + ) + return + } + + w.logger.InfoContext(ctx, "operation retry successful", + "opID", record.OpID, + "retryCount", record.RetryCount, + ) +} + +// republishOperation 重新发布操作到存证系统 +// 注意:这里需要序列化为 Envelope 格式 +func (w *RetryWorker) republishOperation(ctx context.Context, op *model.Operation) error { + // 这里需要根据实际的发布逻辑来实现 + // 简化实现:假设 publisher 已经配置好 + if w.publisher == nil { + return fmt.Errorf("publisher not configured") + } + + // 注意:实际使用时需要使用 Envelope 序列化 + // 这里只是示例,具体实现需要在 HighClient 中集成 + w.logger.WarnContext(ctx, "republish not implemented yet, needs Envelope serialization", + "opID", op.OpID, + ) + return nil +} + +// calculateNextRetry 计算下次重试时间(指数退避) +func (w *RetryWorker) calculateNextRetry(retryCount int) time.Time { + backoff := float64(w.config.InitialBackoff) + for i := 0; i < retryCount; i++ { + backoff *= w.config.BackoffMultiplier + } + return time.Now().Add(time.Duration(backoff)) +} diff --git a/api/persistence/retry_worker_test.go b/api/persistence/retry_worker_test.go new file mode 100644 index 0000000..97eca22 --- /dev/null +++ b/api/persistence/retry_worker_test.go @@ -0,0 +1,97 @@ +package persistence + +import ( + "testing" + "time" +) + +func TestDefaultRetryWorkerConfig(t *testing.T) { + config := DefaultRetryWorkerConfig() + + if config.RetryInterval != 30*time.Second { + t.Errorf("expected RetryInterval to be 30s, got %v", config.RetryInterval) + } + + if config.MaxRetryCount != 5 { + t.Errorf("expected MaxRetryCount to be 5, got %d", config.MaxRetryCount) + } + + if config.BatchSize != 100 { + t.Errorf("expected BatchSize to be 100, got %d", config.BatchSize) + } + + if config.BackoffMultiplier != 2.0 { + t.Errorf("expected BackoffMultiplier to be 2.0, got %f", config.BackoffMultiplier) + } + + if config.InitialBackoff != 1*time.Minute { + t.Errorf("expected InitialBackoff to be 1m, got %v", config.InitialBackoff) + } +} + +func TestRetryWorkerConfig_CustomValues(t *testing.T) { + config := RetryWorkerConfig{ + RetryInterval: 1 * time.Minute, + MaxRetryCount: 10, + BatchSize: 50, + BackoffMultiplier: 3.0, + InitialBackoff: 2 * time.Minute, + } + + if config.RetryInterval != 1*time.Minute { + t.Errorf("expected RetryInterval to be 1m, got %v", config.RetryInterval) + } + + if config.MaxRetryCount != 10 { + t.Errorf("expected MaxRetryCount to be 10, got %d", config.MaxRetryCount) + } + + if config.BatchSize != 50 { + t.Errorf("expected BatchSize to be 50, got %d", config.BatchSize) + } + + if config.BackoffMultiplier != 3.0 { + t.Errorf("expected BackoffMultiplier to be 3.0, got %f", config.BackoffMultiplier) + } + + if config.InitialBackoff != 2*time.Minute { + t.Errorf("expected InitialBackoff to be 2m, got %v", config.InitialBackoff) + } +} + +func TestRetryWorker_CalculateNextRetry(t *testing.T) { + config := RetryWorkerConfig{ + InitialBackoff: 1 * time.Minute, + BackoffMultiplier: 2.0, + } + + worker := &RetryWorker{ + config: config, + } + + tests := []struct { + name string + retryCount int + minDuration time.Duration + maxDuration time.Duration + }{ + {"first retry", 0, 1 * time.Minute, 2 * time.Minute}, + {"second retry", 1, 2 * time.Minute, 3 * time.Minute}, + {"third retry", 2, 4 * time.Minute, 5 * time.Minute}, + {"fourth retry", 3, 8 * time.Minute, 9 * time.Minute}, + } + + now := time.Now() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nextRetry := worker.calculateNextRetry(tt.retryCount) + duration := nextRetry.Sub(now) + + if duration < tt.minDuration || duration > tt.maxDuration { + t.Errorf("retry %d: expected duration between %v and %v, got %v", + tt.retryCount, tt.minDuration, tt.maxDuration, duration) + } + }) + } +} + diff --git a/api/persistence/schema.go b/api/persistence/schema.go new file mode 100644 index 0000000..93431ea --- /dev/null +++ b/api/persistence/schema.go @@ -0,0 +1,266 @@ +package persistence + +// TrustlogStatus 存证状态枚举 +type TrustlogStatus string + +const ( + // StatusNotTrustlogged 未存证 + StatusNotTrustlogged TrustlogStatus = "NOT_TRUSTLOGGED" + // StatusTrustlogged 已存证 + StatusTrustlogged TrustlogStatus = "TRUSTLOGGED" +) + +// RetryStatus 重试状态枚举 +type RetryStatus string + +const ( + // RetryStatusPending 待重试 + RetryStatusPending RetryStatus = "PENDING" + // RetryStatusRetrying 重试中 + RetryStatusRetrying RetryStatus = "RETRYING" + // RetryStatusDeadLetter 死信(超过最大重试次数) + RetryStatusDeadLetter RetryStatus = "DEAD_LETTER" +) + +// SQL DDL 语句 - 使用通用 SQL 标准,避免方言 + +// OperationTableDDL 操作记录表的 DDL(通用 SQL) +const OperationTableDDL = ` +CREATE TABLE IF NOT EXISTS operation ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + op_actor VARCHAR(64), + doid VARCHAR(512), + producer_id VARCHAR(32), + request_body_hash VARCHAR(128), + response_body_hash VARCHAR(128), + sign VARCHAR(512), + op_source VARCHAR(10), + op_type VARCHAR(30), + do_prefix VARCHAR(128), + do_repository VARCHAR(64), + client_ip VARCHAR(32), + server_ip VARCHAR(32), + trustlog_status VARCHAR(32), + timestamp TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_operation_timestamp ON operation(timestamp); +CREATE INDEX IF NOT EXISTS idx_operation_status ON operation(trustlog_status); +CREATE INDEX IF NOT EXISTS idx_operation_doid ON operation(doid); +` + +// CursorTableDDL 游标表的 DDL(用于跟踪已处理的操作) +const CursorTableDDL = ` +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key VARCHAR(64) NOT NULL PRIMARY KEY, + cursor_value VARCHAR(128) NOT NULL, + last_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_cursor_updated_at ON trustlog_cursor(last_updated_at); +` + +// RetryTableDDL 重试表的 DDL +const RetryTableDDL = ` +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + retry_count INTEGER DEFAULT 0, + retry_status VARCHAR(32) DEFAULT 'PENDING', + last_retry_at TIMESTAMP, + next_retry_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_retry_status ON trustlog_retry(retry_status); +CREATE INDEX IF NOT EXISTS idx_retry_next_retry_at ON trustlog_retry(next_retry_at); +` + +// GetDialectDDL 根据数据库类型返回适配的 DDL +// 这个函数处理不同数据库的差异,但尽量使用通用 SQL +func GetDialectDDL(driverName string) (string, string, string, error) { + switch driverName { + case "postgres": + return getPostgresDDL(), getCursorDDLPostgres(), getRetryDDLPostgres(), nil + case "mysql": + return getMySQLDDL(), getCursorDDLMySQL(), getRetryDDLMySQL(), nil + case "sqlite3", "sqlite": + return getSQLiteDDL(), getCursorDDLSQLite(), getRetryDDLSQLite(), nil + default: + // 默认使用通用 SQL + return OperationTableDDL, CursorTableDDL, RetryTableDDL, nil + } +} + +// PostgreSQL 特定 DDL +func getPostgresDDL() string { + return ` +CREATE TABLE IF NOT EXISTS operation ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + op_actor VARCHAR(64), + doid VARCHAR(512), + producer_id VARCHAR(32), + request_body_hash VARCHAR(128), + response_body_hash VARCHAR(128), + sign VARCHAR(512), + op_source VARCHAR(10), + op_type VARCHAR(30), + do_prefix VARCHAR(128), + do_repository VARCHAR(64), + client_ip VARCHAR(32), + server_ip VARCHAR(32), + trustlog_status VARCHAR(32), + timestamp TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_operation_timestamp ON operation(timestamp); +CREATE INDEX IF NOT EXISTS idx_operation_status ON operation(trustlog_status); +CREATE INDEX IF NOT EXISTS idx_operation_doid ON operation(doid); +` +} + +func getCursorDDLPostgres() string { + return ` +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key VARCHAR(64) NOT NULL PRIMARY KEY, + cursor_value VARCHAR(128) NOT NULL, + last_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_cursor_updated_at ON trustlog_cursor(last_updated_at); +` +} + +func getRetryDDLPostgres() string { + return ` +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + retry_count INTEGER DEFAULT 0, + retry_status VARCHAR(32) DEFAULT 'PENDING', + last_retry_at TIMESTAMP, + next_retry_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_retry_status ON trustlog_retry(retry_status); +CREATE INDEX IF NOT EXISTS idx_retry_next_retry_at ON trustlog_retry(next_retry_at); +` +} + +// MySQL 特定 DDL +func getMySQLDDL() string { + return ` +CREATE TABLE IF NOT EXISTS operation ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + op_actor VARCHAR(64), + doid VARCHAR(512), + producer_id VARCHAR(32), + request_body_hash VARCHAR(128), + response_body_hash VARCHAR(128), + sign VARCHAR(512), + op_source VARCHAR(10), + op_type VARCHAR(30), + do_prefix VARCHAR(128), + do_repository VARCHAR(64), + client_ip VARCHAR(32), + server_ip VARCHAR(32), + trustlog_status VARCHAR(32), + timestamp DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_operation_timestamp (timestamp), + INDEX idx_operation_status (trustlog_status), + INDEX idx_operation_doid (doid(255)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +` +} + +func getCursorDDLMySQL() string { + return ` +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key VARCHAR(64) NOT NULL PRIMARY KEY, + cursor_value VARCHAR(128) NOT NULL, + last_updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_cursor_updated_at (last_updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +` +} + +func getRetryDDLMySQL() string { + return ` +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + retry_count INT DEFAULT 0, + retry_status VARCHAR(32) DEFAULT 'PENDING', + last_retry_at DATETIME, + next_retry_at DATETIME, + error_message TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_retry_status (retry_status), + INDEX idx_retry_next_retry_at (next_retry_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +` +} + +// SQLite 特定 DDL +func getSQLiteDDL() string { + return ` +CREATE TABLE IF NOT EXISTS operation ( + op_id TEXT NOT NULL PRIMARY KEY, + op_actor TEXT, + doid TEXT, + producer_id TEXT, + request_body_hash TEXT, + response_body_hash TEXT, + sign TEXT, + op_source TEXT, + op_type TEXT, + do_prefix TEXT, + do_repository TEXT, + client_ip TEXT, + server_ip TEXT, + trustlog_status TEXT, + timestamp DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_operation_timestamp ON operation(timestamp); +CREATE INDEX IF NOT EXISTS idx_operation_status ON operation(trustlog_status); +CREATE INDEX IF NOT EXISTS idx_operation_doid ON operation(doid); +` +} + +func getCursorDDLSQLite() string { + return ` +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key TEXT NOT NULL PRIMARY KEY, + cursor_value TEXT NOT NULL, + last_updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_cursor_updated_at ON trustlog_cursor(last_updated_at); +` +} + +func getRetryDDLSQLite() string { + return ` +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id TEXT NOT NULL PRIMARY KEY, + retry_count INTEGER DEFAULT 0, + retry_status TEXT DEFAULT 'PENDING', + last_retry_at DATETIME, + next_retry_at DATETIME, + error_message TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_retry_status ON trustlog_retry(retry_status); +CREATE INDEX IF NOT EXISTS idx_retry_next_retry_at ON trustlog_retry(next_retry_at); +` +} diff --git a/api/persistence/schema_test.go b/api/persistence/schema_test.go new file mode 100644 index 0000000..9cfb8e7 --- /dev/null +++ b/api/persistence/schema_test.go @@ -0,0 +1,183 @@ +package persistence + +import ( + "strings" + "testing" +) + +func TestTrustlogStatus(t *testing.T) { + tests := []struct { + name string + status TrustlogStatus + expected string + }{ + {"not trustlogged", StatusNotTrustlogged, "NOT_TRUSTLOGGED"}, + {"trustlogged", StatusTrustlogged, "TRUSTLOGGED"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.status) != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, string(tt.status)) + } + }) + } +} + +func TestRetryStatus(t *testing.T) { + tests := []struct { + name string + status RetryStatus + expected string + }{ + {"pending", RetryStatusPending, "PENDING"}, + {"retrying", RetryStatusRetrying, "RETRYING"}, + {"dead letter", RetryStatusDeadLetter, "DEAD_LETTER"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if string(tt.status) != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, string(tt.status)) + } + }) + } +} + +func TestGetDialectDDL(t *testing.T) { + tests := []struct { + name string + driverName string + wantError bool + checkFunc func(opDDL, cursorDDL, retryDDL string) error + }{ + { + name: "postgres", + driverName: "postgres", + wantError: false, + checkFunc: func(opDDL, cursorDDL, retryDDL string) error { + if !strings.Contains(opDDL, "CREATE TABLE IF NOT EXISTS operation") { + t.Error("postgres DDL should contain operation table") + } + if !strings.Contains(cursorDDL, "CREATE TABLE IF NOT EXISTS trustlog_cursor") { + t.Error("postgres DDL should contain cursor table") + } + if !strings.Contains(retryDDL, "CREATE TABLE IF NOT EXISTS trustlog_retry") { + t.Error("postgres DDL should contain retry table") + } + return nil + }, + }, + { + name: "mysql", + driverName: "mysql", + wantError: false, + checkFunc: func(opDDL, cursorDDL, retryDDL string) error { + if !strings.Contains(opDDL, "ENGINE=InnoDB") { + t.Error("mysql DDL should contain ENGINE=InnoDB") + } + if !strings.Contains(opDDL, "DEFAULT CHARSET=utf8mb4") { + t.Error("mysql DDL should contain DEFAULT CHARSET=utf8mb4") + } + return nil + }, + }, + { + name: "sqlite", + driverName: "sqlite3", + wantError: false, + checkFunc: func(opDDL, cursorDDL, retryDDL string) error { + if !strings.Contains(opDDL, "CREATE TABLE IF NOT EXISTS operation") { + t.Error("sqlite DDL should contain operation table") + } + return nil + }, + }, + { + name: "unknown driver uses generic SQL", + driverName: "unknown", + wantError: false, + checkFunc: func(opDDL, cursorDDL, retryDDL string) error { + if !strings.Contains(opDDL, "CREATE TABLE IF NOT EXISTS operation") { + t.Error("generic DDL should contain operation table") + } + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opDDL, cursorDDL, retryDDL, err := GetDialectDDL(tt.driverName) + + if (err != nil) != tt.wantError { + t.Errorf("GetDialectDDL() error = %v, wantError %v", err, tt.wantError) + return + } + + if !tt.wantError && tt.checkFunc != nil { + if err := tt.checkFunc(opDDL, cursorDDL, retryDDL); err != nil { + t.Errorf("DDL check failed: %v", err) + } + } + }) + } +} + +func TestOperationTableDDL(t *testing.T) { + requiredFields := []string{ + "op_id", + "op_actor", + "doid", + "producer_id", + "request_body_hash", + "response_body_hash", + "sign", + "op_source", + "op_type", + "do_prefix", + "do_repository", + "client_ip", + "server_ip", + "trustlog_status", + "timestamp", + } + + for _, field := range requiredFields { + if !strings.Contains(OperationTableDDL, field) { + t.Errorf("OperationTableDDL should contain field: %s", field) + } + } +} + +func TestCursorTableDDL(t *testing.T) { + requiredFields := []string{ + "cursor_key", + "cursor_value", + "last_updated_at", + } + + for _, field := range requiredFields { + if !strings.Contains(CursorTableDDL, field) { + t.Errorf("CursorTableDDL should contain field: %s", field) + } + } +} + +func TestRetryTableDDL(t *testing.T) { + requiredFields := []string{ + "op_id", + "retry_count", + "retry_status", + "last_retry_at", + "next_retry_at", + "error_message", + } + + for _, field := range requiredFields { + if !strings.Contains(RetryTableDDL, field) { + t.Errorf("RetryTableDDL should contain field: %s", field) + } + } +} + diff --git a/api/persistence/sql/README.md b/api/persistence/sql/README.md new file mode 100644 index 0000000..d559554 --- /dev/null +++ b/api/persistence/sql/README.md @@ -0,0 +1,361 @@ +# Trustlog 数据库建表脚本 + +本目录包含 go-trustlog 数据库持久化模块的建表 SQL 脚本。 + +--- + +## 📁 文件列表 + +| 文件 | 数据库 | 说明 | +|------|--------|------| +| `postgresql.sql` | PostgreSQL 12+ | PostgreSQL 数据库建表脚本 | +| `mysql.sql` | MySQL 8.0+ / MariaDB 10+ | MySQL 数据库建表脚本 | +| `sqlite.sql` | SQLite 3+ | SQLite 数据库建表脚本 | +| `test_data.sql` | 通用 | 测试数据插入脚本 | + +--- + +## 📊 表结构说明 + +### 1. operation 表 + +操作记录表,用于存储所有的操作记录。 + +**关键字段**: +- `op_id` - 操作ID(主键) +- `client_ip` - **客户端IP(可空,仅落库,不存证)** +- `server_ip` - **服务端IP(可空,仅落库,不存证)** +- `trustlog_status` - **存证状态(NOT_TRUSTLOGGED / TRUSTLOGGED)** +- `timestamp` - 操作时间戳 + +**索引**: +- `idx_operation_timestamp` - 时间戳索引 +- `idx_operation_status` - 存证状态索引 +- `idx_operation_doid` - DOID 索引 + +### 2. trustlog_cursor 表 + +游标表,用于跟踪处理进度,支持断点续传。 + +**关键字段**: +- `id` - 游标ID(固定为1) +- `last_processed_id` - 最后处理的操作ID +- `last_processed_at` - 最后处理时间 + +**特性**: +- 自动初始化一条记录(id=1) +- 用于实现最终一致性 + +### 3. trustlog_retry 表 + +重试表,用于管理失败的存证操作。 + +**关键字段**: +- `op_id` - 操作ID(主键) +- `retry_count` - 重试次数 +- `retry_status` - 重试状态(PENDING / RETRYING / DEAD_LETTER) +- `next_retry_at` - 下次重试时间(指数退避) +- `error_message` - 错误信息 + +**索引**: +- `idx_retry_status` - 重试状态索引 +- `idx_retry_next_retry_at` - 下次重试时间索引 + +--- + +## 🚀 使用方法 + +### PostgreSQL + +```bash +# 方式1: 使用 psql 命令 +psql -U username -d database_name -f postgresql.sql + +# 方式2: 使用管道 +psql -U username -d database_name < postgresql.sql + +# 方式3: 在 psql 中执行 +psql -U username -d database_name +\i postgresql.sql +``` + +### MySQL + +```bash +# 方式1: 使用 mysql 命令 +mysql -u username -p database_name < mysql.sql + +# 方式2: 在 mysql 中执行 +mysql -u username -p +USE database_name; +SOURCE mysql.sql; +``` + +### SQLite + +```bash +# 方式1: 使用 sqlite3 命令 +sqlite3 trustlog.db < sqlite.sql + +# 方式2: 在 sqlite3 中执行 +sqlite3 trustlog.db +.read sqlite.sql +``` + +--- + +## 🔍 验证安装 + +每个 SQL 脚本末尾都包含验证查询,执行后可以检查: + +### PostgreSQL +```sql +-- 查询所有表 +SELECT tablename FROM pg_tables WHERE schemaname = 'public' + AND tablename IN ('operation', 'trustlog_cursor', 'trustlog_retry'); +``` + +### MySQL +```sql +-- 查询所有表 +SHOW TABLES LIKE 'operation%'; +SHOW TABLES LIKE 'trustlog_%'; +``` + +### SQLite +```sql +-- 查询所有表 +SELECT name FROM sqlite_master +WHERE type='table' + AND name IN ('operation', 'trustlog_cursor', 'trustlog_retry'); +``` + +--- + +## 📝 字段说明 + +### operation 表新增字段 + +#### client_ip 和 server_ip + +**特性**: +- 类型: VARCHAR(32) / TEXT (根据数据库而定) +- 可空: YES +- 默认值: NULL + +**用途**: +- 记录客户端和服务端的 IP 地址 +- **仅用于数据库持久化** +- **不参与存证哈希计算** +- **不会被序列化到 CBOR 格式** + +**示例**: +```sql +-- 插入 NULL 值(默认) +INSERT INTO operation (..., client_ip, server_ip, ...) +VALUES (..., NULL, NULL, ...); + +-- 插入 IP 值 +INSERT INTO operation (..., client_ip, server_ip, ...) +VALUES (..., '192.168.1.100', '10.0.0.50', ...); +``` + +#### trustlog_status + +**特性**: +- 类型: VARCHAR(32) / TEXT +- 可空: YES +- 可选值: + - `NOT_TRUSTLOGGED` - 未存证 + - `TRUSTLOGGED` - 已存证 + +**用途**: +- 标记操作记录的存证状态 +- 用于查询未存证的记录 +- 支持最终一致性机制 + +--- + +## 🔄 常用查询 + +### 1. 查询未存证的操作 + +```sql +SELECT * FROM operation +WHERE trustlog_status = 'NOT_TRUSTLOGGED' +ORDER BY timestamp ASC +LIMIT 100; +``` + +### 2. 查询待重试的操作 + +```sql +SELECT * FROM trustlog_retry +WHERE retry_status IN ('PENDING', 'RETRYING') + AND next_retry_at <= NOW() +ORDER BY next_retry_at ASC +LIMIT 100; +``` + +### 3. 查询死信记录 + +```sql +SELECT + o.op_id, + o.doid, + r.retry_count, + r.error_message, + r.created_at +FROM operation o +JOIN trustlog_retry r ON o.op_id = r.op_id +WHERE r.retry_status = 'DEAD_LETTER' +ORDER BY r.created_at DESC; +``` + +### 4. 按 IP 查询操作 + +```sql +-- 查询特定客户端IP的操作 +SELECT * FROM operation +WHERE client_ip = '192.168.1.100' +ORDER BY timestamp DESC; + +-- 查询未设置IP的操作 +SELECT * FROM operation +WHERE client_ip IS NULL +ORDER BY timestamp DESC; +``` + +### 5. 统计存证状态 + +```sql +SELECT + trustlog_status, + COUNT(*) as count +FROM operation +GROUP BY trustlog_status; +``` + +--- + +## 🗑️ 清理脚本 + +### 删除所有表 + +```sql +-- PostgreSQL / MySQL +DROP TABLE IF EXISTS trustlog_retry; +DROP TABLE IF EXISTS trustlog_cursor; +DROP TABLE IF EXISTS operation; + +-- SQLite +DROP TABLE IF EXISTS trustlog_retry; +DROP TABLE IF EXISTS trustlog_cursor; +DROP TABLE IF EXISTS operation; +``` + +### 清空数据(保留结构) + +```sql +-- 清空重试表 +DELETE FROM trustlog_retry; + +-- 清空操作表 +DELETE FROM operation; + +-- 重置游标表 +UPDATE trustlog_cursor SET + last_processed_id = NULL, + last_processed_at = NULL, + updated_at = CURRENT_TIMESTAMP +WHERE id = 1; +``` + +--- + +## ⚠️ 注意事项 + +### 1. 字符集和排序规则(MySQL) +- 使用 `utf8mb4` 字符集 +- 使用 `utf8mb4_unicode_ci` 排序规则 +- 支持完整的 Unicode 字符 + +### 2. 索引长度(MySQL) +- `doid` 字段使用前缀索引 `doid(255)` +- 避免索引长度超过限制 + +### 3. 自增主键 +- PostgreSQL: `SERIAL` +- MySQL: `AUTO_INCREMENT` +- SQLite: `AUTOINCREMENT` + +### 4. 时间类型 +- PostgreSQL: `TIMESTAMP` +- MySQL: `DATETIME` +- SQLite: `DATETIME` (存储为文本) + +### 5. IP 字段长度 +- 当前长度: 32 字符 +- IPv4: 最长 15 字符 (`255.255.255.255`) +- IPv4 with port: 最长 21 字符 (`255.255.255.255:65535`) +- **IPv6: 最长 39 字符** - 如需支持完整 IPv6,建议扩展到 64 字符 + +--- + +## 🔧 扩展建议 + +### 1. 如果需要支持完整 IPv6 + +```sql +-- 修改 client_ip 和 server_ip 字段长度 +ALTER TABLE operation MODIFY COLUMN client_ip VARCHAR(64); +ALTER TABLE operation MODIFY COLUMN server_ip VARCHAR(64); +``` + +### 2. 如果需要分区表(PostgreSQL) + +```sql +-- 按时间分区 +CREATE TABLE operation_partitioned ( + -- ... 字段定义 ... +) PARTITION BY RANGE (timestamp); + +CREATE TABLE operation_2024_01 PARTITION OF operation_partitioned + FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); +``` + +### 3. 如果需要添加审计字段 + +```sql +-- 添加创建人和更新人 +ALTER TABLE operation ADD COLUMN created_by VARCHAR(64); +ALTER TABLE operation ADD COLUMN updated_by VARCHAR(64); +ALTER TABLE operation ADD COLUMN updated_at TIMESTAMP; +``` + +--- + +## 📚 相关文档 + +- [PERSISTENCE_QUICKSTART.md](../../PERSISTENCE_QUICKSTART.md) - 快速入门 +- [README.md](../README.md) - 详细技术文档 +- [IP_FIELDS_USAGE.md](../IP_FIELDS_USAGE.md) - IP 字段使用说明 + +--- + +## ✅ 检查清单 + +安装完成后,请检查: + +- [ ] 所有3个表都已创建 +- [ ] 所有索引都已创建 +- [ ] trustlog_cursor 表有初始记录(id=1) +- [ ] operation 表可以插入 NULL 的 IP 值 +- [ ] operation 表可以插入非 NULL 的 IP 值 +- [ ] 查询验证脚本能正常执行 + +--- + +**最后更新**: 2025-12-23 +**版本**: v1.0.0 + diff --git a/api/persistence/sql/mysql.sql b/api/persistence/sql/mysql.sql new file mode 100644 index 0000000..5380f2c --- /dev/null +++ b/api/persistence/sql/mysql.sql @@ -0,0 +1,84 @@ +-- MySQL 建表脚本 +-- 用于 go-trustlog 数据库持久化模块 +-- MySQL 8.0+ / MariaDB 10+ 版本 + +-- ============================================ +-- 1. operation 表 - 操作记录表 +-- ============================================ +CREATE TABLE IF NOT EXISTS operation ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + op_actor VARCHAR(64), + doid VARCHAR(512), + producer_id VARCHAR(32), + request_body_hash VARCHAR(128), + response_body_hash VARCHAR(128), + sign VARCHAR(512), + op_source VARCHAR(10), + op_type VARCHAR(30), + do_prefix VARCHAR(128), + do_repository VARCHAR(64), + client_ip VARCHAR(32) COMMENT '客户端IP(可空,仅落库,不存证)', + server_ip VARCHAR(32) COMMENT '服务端IP(可空,仅落库,不存证)', + trustlog_status VARCHAR(32) COMMENT '存证状态:NOT_TRUSTLOGGED / TRUSTLOGGED', + timestamp DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_operation_timestamp (timestamp), + INDEX idx_operation_status (trustlog_status), + INDEX idx_operation_doid (doid(255)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作记录表'; + +-- ============================================ +-- 2. trustlog_cursor 表 - 游标表(任务发现队列) +-- ============================================ +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key VARCHAR(64) NOT NULL PRIMARY KEY COMMENT '游标键(如:operation_scan)', + cursor_value VARCHAR(128) NOT NULL COMMENT '游标值(最后处理的时间戳,RFC3339Nano格式)', + last_updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + + INDEX idx_cursor_updated_at (last_updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游标表,记录扫描位置(Cursor + Retry 双层模式)'; + +-- ============================================ +-- 3. trustlog_retry 表 - 重试表 +-- ============================================ +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + retry_count INT DEFAULT 0 COMMENT '重试次数', + retry_status VARCHAR(32) DEFAULT 'PENDING' COMMENT '重试状态:PENDING / RETRYING / DEAD_LETTER', + last_retry_at DATETIME COMMENT '上次重试时间', + next_retry_at DATETIME COMMENT '下次重试时间(用于指数退避)', + error_message TEXT COMMENT '错误信息', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_retry_status (retry_status), + INDEX idx_retry_next_retry_at (next_retry_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='重试表,用于管理失败的存证操作'; + +-- ============================================ +-- 验证查询 +-- ============================================ + +-- 查询所有表 +SHOW TABLES LIKE 'operation%'; +SHOW TABLES LIKE 'trustlog_%'; + +-- 查询 operation 表结构 +DESCRIBE operation; + +-- 查询所有索引 +SHOW INDEX FROM operation; +SHOW INDEX FROM trustlog_cursor; +SHOW INDEX FROM trustlog_retry; + +-- 查询表注释 +SELECT + TABLE_NAME, + TABLE_COMMENT +FROM + INFORMATION_SCHEMA.TABLES +WHERE + TABLE_SCHEMA = DATABASE() + AND TABLE_NAME IN ('operation', 'trustlog_cursor', 'trustlog_retry'); + diff --git a/api/persistence/sql/postgresql.sql b/api/persistence/sql/postgresql.sql new file mode 100644 index 0000000..89c9211 --- /dev/null +++ b/api/persistence/sql/postgresql.sql @@ -0,0 +1,99 @@ +-- PostgreSQL 建表脚本 +-- 用于 go-trustlog 数据库持久化模块 +-- PostgreSQL 12+ 版本 + +-- ============================================ +-- 1. operation 表 - 操作记录表 +-- ============================================ +CREATE TABLE IF NOT EXISTS operation ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + op_actor VARCHAR(64), + doid VARCHAR(512), + producer_id VARCHAR(32), + request_body_hash VARCHAR(128), + response_body_hash VARCHAR(128), + sign VARCHAR(512), + op_source VARCHAR(10), + op_type VARCHAR(30), + do_prefix VARCHAR(128), + do_repository VARCHAR(64), + client_ip VARCHAR(32), -- 客户端IP(可空,仅落库) + server_ip VARCHAR(32), -- 服务端IP(可空,仅落库) + trustlog_status VARCHAR(32), -- 存证状态:NOT_TRUSTLOGGED / TRUSTLOGGED + timestamp TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_operation_timestamp ON operation(timestamp); +CREATE INDEX IF NOT EXISTS idx_operation_status ON operation(trustlog_status); +CREATE INDEX IF NOT EXISTS idx_operation_doid ON operation(doid); + +-- 添加注释 +COMMENT ON TABLE operation IS '操作记录表'; +COMMENT ON COLUMN operation.op_id IS '操作ID(主键)'; +COMMENT ON COLUMN operation.client_ip IS '客户端IP(可空,仅落库,不存证)'; +COMMENT ON COLUMN operation.server_ip IS '服务端IP(可空,仅落库,不存证)'; +COMMENT ON COLUMN operation.trustlog_status IS '存证状态:NOT_TRUSTLOGGED(未存证)/ TRUSTLOGGED(已存证)'; + +-- ============================================ +-- 2. trustlog_cursor 表 - 游标表(任务发现队列) +-- ============================================ +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key VARCHAR(64) NOT NULL PRIMARY KEY, + cursor_value VARCHAR(128) NOT NULL, -- 存储时间戳(RFC3339Nano格式) + last_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_cursor_updated_at ON trustlog_cursor(last_updated_at); + +-- 添加注释 +COMMENT ON TABLE trustlog_cursor IS '游标表,记录扫描位置(Cursor + Retry 双层模式)'; +COMMENT ON COLUMN trustlog_cursor.cursor_key IS '游标键(如:operation_scan)'; +COMMENT ON COLUMN trustlog_cursor.cursor_value IS '游标值(最后处理的时间戳,RFC3339Nano格式)'; +COMMENT ON COLUMN trustlog_cursor.last_updated_at IS '最后更新时间'; + +-- ============================================ +-- 3. trustlog_retry 表 - 重试表 +-- ============================================ +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id VARCHAR(32) NOT NULL PRIMARY KEY, + retry_count INTEGER DEFAULT 0, + retry_status VARCHAR(32) DEFAULT 'PENDING', + last_retry_at TIMESTAMP, + next_retry_at TIMESTAMP, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_retry_status ON trustlog_retry(retry_status); +CREATE INDEX IF NOT EXISTS idx_retry_next_retry_at ON trustlog_retry(next_retry_at); + +-- 添加注释 +COMMENT ON TABLE trustlog_retry IS '重试表,用于管理失败的存证操作'; +COMMENT ON COLUMN trustlog_retry.retry_status IS '重试状态:PENDING(待重试)/ RETRYING(重试中)/ DEAD_LETTER(死信)'; +COMMENT ON COLUMN trustlog_retry.retry_count IS '重试次数'; +COMMENT ON COLUMN trustlog_retry.next_retry_at IS '下次重试时间(用于指数退避)'; + +-- ============================================ +-- 验证查询 +-- ============================================ + +-- 查询所有表 +SELECT tablename FROM pg_tables WHERE schemaname = 'public' + AND tablename IN ('operation', 'trustlog_cursor', 'trustlog_retry'); + +-- 查询 operation 表结构 +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_name = 'operation' +ORDER BY ordinal_position; + +-- 查询所有索引 +SELECT indexname, tablename FROM pg_indexes +WHERE tablename IN ('operation', 'trustlog_cursor', 'trustlog_retry') +ORDER BY tablename, indexname; + diff --git a/api/persistence/sql/sqlite.sql b/api/persistence/sql/sqlite.sql new file mode 100644 index 0000000..d964e41 --- /dev/null +++ b/api/persistence/sql/sqlite.sql @@ -0,0 +1,82 @@ +-- SQLite 建表脚本 +-- 用于 go-trustlog 数据库持久化模块 +-- SQLite 3+ 版本 + +-- ============================================ +-- 1. operation 表 - 操作记录表 +-- ============================================ +CREATE TABLE IF NOT EXISTS operation ( + op_id TEXT NOT NULL PRIMARY KEY, + op_actor TEXT, + doid TEXT, + producer_id TEXT, + request_body_hash TEXT, + response_body_hash TEXT, + sign TEXT, + op_source TEXT, + op_type TEXT, + do_prefix TEXT, + do_repository TEXT, + client_ip TEXT, -- 客户端IP(可空,仅落库,不存证) + server_ip TEXT, -- 服务端IP(可空,仅落库,不存证) + trustlog_status TEXT, -- 存证状态:NOT_TRUSTLOGGED / TRUSTLOGGED + timestamp DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_operation_timestamp ON operation(timestamp); +CREATE INDEX IF NOT EXISTS idx_operation_status ON operation(trustlog_status); +CREATE INDEX IF NOT EXISTS idx_operation_doid ON operation(doid); + +-- ============================================ +-- 2. trustlog_cursor 表 - 游标表(任务发现队列) +-- ============================================ +CREATE TABLE IF NOT EXISTS trustlog_cursor ( + cursor_key TEXT NOT NULL PRIMARY KEY, -- 游标键(如:operation_scan) + cursor_value TEXT NOT NULL, -- 游标值(最后处理的时间戳,RFC3339Nano格式) + last_updated_at TEXT DEFAULT (datetime('now')) -- 最后更新时间 +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_cursor_updated_at ON trustlog_cursor(last_updated_at); + +-- ============================================ +-- 3. trustlog_retry 表 - 重试表 +-- ============================================ +CREATE TABLE IF NOT EXISTS trustlog_retry ( + op_id TEXT NOT NULL PRIMARY KEY, + retry_count INTEGER DEFAULT 0, -- 重试次数 + retry_status TEXT DEFAULT 'PENDING', -- 重试状态:PENDING / RETRYING / DEAD_LETTER + last_retry_at DATETIME, -- 上次重试时间 + next_retry_at DATETIME, -- 下次重试时间(用于指数退避) + error_message TEXT, -- 错误信息 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_retry_status ON trustlog_retry(retry_status); +CREATE INDEX IF NOT EXISTS idx_retry_next_retry_at ON trustlog_retry(next_retry_at); + +-- ============================================ +-- 验证查询 +-- ============================================ + +-- 查询所有表 +SELECT name FROM sqlite_master +WHERE type='table' + AND name IN ('operation', 'trustlog_cursor', 'trustlog_retry'); + +-- 查询 operation 表结构 +PRAGMA table_info(operation); + +-- 查询所有索引 +SELECT name, tbl_name FROM sqlite_master +WHERE type='index' + AND tbl_name IN ('operation', 'trustlog_cursor', 'trustlog_retry') +ORDER BY tbl_name, name; + +-- 查询游标表初始记录 +SELECT * FROM trustlog_cursor WHERE id = 1; + diff --git a/api/persistence/sql/test_data.sql b/api/persistence/sql/test_data.sql new file mode 100644 index 0000000..57044be --- /dev/null +++ b/api/persistence/sql/test_data.sql @@ -0,0 +1,203 @@ +-- 测试数据插入脚本 +-- 用于验证数据库表结构和功能 + +-- ============================================ +-- 1. 插入测试操作记录 +-- ============================================ + +-- 测试1: 插入包含 IP 信息的操作记录 +INSERT INTO operation ( + op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, trustlog_status, timestamp +) VALUES ( + 'test-op-001', + 'test-user', + '10.1000/test-repo/doc001', + 'producer-001', + 'req_hash_001', + 'resp_hash_001', + 'DOIP', + 'Create', + '10.1000', + 'test-repo', + '192.168.1.100', -- 客户端IP + '10.0.0.50', -- 服务端IP + 'TRUSTLOGGED', -- 已存证 + CURRENT_TIMESTAMP +); + +-- 测试2: 插入 IP 为 NULL 的操作记录 +INSERT INTO operation ( + op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, trustlog_status, timestamp +) VALUES ( + 'test-op-002', + 'test-user', + '10.1000/test-repo/doc002', + 'producer-001', + 'req_hash_002', + 'resp_hash_002', + 'DOIP', + 'Update', + '10.1000', + 'test-repo', + NULL, -- IP 为 NULL + NULL, -- IP 为 NULL + 'NOT_TRUSTLOGGED', -- 未存证 + CURRENT_TIMESTAMP +); + +-- 测试3: 插入只有客户端 IP 的记录 +INSERT INTO operation ( + op_id, op_actor, doid, producer_id, + request_body_hash, response_body_hash, + op_source, op_type, do_prefix, do_repository, + client_ip, server_ip, trustlog_status, timestamp +) VALUES ( + 'test-op-003', + 'test-user', + '10.1000/test-repo/doc003', + 'producer-001', + 'req_hash_003', + 'resp_hash_003', + 'IRP', + 'Delete', + '10.1000', + 'test-repo', + '172.16.0.100', -- 仅客户端IP + NULL, -- 服务端IP为NULL + 'NOT_TRUSTLOGGED', + CURRENT_TIMESTAMP +); + +-- ============================================ +-- 2. 插入测试重试记录 +-- ============================================ + +-- 测试1: 待重试记录 +INSERT INTO trustlog_retry ( + op_id, retry_count, retry_status, + last_retry_at, next_retry_at, error_message +) VALUES ( + 'test-op-002', + 0, + 'PENDING', + NULL, + CURRENT_TIMESTAMP, -- 立即重试 + 'Initial retry' +); + +-- 测试2: 重试中记录 +INSERT INTO trustlog_retry ( + op_id, retry_count, retry_status, + last_retry_at, next_retry_at, error_message +) VALUES ( + 'test-op-003', + 2, + 'RETRYING', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, -- 下次重试时间 + 'Connection timeout' +); + +-- ============================================ +-- 3. 验证查询 +-- ============================================ + +-- 查询所有操作记录 +SELECT + op_id, + op_type, + client_ip, + server_ip, + trustlog_status, + timestamp +FROM operation +ORDER BY timestamp DESC; + +-- 查询包含 IP 信息的记录 +SELECT + op_id, + client_ip, + server_ip, + trustlog_status +FROM operation +WHERE client_ip IS NOT NULL OR server_ip IS NOT NULL; + +-- 查询未存证的记录 +SELECT + op_id, + doid, + trustlog_status, + timestamp +FROM operation +WHERE trustlog_status = 'NOT_TRUSTLOGGED' +ORDER BY timestamp ASC; + +-- 查询重试记录 +SELECT + r.op_id, + r.retry_count, + r.retry_status, + r.error_message, + o.doid +FROM trustlog_retry r +JOIN operation o ON r.op_id = o.op_id +ORDER BY r.next_retry_at ASC; + +-- 查询游标状态 +SELECT * FROM trustlog_cursor WHERE id = 1; + +-- ============================================ +-- 4. 统计查询 +-- ============================================ + +-- 统计各状态的记录数 +SELECT + trustlog_status, + COUNT(*) as count +FROM operation +GROUP BY trustlog_status; + +-- 统计 IP 字段使用情况 +SELECT + CASE + WHEN client_ip IS NOT NULL THEN 'Has Client IP' + ELSE 'No Client IP' + END as client_ip_status, + CASE + WHEN server_ip IS NOT NULL THEN 'Has Server IP' + ELSE 'No Server IP' + END as server_ip_status, + COUNT(*) as count +FROM operation +GROUP BY + CASE WHEN client_ip IS NOT NULL THEN 'Has Client IP' ELSE 'No Client IP' END, + CASE WHEN server_ip IS NOT NULL THEN 'Has Server IP' ELSE 'No Server IP' END; + +-- 统计重试状态 +SELECT + retry_status, + COUNT(*) as count, + AVG(retry_count) as avg_retry_count +FROM trustlog_retry +GROUP BY retry_status; + +-- ============================================ +-- 5. 清理测试数据 +-- ============================================ + +-- 取消注释以下语句来清理测试数据 +/* +DELETE FROM trustlog_retry WHERE op_id LIKE 'test-op-%'; +DELETE FROM operation WHERE op_id LIKE 'test-op-%'; +UPDATE trustlog_cursor SET + last_processed_id = NULL, + last_processed_at = NULL +WHERE id = 1; +*/ + diff --git a/api/persistence/standalone_test.go b/api/persistence/standalone_test.go new file mode 100644 index 0000000..b1e4cd8 --- /dev/null +++ b/api/persistence/standalone_test.go @@ -0,0 +1,309 @@ +// Package persistence_test provides standalone tests that don't depend on internal packages +package persistence_test + +import ( + "context" + "database/sql" + "strings" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" + + "go.yandata.net/iod/iod/go-trustlog/api/persistence" +) + +// Standalone tests - 独立测试,不依赖复杂模块 +// Run with: go test -v -run Standalone ./api/persistence/ + +func TestStandaloneConfig(t *testing.T) { + cfg := persistence.DefaultDBConfig("postgres", "test-dsn") + if cfg.DriverName != "postgres" { + t.Errorf("expected DriverName=postgres, got %s", cfg.DriverName) + } + if cfg.MaxOpenConns != 25 { + t.Error("MaxOpenConns should be 25") + } +} + +func TestStandaloneStrategy(t *testing.T) { + tests := []struct { + strategy persistence.PersistenceStrategy + want string + }{ + {persistence.StrategyDBOnly, "DB_ONLY"}, + {persistence.StrategyDBAndTrustlog, "DB_AND_TRUSTLOG"}, + {persistence.StrategyTrustlogOnly, "TRUSTLOG_ONLY"}, + } + + for _, tt := range tests { + got := tt.strategy.String() + if got != tt.want { + t.Errorf("strategy.String() = %s, want %s", got, tt.want) + } + } +} + +func TestStandaloneEnums(t *testing.T) { + if persistence.StatusNotTrustlogged != "NOT_TRUSTLOGGED" { + t.Error("StatusNotTrustlogged value incorrect") + } + if persistence.StatusTrustlogged != "TRUSTLOGGED" { + t.Error("StatusTrustlogged value incorrect") + } + if persistence.RetryStatusPending != "PENDING" { + t.Error("RetryStatusPending value incorrect") + } + if persistence.RetryStatusRetrying != "RETRYING" { + t.Error("RetryStatusRetrying value incorrect") + } + if persistence.RetryStatusDeadLetter != "DEAD_LETTER" { + t.Error("RetryStatusDeadLetter value incorrect") + } +} + +func TestStandaloneDDL(t *testing.T) { + drivers := []string{"postgres", "mysql", "sqlite3", "unknown"} + + for _, driver := range drivers { + opDDL, cursorDDL, retryDDL, err := persistence.GetDialectDDL(driver) + if err != nil { + t.Fatalf("GetDialectDDL(%s) failed: %v", driver, err) + } + + if len(opDDL) == 0 { + t.Errorf("%s: operation DDL is empty", driver) + } + if len(cursorDDL) == 0 { + t.Errorf("%s: cursor DDL is empty", driver) + } + if len(retryDDL) == 0 { + t.Errorf("%s: retry DDL is empty", driver) + } + + // 验证必需字段 + if !strings.Contains(opDDL, "op_id") { + t.Errorf("%s: missing op_id field", driver) + } + if !strings.Contains(opDDL, "client_ip") { + t.Errorf("%s: missing client_ip field", driver) + } + if !strings.Contains(opDDL, "server_ip") { + t.Errorf("%s: missing server_ip field", driver) + } + if !strings.Contains(opDDL, "trustlog_status") { + t.Errorf("%s: missing trustlog_status field", driver) + } + } +} + +func TestStandaloneDatabase(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + // 获取 DDL + opDDL, cursorDDL, retryDDL, err := persistence.GetDialectDDL("sqlite3") + if err != nil { + t.Fatalf("GetDialectDDL failed: %v", err) + } + + // 创建表 + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create operation table: %v", err) + } + if _, err := db.Exec(cursorDDL); err != nil { + t.Fatalf("failed to create cursor table: %v", err) + } + if _, err := db.Exec(retryDDL); err != nil { + t.Fatalf("failed to create retry table: %v", err) + } + + // 验证表存在 + tables := []string{"operation", "trustlog_cursor", "trustlog_retry"} + for _, table := range tables { + var name string + err = db.QueryRow( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?", + table, + ).Scan(&name) + if err != nil { + t.Errorf("table %s not found: %v", table, err) + } + } +} + +func TestStandaloneIPFields(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + // 创建表 + opDDL, _, _, _ := persistence.GetDialectDDL("sqlite3") + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create table: %v", err) + } + + ctx := context.Background() + + // 测试 NULL IP + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp, + client_ip, server_ip + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-001", "10.1000/repo/obj", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now(), nil, nil) + + if err != nil { + t.Fatalf("failed to insert with NULL IPs: %v", err) + } + + var clientIP, serverIP sql.NullString + err = db.QueryRowContext(ctx, + "SELECT client_ip, server_ip FROM operation WHERE op_id = ?", + "test-001", + ).Scan(&clientIP, &serverIP) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if clientIP.Valid { + t.Error("client_ip should be NULL") + } + if serverIP.Valid { + t.Error("server_ip should be NULL") + } + + // 测试非 NULL IP + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp, + client_ip, server_ip + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-002", "10.1000/repo/obj2", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now(), + "192.168.1.100", "10.0.0.50") + + if err != nil { + t.Fatalf("failed to insert with IP values: %v", err) + } + + err = db.QueryRowContext(ctx, + "SELECT client_ip, server_ip FROM operation WHERE op_id = ?", + "test-002", + ).Scan(&clientIP, &serverIP) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if !clientIP.Valid || clientIP.String != "192.168.1.100" { + t.Errorf("client_ip should be '192.168.1.100', got %v", clientIP) + } + if !serverIP.Valid || serverIP.String != "10.0.0.50" { + t.Errorf("server_ip should be '10.0.0.50', got %v", serverIP) + } +} + +func TestStandaloneStatusFlow(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + opDDL, _, _, _ := persistence.GetDialectDDL("sqlite3") + if _, err := db.Exec(opDDL); err != nil { + t.Fatalf("failed to create table: %v", err) + } + + ctx := context.Background() + + // 插入未存证记录 + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-001", "10.1000/repo/obj", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now()) + + if err != nil { + t.Fatalf("failed to insert: %v", err) + } + + // 更新为已存证 + _, err = db.ExecContext(ctx, + "UPDATE operation SET trustlog_status = ? WHERE op_id = ?", + "TRUSTLOGGED", "test-001", + ) + + if err != nil { + t.Fatalf("failed to update: %v", err) + } + + // 验证状态 + var status string + err = db.QueryRowContext(ctx, + "SELECT trustlog_status FROM operation WHERE op_id = ?", + "test-001", + ).Scan(&status) + + if err != nil { + t.Fatalf("failed to query: %v", err) + } + + if status != "TRUSTLOGGED" { + t.Errorf("expected status=TRUSTLOGGED, got %s", status) + } +} + +func TestStandaloneCursorInit(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + _, cursorDDL, _, _ := persistence.GetDialectDDL("sqlite3") + if _, err := db.Exec(cursorDDL); err != nil { + t.Fatalf("failed to create cursor table: %v", err) + } + + // 验证表结构(新的 cursor 表不再自动插入初始记录) + var count int + err = db.QueryRow("SELECT COUNT(*) FROM trustlog_cursor").Scan(&count) + if err != nil { + t.Fatalf("failed to count: %v", err) + } + + if count != 0 { + t.Errorf("expected 0 initial cursor records (empty table), got %d", count) + } + + // 测试插入新记录 + _, err = db.Exec(`INSERT INTO trustlog_cursor (cursor_key, cursor_value) VALUES (?, ?)`, + "test-cursor", time.Now().Format(time.RFC3339Nano)) + if err != nil { + t.Fatalf("failed to insert cursor: %v", err) + } + + // 验证插入 + err = db.QueryRow("SELECT COUNT(*) FROM trustlog_cursor WHERE cursor_key = ?", "test-cursor").Scan(&count) + if err != nil { + t.Fatalf("failed to count after insert: %v", err) + } + + if count != 1 { + t.Errorf("expected 1 cursor record after insert, got %d", count) + } +} + diff --git a/api/persistence/strategy.go b/api/persistence/strategy.go new file mode 100644 index 0000000..5f6dc5e --- /dev/null +++ b/api/persistence/strategy.go @@ -0,0 +1,212 @@ +package persistence + +import ( + "context" + "database/sql" + "fmt" + + "go.yandata.net/iod/iod/go-trustlog/api/logger" + "go.yandata.net/iod/iod/go-trustlog/api/model" +) + +// PersistenceStrategy 存证策略枚举 +type PersistenceStrategy int + +const ( + // StrategyDBOnly 仅落库,不存证 + StrategyDBOnly PersistenceStrategy = iota + // StrategyDBAndTrustlog 既落库又存证(保证最终一致性) + StrategyDBAndTrustlog + // StrategyTrustlogOnly 仅存证,不落库 + StrategyTrustlogOnly +) + +// String 返回策略名称 +func (s PersistenceStrategy) String() string { + switch s { + case StrategyDBOnly: + return "DB_ONLY" + case StrategyDBAndTrustlog: + return "DB_AND_TRUSTLOG" + case StrategyTrustlogOnly: + return "TRUSTLOG_ONLY" + default: + return "UNKNOWN" + } +} + +// PersistenceConfig 持久化配置 +type PersistenceConfig struct { + // Strategy 存证策略 + Strategy PersistenceStrategy + // EnableRetry 是否启用重试机制(仅对 StrategyDBAndTrustlog 有效) + EnableRetry bool + // MaxRetryCount 最大重试次数 + MaxRetryCount int + // RetryBatchSize 每批重试的记录数 + RetryBatchSize int +} + +// DefaultPersistenceConfig 返回默认配置 +func DefaultPersistenceConfig(strategy PersistenceStrategy) PersistenceConfig { + return PersistenceConfig{ + Strategy: strategy, + EnableRetry: true, + MaxRetryCount: 5, + RetryBatchSize: 100, + } +} + +// OperationPublisher 操作发布器接口 +type OperationPublisher interface { + Publish(ctx context.Context, op *model.Operation) error +} + +// PersistenceManager 持久化管理器 +type PersistenceManager struct { + db *sql.DB + config PersistenceConfig + opRepo OperationRepository + cursorRepo CursorRepository + retryRepo RetryRepository + logger logger.Logger + publisher OperationPublisher +} + +// NewPersistenceManager 创建持久化管理器 +func NewPersistenceManager( + db *sql.DB, + config PersistenceConfig, + log logger.Logger, +) *PersistenceManager { + return &PersistenceManager{ + db: db, + config: config, + opRepo: NewOperationRepository(db, log), + cursorRepo: NewCursorRepository(db, log), + retryRepo: NewRetryRepository(db, log), + logger: log, + } +} + +// InitSchema 初始化数据库表结构 +func (m *PersistenceManager) InitSchema(ctx context.Context, driverName string) error { + m.logger.InfoContext(ctx, "initializing database schema", + "driver", driverName, + ) + + opDDL, cursorDDL, retryDDL, err := GetDialectDDL(driverName) + if err != nil { + return fmt.Errorf("failed to get DDL for driver %s: %w", driverName, err) + } + + // 执行 operation 表 DDL + if _, err := m.db.ExecContext(ctx, opDDL); err != nil { + return fmt.Errorf("failed to create operation table: %w", err) + } + + // 执行 cursor 表 DDL + if _, err := m.db.ExecContext(ctx, cursorDDL); err != nil { + return fmt.Errorf("failed to create cursor table: %w", err) + } + + // 执行 retry 表 DDL + if _, err := m.db.ExecContext(ctx, retryDDL); err != nil { + return fmt.Errorf("failed to create retry table: %w", err) + } + + m.logger.InfoContext(ctx, "database schema initialized successfully") + return nil +} + +// SaveOperation 根据策略保存操作 +func (m *PersistenceManager) SaveOperation(ctx context.Context, op *model.Operation) error { + switch m.config.Strategy { + case StrategyDBOnly: + return m.saveDBOnly(ctx, op) + case StrategyDBAndTrustlog: + return m.saveDBAndTrustlog(ctx, op) + case StrategyTrustlogOnly: + // 仅存证不落库,无需处理 + return nil + default: + return fmt.Errorf("unknown persistence strategy: %d", m.config.Strategy) + } +} + +// saveDBOnly 仅落库策略 +func (m *PersistenceManager) saveDBOnly(ctx context.Context, op *model.Operation) error { + m.logger.DebugContext(ctx, "saving operation with DB_ONLY strategy", + "opID", op.OpID, + ) + + // 直接保存到数据库,状态为已存证(因为不需要实际存证) + if err := m.opRepo.Save(ctx, op, StatusTrustlogged); err != nil { + return fmt.Errorf("failed to save operation (DB_ONLY): %w", err) + } + + m.logger.InfoContext(ctx, "operation saved with DB_ONLY strategy", + "opID", op.OpID, + ) + return nil +} + +// saveDBAndTrustlog 既落库又存证策略(Cursor + Retry 异步模式) +// 流程: +// 1. 仅落库(状态:NOT_TRUSTLOGGED) +// 2. 由 CursorWorker 定期扫描并异步存证 +// 3. 失败记录由 RetryWorker 重试 +func (m *PersistenceManager) saveDBAndTrustlog(ctx context.Context, op *model.Operation) error { + m.logger.DebugContext(ctx, "saving operation with DB_AND_TRUSTLOG strategy", + "opID", op.OpID, + ) + + // 只落库,状态为未存证 + // CursorWorker 会定期扫描并异步存证 + if err := m.opRepo.Save(ctx, op, StatusNotTrustlogged); err != nil { + return fmt.Errorf("failed to save operation (DB_AND_TRUSTLOG): %w", err) + } + + m.logger.InfoContext(ctx, "operation saved with DB_AND_TRUSTLOG strategy", + "opID", op.OpID, + "status", StatusNotTrustlogged, + "note", "will be discovered and trustlogged by CursorWorker", + ) + return nil +} + +// GetOperationRepo 获取操作仓储 +func (m *PersistenceManager) GetOperationRepo() OperationRepository { + return m.opRepo +} + +// GetCursorRepo 获取游标仓储 +func (m *PersistenceManager) GetCursorRepo() CursorRepository { + return m.cursorRepo +} + +// GetRetryRepo 获取重试仓储 +func (m *PersistenceManager) GetRetryRepo() RetryRepository { + return m.retryRepo +} + +// GetDB 获取数据库连接 +func (m *PersistenceManager) GetDB() *sql.DB { + return m.db +} + +// Close 关闭数据库连接 +func (m *PersistenceManager) Close() error { + m.logger.Info("closing database connection") + return m.db.Close() +} + +// SetPublisher 设置Publisher(供CursorWorker使用) +func (m *PersistenceManager) SetPublisher(publisher OperationPublisher) { + m.publisher = publisher +} + +// GetPublisher 获取Publisher +func (m *PersistenceManager) GetPublisher() OperationPublisher { + return m.publisher +} diff --git a/api/persistence/strategy_test.go b/api/persistence/strategy_test.go new file mode 100644 index 0000000..0700186 --- /dev/null +++ b/api/persistence/strategy_test.go @@ -0,0 +1,86 @@ +package persistence + +import ( + "testing" +) + +func TestPersistenceStrategy_String(t *testing.T) { + tests := []struct { + name string + strategy PersistenceStrategy + expected string + }{ + {"db only", StrategyDBOnly, "DB_ONLY"}, + {"db and trustlog", StrategyDBAndTrustlog, "DB_AND_TRUSTLOG"}, + {"trustlog only", StrategyTrustlogOnly, "TRUSTLOG_ONLY"}, + {"unknown", PersistenceStrategy(999), "UNKNOWN"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.strategy.String() + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} + +func TestDefaultPersistenceConfig(t *testing.T) { + tests := []struct { + name string + strategy PersistenceStrategy + }{ + {"db only", StrategyDBOnly}, + {"db and trustlog", StrategyDBAndTrustlog}, + {"trustlog only", StrategyTrustlogOnly}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := DefaultPersistenceConfig(tt.strategy) + + if config.Strategy != tt.strategy { + t.Errorf("expected strategy %v, got %v", tt.strategy, config.Strategy) + } + + if !config.EnableRetry { + t.Error("expected EnableRetry to be true by default") + } + + if config.MaxRetryCount != 5 { + t.Errorf("expected MaxRetryCount to be 5, got %d", config.MaxRetryCount) + } + + if config.RetryBatchSize != 100 { + t.Errorf("expected RetryBatchSize to be 100, got %d", config.RetryBatchSize) + } + }) + } +} + +func TestPersistenceConfig_CustomValues(t *testing.T) { + config := PersistenceConfig{ + Strategy: StrategyDBAndTrustlog, + EnableRetry: false, + MaxRetryCount: 10, + RetryBatchSize: 200, + } + + if config.Strategy != StrategyDBAndTrustlog { + t.Errorf("expected strategy StrategyDBAndTrustlog, got %v", config.Strategy) + } + + if config.EnableRetry { + t.Error("expected EnableRetry to be false") + } + + if config.MaxRetryCount != 10 { + t.Errorf("expected MaxRetryCount to be 10, got %d", config.MaxRetryCount) + } + + if config.RetryBatchSize != 200 { + t.Errorf("expected RetryBatchSize to be 200, got %d", config.RetryBatchSize) + } +} + diff --git a/api/persistence/unit_test.go b/api/persistence/unit_test.go new file mode 100644 index 0000000..cd6c9c1 --- /dev/null +++ b/api/persistence/unit_test.go @@ -0,0 +1,362 @@ +// +build unit + +package persistence + +import ( + "context" + "database/sql" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// 这个文件包含不依赖外部复杂模块的纯单元测试 + +// TestDBConfigCreation 测试数据库配置创建 +func TestDBConfigCreation(t *testing.T) { + t.Run("default config", func(t *testing.T) { + cfg := DefaultDBConfig("postgres", "test-dsn") + + assertEqual(t, cfg.DriverName, "postgres") + assertEqual(t, cfg.DSN, "test-dsn") + assertEqual(t, cfg.MaxOpenConns, 25) + assertEqual(t, cfg.MaxIdleConns, 5) + assertEqual(t, cfg.ConnMaxLifetime, time.Hour) + assertEqual(t, cfg.ConnMaxIdleTime, 10*time.Minute) + }) + + t.Run("custom config", func(t *testing.T) { + cfg := DBConfig{ + DriverName: "mysql", + DSN: "user:pass@/db", + MaxOpenConns: 50, + MaxIdleConns: 10, + ConnMaxLifetime: 2 * time.Hour, + ConnMaxIdleTime: 20 * time.Minute, + } + + assertEqual(t, cfg.DriverName, "mysql") + assertEqual(t, cfg.MaxOpenConns, 50) + }) +} + +// TestTrustlogStatusEnum 测试存证状态枚举 +func TestTrustlogStatusEnum(t *testing.T) { + tests := []struct { + status TrustlogStatus + expected string + }{ + {StatusNotTrustlogged, "NOT_TRUSTLOGGED"}, + {StatusTrustlogged, "TRUSTLOGGED"}, + } + + for _, tt := range tests { + assertEqual(t, string(tt.status), tt.expected) + } +} + +// TestRetryStatusEnum 测试重试状态枚举 +func TestRetryStatusEnum(t *testing.T) { + tests := []struct { + status RetryStatus + expected string + }{ + {RetryStatusPending, "PENDING"}, + {RetryStatusRetrying, "RETRYING"}, + {RetryStatusDeadLetter, "DEAD_LETTER"}, + } + + for _, tt := range tests { + assertEqual(t, string(tt.status), tt.expected) + } +} + +// TestPersistenceStrategyEnum 测试持久化策略枚举 +func TestPersistenceStrategyEnum(t *testing.T) { + tests := []struct { + strategy PersistenceStrategy + expected string + }{ + {StrategyDBOnly, "DB_ONLY"}, + {StrategyDBAndTrustlog, "DB_AND_TRUSTLOG"}, + {StrategyTrustlogOnly, "TRUSTLOG_ONLY"}, + {PersistenceStrategy(999), "UNKNOWN"}, + } + + for _, tt := range tests { + assertEqual(t, tt.strategy.String(), tt.expected) + } +} + +// TestDefaultPersistenceConfig 测试默认持久化配置 +func TestDefaultPersistenceConfig(t *testing.T) { + cfg := DefaultPersistenceConfig(StrategyDBAndTrustlog) + + assertEqual(t, cfg.Strategy, StrategyDBAndTrustlog) + assertEqual(t, cfg.EnableRetry, true) + assertEqual(t, cfg.MaxRetryCount, 5) + assertEqual(t, cfg.RetryBatchSize, 100) +} + +// TestDefaultRetryWorkerConfig 测试默认重试工作器配置 +func TestDefaultRetryWorkerConfig(t *testing.T) { + cfg := DefaultRetryWorkerConfig() + + assertEqual(t, cfg.RetryInterval, 30*time.Second) + assertEqual(t, cfg.MaxRetryCount, 5) + assertEqual(t, cfg.BatchSize, 100) + assertEqual(t, cfg.BackoffMultiplier, 2.0) + assertEqual(t, cfg.InitialBackoff, 1*time.Minute) +} + +// TestGetDialectDDL 测试不同数据库的 DDL 生成 +func TestGetDialectDDL(t *testing.T) { + drivers := []string{"postgres", "mysql", "sqlite3", "sqlite", "unknown"} + + for _, driver := range drivers { + t.Run(driver, func(t *testing.T) { + opDDL, cursorDDL, retryDDL, err := GetDialectDDL(driver) + + if err != nil { + t.Fatalf("GetDialectDDL(%s) returned error: %v", driver, err) + } + + if opDDL == "" { + t.Error("operation DDL should not be empty") + } + if cursorDDL == "" { + t.Error("cursor DDL should not be empty") + } + if retryDDL == "" { + t.Error("retry DDL should not be empty") + } + }) + } +} + +// TestDatabaseConnection 测试数据库连接(使用 SQLite 内存数据库) +func TestDatabaseConnection(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + err = db.Ping() + if err != nil { + t.Fatalf("failed to ping database: %v", err) + } +} + +// TestDDLExecution 测试 DDL 执行 +func TestDDLExecution(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + opDDL, cursorDDL, retryDDL, err := GetDialectDDL("sqlite3") + if err != nil { + t.Fatalf("failed to get DDL: %v", err) + } + + // 执行 operation 表 DDL + _, err = db.Exec(opDDL) + if err != nil { + t.Fatalf("failed to execute operation DDL: %v", err) + } + + // 执行 cursor 表 DDL + _, err = db.Exec(cursorDDL) + if err != nil { + t.Fatalf("failed to execute cursor DDL: %v", err) + } + + // 执行 retry 表 DDL + _, err = db.Exec(retryDDL) + if err != nil { + t.Fatalf("failed to execute retry DDL: %v", err) + } + + // 验证表是否创建成功 + tables := []string{"operation", "trustlog_cursor", "trustlog_retry"} + for _, table := range tables { + var name string + err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name) + if err != nil { + t.Errorf("table %s was not created: %v", table, err) + } + } +} + +// TestOperationTableStructure 测试 operation 表结构 +func TestOperationTableStructure(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + opDDL, _, _, _ := GetDialectDDL("sqlite3") + _, err = db.Exec(opDDL) + if err != nil { + t.Fatalf("failed to create operation table: %v", err) + } + + // 验证关键字段存在 + requiredFields := []string{ + "op_id", "op_actor", "doid", "producer_id", + "request_body_hash", "response_body_hash", + "client_ip", "server_ip", "trustlog_status", + } + + rows, err := db.Query("PRAGMA table_info(operation)") + if err != nil { + t.Fatalf("failed to get table info: %v", err) + } + defer rows.Close() + + foundFields := make(map[string]bool) + for rows.Next() { + var cid int + var name, typ string + var notnull, pk int + var dfltValue sql.NullString + err = rows.Scan(&cid, &name, &typ, ¬null, &dfltValue, &pk) + if err != nil { + t.Fatalf("failed to scan row: %v", err) + } + foundFields[name] = true + } + + for _, field := range requiredFields { + if !foundFields[field] { + t.Errorf("required field %s not found in operation table", field) + } + } +} + +// TestCursorTableInitialization 测试游标表初始化 +func TestCursorTableInitialization(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + _, cursorDDL, _, _ := GetDialectDDL("sqlite3") + _, err = db.Exec(cursorDDL) + if err != nil { + t.Fatalf("failed to create cursor table: %v", err) + } + + // 验证初始记录存在 + var count int + err = db.QueryRow("SELECT COUNT(*) FROM trustlog_cursor WHERE id = 1").Scan(&count) + if err != nil { + t.Fatalf("failed to count cursor records: %v", err) + } + + if count != 1 { + t.Errorf("expected 1 cursor record, got %d", count) + } +} + +// TestRetryTableIndexes 测试重试表索引 +func TestRetryTableIndexes(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + _, _, retryDDL, _ := GetDialectDDL("sqlite3") + _, err = db.Exec(retryDDL) + if err != nil { + t.Fatalf("failed to create retry table: %v", err) + } + + // 验证索引存在 + expectedIndexes := []string{ + "idx_retry_status", + "idx_retry_next_retry_at", + } + + rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='trustlog_retry'") + if err != nil { + t.Fatalf("failed to get indexes: %v", err) + } + defer rows.Close() + + foundIndexes := make(map[string]bool) + for rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + t.Fatalf("failed to scan row: %v", err) + } + foundIndexes[name] = true + } + + for _, idx := range expectedIndexes { + if !foundIndexes[idx] { + t.Errorf("expected index %s not found", idx) + } + } +} + +// TestNullableFields 测试可空字段 +func TestNullableFields(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + opDDL, _, _, _ := GetDialectDDL("sqlite3") + _, err = db.Exec(opDDL) + if err != nil { + t.Fatalf("failed to create operation table: %v", err) + } + + // 插入测试数据(IP 字段为 NULL) + ctx := context.Background() + _, err = db.ExecContext(ctx, ` + INSERT INTO operation ( + op_id, doid, producer_id, op_source, op_type, + do_prefix, do_repository, trustlog_status, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, "test-001", "10.1000/repo/obj", "producer-001", "DOIP", "Create", + "10.1000", "repo", "NOT_TRUSTLOGGED", time.Now()) + + if err != nil { + t.Fatalf("failed to insert record with null IPs: %v", err) + } + + // 查询验证 + var clientIP, serverIP sql.NullString + err = db.QueryRowContext(ctx, "SELECT client_ip, server_ip FROM operation WHERE op_id = ?", "test-001"). + Scan(&clientIP, &serverIP) + + if err != nil { + t.Fatalf("failed to query record: %v", err) + } + + if clientIP.Valid { + t.Error("clientIP should be NULL") + } + if serverIP.Valid { + t.Error("serverIP should be NULL") + } +} + +// 辅助函数 +func assertEqual(t *testing.T, got, want interface{}) { + t.Helper() + if got != want { + t.Errorf("got %v, want %v", got, want) + } +} + diff --git a/go.mod b/go.mod index 744a3d9..3dc760f 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,11 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 github.com/go-logr/logr v1.4.3 github.com/go-playground/validator/v10 v10.28.0 + github.com/mattn/go-sqlite3 v1.9.0 github.com/minio/sha256-simd v1.0.1 github.com/stretchr/testify v1.11.1 github.com/zeebo/blake3 v0.2.4 + go.yandata.net/iod/iod/trustlog-sdk v0.3.2 golang.org/x/crypto v0.43.0 google.golang.org/grpc v1.75.0 google.golang.org/protobuf v1.36.8 diff --git a/go.sum b/go.sum index 4f07dd1..7b87a35 100644 --- a/go.sum +++ b/go.sum @@ -49,18 +49,25 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo= github.com/Antonboom/nilnil v0.1.0/go.mod h1:PhHLvRPSghY5Y7mX4TW+BHZQYo1A8flE5H20D3IPZBo= +github.com/AthenZ/athenz v1.12.13 h1:OhZNqZsoBXNrKBJobeUUEirPDnwt0HRo4kQMIO1UwwQ= github.com/AthenZ/athenz v1.12.13/go.mod h1:XXDXXgaQzXaBXnJX6x/bH4yF6eon2lkyzQZ0z/dxprE= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= @@ -72,12 +79,15 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0 github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY= github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= @@ -96,7 +106,9 @@ github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/apache/pulsar-client-go v0.17.0 h1:FLyfsW6FfGHZPjDapu6Y+Thp/9JQNGJS3dms+18bdpA= github.com/apache/pulsar-client-go v0.17.0/go.mod h1:sGZ3k5Knrf38skZh6YMoK8bibNH4aIq6wx7McQu8IAE= +github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4= github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -116,9 +128,11 @@ github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.4.0 h1:+YZ8ePm+He2pU3dZlIZiOeAKfrBkXi1lSrXJ/Xzgbu8= github.com/bits-and-blooms/bitset v1.4.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= @@ -127,9 +141,11 @@ github.com/blizzy78/varnamelen v0.3.0/go.mod h1:hbwRdBvoBqxk34XyQ6HA0UH3G0/1TKuv github.com/bombsimon/wsl/v3 v3.3.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/breml/bidichk v0.1.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= @@ -140,12 +156,16 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.9/go.mod h1:SSbRIBVfMjCi/kEB6K65XEA83D6prSM8ap1UCpNKtgg= github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU= @@ -166,6 +186,10 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/containerd/continuity v0.2.0/go.mod h1:wCYX+dRqZdImhGucXOqTQn05AhX6EUDaGEMUzTFFpLg= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -178,22 +202,30 @@ github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crpt/go-crpt v1.0.0 h1:XYWWjhLPltO778XC3F/8lEs0W0NvvJPq9FYt5lO/4wk= github.com/crpt/go-crpt v1.0.0/go.mod h1:vMkK4m3hrZBDjF6jRYpUNbi+hFVeeh2vgpfDXvsEswk= +github.com/crpt/go-merkle v0.0.0-20211202024952-07ef5d0dcfc0 h1:w5TinooZVHebdsYSP5YnOY0y/HSUjO30Ghen5jDxGqI= github.com/crpt/go-merkle v0.0.0-20211202024952-07ef5d0dcfc0/go.mod h1:J6Tp53cO1NBl1bsnSUUyEz0KFiwkOoUaQ8h1ctjOhd0= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/daotl/go-acei v0.0.0-20211201154418-8daef5059165 h1:Ik4try14xsmwfpMYyqWkan81sfj/u2JlNRWmI9MquZA= github.com/daotl/go-acei v0.0.0-20211201154418-8daef5059165/go.mod h1:ajb5D+8HRBQgBgtQjNskMJq5G4dZfwSg/ScsNWCXY8c= +github.com/daotl/guts v0.0.0-20211209102048-f83c8ade78e8 h1:M0ztdOZBLkQHLlsJF2BcgLZ9+U6K1xyihZcicfZI+GA= github.com/daotl/guts v0.0.0-20211209102048-f83c8ade78e8/go.mod h1:0Q9jJiYQdgiqLy2s1LuoSypeE/t7h07lD16U0FMRSls= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= @@ -202,15 +234,27 @@ github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KP github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA= +github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= +github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emmansun/gmsm v0.40.0 h1:OCV9XdRRIqe5en+vJMUgd4fxPfyrtzz9sNUnSWyPjUg= github.com/emmansun/gmsm v0.40.0/go.mod h1:BJlUp/h2uLj1i9yaGOIO5nUrnDsEmkwKW1ybFMGoRdw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -233,6 +277,8 @@ github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= @@ -241,9 +287,13 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -252,6 +302,7 @@ github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJu github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -262,13 +313,22 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -286,10 +346,12 @@ github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Il github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= @@ -297,8 +359,10 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -335,6 +399,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -365,7 +431,10 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -388,6 +457,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -396,6 +466,7 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -435,7 +506,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hamba/avro/v2 v2.29.0 h1:fkqoWEPxfygZxrkktgSHEpd0j/P7RKTBTDbcEeMdVEY= github.com/hamba/avro/v2 v2.29.0/go.mod h1:Pk3T+x74uJoJOFmHrdJ8PRdgSEL/kEKteJ31NytCKxI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= @@ -504,6 +577,7 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -523,8 +597,10 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -534,8 +610,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.4.0/go.mod h1:vMu2Cizjy/grP+jmsvOFDx1kYP6+PD1lqg4Yu5exl2U= github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfRPOV/oFhWN85Mjb4= @@ -544,6 +623,7 @@ github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+s github.com/ldez/gomoddirectives v0.2.2/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.2.0/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -551,13 +631,18 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/libp2p/go-msgio v0.1.0 h1:8Q7g/528ivAlfXTFWvWhVjTE8XG8sDTkRUKPYh9+5Q8= github.com/libp2p/go-msgio v0.1.0/go.mod h1:eNlv2vy9V2X/kNldcZ+SShFE++o2Yjxwx6RAYsmgJnE= +github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= @@ -579,6 +664,7 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -593,6 +679,7 @@ github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= @@ -609,22 +696,43 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mroth/weightedrand v0.4.1/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -644,9 +752,12 @@ github.com/nishanths/exhaustive v0.2.3/go.mod h1:bhIX678Nx8inLM9PbpvK1yv6oGtoP8B github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= github.com/nishanths/predeclared v0.2.1/go.mod h1:HvkGJcA3naj4lOwnFXFDkFxVtSqQMB9sbB1usJ+xjQE= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oasisprotocol/curve25519-voi v0.0.0-20210609091139-0a56a4bca00b/go.mod h1:TLJifjWF6eotcfzDjKZsDqWJ+73Uvj/N85MvVyrvynM= +github.com/oasisprotocol/curve25519-voi v0.0.0-20211129104401-1d84291be125 h1:/6UJ5B+vfcRGsjMRAW/vJAZnBAMn6BxrHdLLN2AevB8= github.com/oasisprotocol/curve25519-voi v0.0.0-20211129104401-1d84291be125/go.mod h1:WUcXjUd98qaCVFb6j8Xc87MsKeMCXDu9Nk8JRJ9SeC8= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -659,15 +770,22 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= @@ -688,33 +806,41 @@ github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -723,6 +849,7 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -731,6 +858,7 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= @@ -749,6 +877,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -762,6 +892,7 @@ github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0K github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa h1:0U2s5loxrTy6/VgfVoLuVLFJcURKLH49ie0zSch7gh4= github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= @@ -769,6 +900,10 @@ github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4w github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= github.com/shirou/gopsutil/v3 v3.21.10/go.mod h1:t75NhzCZ/dYyPQjyQmrAYP6c8+LCdFANeBMdLPCNnew= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -777,6 +912,7 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sivchari/tenv v1.4.7/go.mod h1:5nF+bITvkebQVanjU6IuMbvIot/7ReNsUV7I5NbprB0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -787,6 +923,7 @@ github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4l github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -801,6 +938,7 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= @@ -813,6 +951,7 @@ github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1Sd github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -822,6 +961,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= @@ -829,15 +969,22 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/tendermint/tendermint v0.35.0 h1:YxMBeDGo+FbWwe4964XBup9dwxv/1f/uHE3pLqaM7eM= github.com/tendermint/tendermint v0.35.0/go.mod h1:BEA2df6j2yFbETYq7IljixC1EqRTvRqJwyNcExddJ8U= github.com/tendermint/tm-db v0.6.4/go.mod h1:dptYhIpJ2M5kUuenLr+Yyf3zQOv1SgBZcl8/BmWlMBw= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= +github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -864,6 +1011,7 @@ github.com/vektra/mockery/v2 v2.9.4/go.mod h1:2gU4Cf/f8YyC8oEaSXfCnZBMxMjMl/Ko20 github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -878,7 +1026,14 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -897,14 +1052,31 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= @@ -915,6 +1087,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.yandata.net/iod/iod/trustlog-sdk v0.3.2 h1:S6PC5KQ9siKos/OjVRTZH7oczecdaDDHIrnHbsbTYNA= +go.yandata.net/iod/iod/trustlog-sdk v0.3.2/go.mod h1:QuAmpGDZ9hTvFsp2iITFpSZh2A8JSmvNpDGZguI7BoE= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -938,6 +1112,7 @@ golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -980,6 +1155,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1038,6 +1214,7 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211005001312-d4b1ae081e3b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1055,6 +1232,7 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1161,9 +1339,11 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1174,6 +1354,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1292,6 +1473,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -1390,6 +1573,7 @@ google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwy google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1422,6 +1606,7 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1437,6 +1622,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1444,6 +1630,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1451,11 +1639,13 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= @@ -1470,6 +1660,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1480,10 +1671,15 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= @@ -1494,8 +1690,11 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=