Files
ragflow/internal/harness/graph/checkpoint/checkpoint_edge_serde_test.go
Yingfeng 5b0b86c276 More resilient graph engine (#16325)
### What problem does this PR solve?

- OpenTelemetry integration
- Checkpoint conformance tests
- State inspector API
- Callbacks
- A series of fault injection tests
- Pregel integration tests

### Type of change

- [x] Refactoring
2026-06-24 23:05:07 +08:00

345 lines
9.4 KiB
Go

// Package checkpoint provides edge case tests for serialization,
// concurrent access patterns, and boundary conditions.
package checkpoint
import (
"context"
"fmt"
"sync"
"testing"
"time"
"ragflow/internal/harness/graph/constants"
)
// ============================================================
// P0: Serialization — various data types
// ============================================================
// TestCheckpointSerde_VariousTypes verifies round-trip of all basic types.
func TestCheckpointSerde_VariousTypes(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "serde-types"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
data := map[string]interface{}{
"int": 42,
"float": 3.14,
"string": "hello",
"bool_true": true,
"bool_false": false,
"nil_val": nil,
"int_slice": []interface{}{1, 2, 3},
"str_slice": []interface{}{"a", "b", "c"},
"nested_map": map[string]interface{}{
"inner_int": 99,
"inner_string": "deep",
},
}
if err := ms.Put(ctx, cfg, data); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := ms.Get(ctx, cfg)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got == nil {
t.Fatal("nil checkpoint")
}
if got["int"].(float64) != 42 {
t.Fatalf("expected int=42, got %v", got["int"])
}
if got["string"] != "hello" {
t.Fatalf("expected string=hello, got %v", got["string"])
}
}
// ============================================================
// P0: Serialization — empty map
// ============================================================
// TestCheckpointSerde_EmptyMap verifies empty map round-trip.
func TestCheckpointSerde_EmptyMap(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "serde-empty"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
if err := ms.Put(ctx, cfg, map[string]interface{}{}); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := ms.Get(ctx, cfg)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got == nil {
t.Fatal("nil checkpoint after empty map Put")
}
}
// ============================================================
// P0: Serialization — large map
// ============================================================
// TestCheckpointSerde_LargeMap verifies large map serialization.
func TestCheckpointSerde_LargeMap(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "serde-large"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
data := make(map[string]interface{})
for i := 0; i < 10000; i++ {
data[fmt.Sprintf("key_%d", i)] = fmt.Sprintf("value_%d", i)
}
if err := ms.Put(ctx, cfg, data); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := ms.Get(ctx, cfg)
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(got) != 10000 {
t.Fatalf("expected 10000 keys, got %d", len(got))
}
}
// ============================================================
// P1: Serialization — deeply nested arrays
// ============================================================
// TestCheckpointSerde_NestedArrays verifies deeply nested arrays.
func TestCheckpointSerde_NestedArrays(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "serde-nest-arr"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
nested := []interface{}{"a"}
current := &nested
for i := 0; i < 10; i++ {
inner := []interface{}{"level", i}
*current = append(*current, inner)
current = &inner
}
data := map[string]interface{}{"nested": nested}
if err := ms.Put(ctx, cfg, data); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := ms.Get(ctx, cfg)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got == nil {
t.Fatal("nil checkpoint")
}
}
// ============================================================
// P1: Concurrent Put on different threads (no conflict)
// ============================================================
// TestCheckpointConcurrent_DifferentThreads runs Put on 100 threads.
func TestCheckpointConcurrent_DifferentThreads(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
tid := fmt.Sprintf("conc-diff-%d", idx)
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
if err := ms.Put(ctx, cfg, map[string]interface{}{"idx": idx}); err != nil {
t.Errorf("Put %d: %v", idx, err)
}
}(i)
}
wg.Wait()
}
// ============================================================
// P1: List with zero limit
// ============================================================
// TestCheckpointSerde_ListZeroLimit verifies List returns all when limit=0.
func TestCheckpointSerde_ListZeroLimit(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "list-zero"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
for i := 0; i < 5; i++ {
ms.Put(ctx, cfg, map[string]interface{}{"i": i})
}
entries, err := ms.List(ctx, cfg, 0)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 5 {
t.Fatalf("expected 5 entries with limit=0, got %d", len(entries))
}
}
// ============================================================
// P1: Get after many Puts (latest is correct)
// ============================================================
// TestCheckpointSerde_LatestAfterManyPuts verifies Get returns latest.
func TestCheckpointSerde_LatestAfterManyPuts(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "latest-after-many"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
for i := 0; i < 50; i++ {
if err := ms.Put(ctx, cfg, map[string]interface{}{"version": i}); err != nil {
t.Fatalf("Put #%d: %v", i, err)
}
}
got, err := ms.Get(ctx, cfg)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got == nil {
t.Fatal("nil checkpoint")
}
v, ok := got["version"]
if !ok {
t.Fatal("missing version")
}
if v.(float64) != 49 {
t.Fatalf("expected version=49, got %v", v)
}
}
// ============================================================
// P2: Timestamp ordering in List
// ============================================================
// TestCheckpointSerde_TimestampOrdering verifies List ordering.
func TestCheckpointSerde_TimestampOrdering(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "ts-ordering"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
for i := 0; i < 5; i++ {
ms.Put(ctx, cfg, map[string]interface{}{"i": i})
time.Sleep(time.Millisecond)
}
entries, err := ms.List(ctx, cfg, 5)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 5 {
t.Fatalf("expected 5 entries, got %d", len(entries))
}
// Check reverse chronological order.
for i := 1; i < len(entries); i++ {
t1 := entries[i-1]["created_at"].(time.Time)
t2 := entries[i]["created_at"].(time.Time)
if t1.Before(t2) {
t.Fatalf("entry %d created at %v is before entry %d at %v (not reverse order)", i-1, t1, i, t2)
}
}
}
// ============================================================
// P2: Parent ID chain consistency
// ============================================================
// TestCheckpointSerde_ParentIDChain verifies parent_id links form a chain.
func TestCheckpointSerde_ParentIDChain(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "parent-chain"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
for i := 0; i < 10; i++ {
if i > 0 {
prevEntries, _ := ms.List(ctx, cfg, 1)
if len(prevEntries) > 0 {
if pid, ok := prevEntries[0][constants.ConfigKeyCheckpointID].(string); ok {
cfg["parent_checkpoint_id"] = pid
}
}
}
if err := ms.Put(ctx, cfg, map[string]interface{}{"i": i}); err != nil {
t.Fatalf("Put #%d: %v", i, err)
}
}
entries, err := ms.List(ctx, cfg, 10)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(entries) != 10 {
t.Fatalf("expected 10 entries, got %d", len(entries))
}
}
// ============================================================
// P2: Rapid Put after Get on same thread
// ============================================================
// TestCheckpointSerde_RapidPutGet does 1000 Put/Get cycles on same thread.
func TestCheckpointSerde_RapidPutGet(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
tid := "rapid-pg"
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
for i := 0; i < 1000; i++ {
if err := ms.Put(ctx, cfg, map[string]interface{}{"i": i}); err != nil {
t.Fatalf("Put %d: %v", i, err)
}
got, err := ms.Get(ctx, cfg)
if err != nil || got == nil {
t.Fatalf("Get %d: %v", i, err)
}
}
}
// ============================================================
// P2: Concurrent Put/Get on different threads, same checkpointer
// ============================================================
// TestCheckpointConcurrent_RapidCycle runs rapid Put/Get cycles
// on multiple threads.
func TestCheckpointConcurrent_RapidCycle(t *testing.T) {
ms := NewMemorySaver()
ctx := context.Background()
var wg sync.WaitGroup
for g := 0; g < 20; g++ {
wg.Add(1)
go func(gid int) {
defer wg.Done()
tid := fmt.Sprintf("rapid-cycle-%d", gid)
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
for i := 0; i < 100; i++ {
if err := ms.Put(ctx, cfg, map[string]interface{}{"i": i}); err != nil {
t.Errorf("Put: %v", err)
return
}
_, err := ms.Get(ctx, cfg)
if err != nil {
t.Errorf("Get: %v", err)
return
}
}
}(g)
}
wg.Wait()
}