kevo/pkg/transport/replication_test.go
Jeremy Tregunna 5963538bc5
All checks were successful
Go Tests / Run Tests (1.24.2) (pull_request) Successful in 9m49s
feat: implement replication transport layer
This commit implements the replication transport layer as part of Phase 2 of the replication plan.
Key components include:

- Add protocol buffer definitions for replication services
- Implement WALReplicator extension for processor management
- Create replication service server implementation
- Add replication client and server transport implementations
- Implement storage snapshot interface for bootstrap operations
- Standardize package naming across replication components
2025-04-26 13:05:54 -06:00

401 lines
9.5 KiB
Go

package transport
import (
"context"
"testing"
"time"
"github.com/KevoDB/kevo/pkg/wal"
)
// MockReplicationClient implements ReplicationClient for testing
type MockReplicationClient struct {
connected bool
registeredAsReplica bool
heartbeatSent bool
walEntriesRequested bool
bootstrapRequested bool
replicaID string
walEntries []*wal.Entry
bootstrapIterator BootstrapIterator
status TransportStatus
}
func NewMockReplicationClient() *MockReplicationClient {
return &MockReplicationClient{
connected: false,
registeredAsReplica: false,
heartbeatSent: false,
walEntriesRequested: false,
bootstrapRequested: false,
status: TransportStatus{
Connected: false,
LastConnected: time.Time{},
LastError: nil,
BytesSent: 0,
BytesReceived: 0,
RTT: 0,
},
}
}
func (c *MockReplicationClient) Connect(ctx context.Context) error {
c.connected = true
c.status.Connected = true
c.status.LastConnected = time.Now()
return nil
}
func (c *MockReplicationClient) Close() error {
c.connected = false
c.status.Connected = false
return nil
}
func (c *MockReplicationClient) IsConnected() bool {
return c.connected
}
func (c *MockReplicationClient) Status() TransportStatus {
return c.status
}
func (c *MockReplicationClient) Send(ctx context.Context, request Request) (Response, error) {
return nil, ErrInvalidRequest
}
func (c *MockReplicationClient) Stream(ctx context.Context) (Stream, error) {
return nil, ErrInvalidRequest
}
func (c *MockReplicationClient) RegisterAsReplica(ctx context.Context, replicaID string) error {
c.registeredAsReplica = true
c.replicaID = replicaID
return nil
}
func (c *MockReplicationClient) SendHeartbeat(ctx context.Context, status *ReplicaInfo) error {
c.heartbeatSent = true
return nil
}
func (c *MockReplicationClient) RequestWALEntries(ctx context.Context, fromLSN uint64) ([]*wal.Entry, error) {
c.walEntriesRequested = true
return c.walEntries, nil
}
func (c *MockReplicationClient) RequestBootstrap(ctx context.Context) (BootstrapIterator, error) {
c.bootstrapRequested = true
return c.bootstrapIterator, nil
}
// MockBootstrapIterator implements BootstrapIterator for testing
type MockBootstrapIterator struct {
pairs []struct{ key, value []byte }
position int
progress float64
closed bool
}
func NewMockBootstrapIterator() *MockBootstrapIterator {
return &MockBootstrapIterator{
pairs: []struct{ key, value []byte }{
{[]byte("key1"), []byte("value1")},
{[]byte("key2"), []byte("value2")},
{[]byte("key3"), []byte("value3")},
},
position: 0,
progress: 0.0,
closed: false,
}
}
func (it *MockBootstrapIterator) Next() ([]byte, []byte, error) {
if it.position >= len(it.pairs) {
return nil, nil, nil
}
pair := it.pairs[it.position]
it.position++
it.progress = float64(it.position) / float64(len(it.pairs))
return pair.key, pair.value, nil
}
func (it *MockBootstrapIterator) Close() error {
it.closed = true
return nil
}
func (it *MockBootstrapIterator) Progress() float64 {
return it.progress
}
// Tests
func TestReplicationClientInterface(t *testing.T) {
// Create a mock client
client := NewMockReplicationClient()
// Test Connect
ctx := context.Background()
err := client.Connect(ctx)
if err != nil {
t.Errorf("Connect failed: %v", err)
}
// Test IsConnected
if !client.IsConnected() {
t.Errorf("Expected client to be connected")
}
// Test Status
status := client.Status()
if !status.Connected {
t.Errorf("Expected status.Connected to be true")
}
// Test RegisterAsReplica
err = client.RegisterAsReplica(ctx, "replica1")
if err != nil {
t.Errorf("RegisterAsReplica failed: %v", err)
}
if !client.registeredAsReplica {
t.Errorf("Expected client to be registered as replica")
}
if client.replicaID != "replica1" {
t.Errorf("Expected replicaID to be 'replica1', got '%s'", client.replicaID)
}
// Test SendHeartbeat
replicaInfo := &ReplicaInfo{
ID: "replica1",
Address: "localhost:50051",
Role: RoleReplica,
Status: StatusReady,
LastSeen: time.Now(),
CurrentLSN: 100,
ReplicationLag: 0,
}
err = client.SendHeartbeat(ctx, replicaInfo)
if err != nil {
t.Errorf("SendHeartbeat failed: %v", err)
}
if !client.heartbeatSent {
t.Errorf("Expected heartbeat to be sent")
}
// Test RequestWALEntries
client.walEntries = []*wal.Entry{
{SequenceNumber: 101, Type: 1, Key: []byte("key1"), Value: []byte("value1")},
{SequenceNumber: 102, Type: 1, Key: []byte("key2"), Value: []byte("value2")},
}
entries, err := client.RequestWALEntries(ctx, 100)
if err != nil {
t.Errorf("RequestWALEntries failed: %v", err)
}
if !client.walEntriesRequested {
t.Errorf("Expected WAL entries to be requested")
}
if len(entries) != 2 {
t.Errorf("Expected 2 entries, got %d", len(entries))
}
// Test RequestBootstrap
client.bootstrapIterator = NewMockBootstrapIterator()
iterator, err := client.RequestBootstrap(ctx)
if err != nil {
t.Errorf("RequestBootstrap failed: %v", err)
}
if !client.bootstrapRequested {
t.Errorf("Expected bootstrap to be requested")
}
// Test iterator
key, value, err := iterator.Next()
if err != nil {
t.Errorf("Iterator.Next failed: %v", err)
}
if string(key) != "key1" || string(value) != "value1" {
t.Errorf("Expected key1/value1, got %s/%s", string(key), string(value))
}
progress := iterator.Progress()
if progress != 1.0/3.0 {
t.Errorf("Expected progress to be 1/3, got %f", progress)
}
// Test Close
err = client.Close()
if err != nil {
t.Errorf("Close failed: %v", err)
}
if client.IsConnected() {
t.Errorf("Expected client to be disconnected")
}
// Test iterator Close
err = iterator.Close()
if err != nil {
t.Errorf("Iterator.Close failed: %v", err)
}
mockIter := iterator.(*MockBootstrapIterator)
if !mockIter.closed {
t.Errorf("Expected iterator to be closed")
}
}
// MockReplicationServer implements ReplicationServer for testing
type MockReplicationServer struct {
started bool
stopped bool
replicas map[string]*ReplicaInfo
streamingReplicas map[string]bool
}
func NewMockReplicationServer() *MockReplicationServer {
return &MockReplicationServer{
started: false,
stopped: false,
replicas: make(map[string]*ReplicaInfo),
streamingReplicas: make(map[string]bool),
}
}
func (s *MockReplicationServer) Start() error {
s.started = true
return nil
}
func (s *MockReplicationServer) Serve() error {
s.started = true
return nil
}
func (s *MockReplicationServer) Stop(ctx context.Context) error {
s.stopped = true
return nil
}
func (s *MockReplicationServer) SetRequestHandler(handler RequestHandler) {
// No-op for testing
}
func (s *MockReplicationServer) RegisterReplica(replicaInfo *ReplicaInfo) error {
s.replicas[replicaInfo.ID] = replicaInfo
return nil
}
func (s *MockReplicationServer) UpdateReplicaStatus(replicaID string, status ReplicaStatus, lsn uint64) error {
replica, exists := s.replicas[replicaID]
if !exists {
return ErrInvalidRequest
}
replica.Status = status
replica.CurrentLSN = lsn
return nil
}
func (s *MockReplicationServer) GetReplicaInfo(replicaID string) (*ReplicaInfo, error) {
replica, exists := s.replicas[replicaID]
if !exists {
return nil, ErrInvalidRequest
}
return replica, nil
}
func (s *MockReplicationServer) ListReplicas() ([]*ReplicaInfo, error) {
result := make([]*ReplicaInfo, 0, len(s.replicas))
for _, replica := range s.replicas {
result = append(result, replica)
}
return result, nil
}
func (s *MockReplicationServer) StreamWALEntriesToReplica(ctx context.Context, replicaID string, fromLSN uint64) error {
_, exists := s.replicas[replicaID]
if !exists {
return ErrInvalidRequest
}
s.streamingReplicas[replicaID] = true
return nil
}
func TestReplicationServerInterface(t *testing.T) {
// Create a mock server
server := NewMockReplicationServer()
// Test Start
err := server.Start()
if err != nil {
t.Errorf("Start failed: %v", err)
}
if !server.started {
t.Errorf("Expected server to be started")
}
// Test RegisterReplica
replica1 := &ReplicaInfo{
ID: "replica1",
Address: "localhost:50051",
Role: RoleReplica,
Status: StatusConnecting,
LastSeen: time.Now(),
CurrentLSN: 0,
ReplicationLag: 0,
}
err = server.RegisterReplica(replica1)
if err != nil {
t.Errorf("RegisterReplica failed: %v", err)
}
// Test UpdateReplicaStatus
err = server.UpdateReplicaStatus("replica1", StatusReady, 100)
if err != nil {
t.Errorf("UpdateReplicaStatus failed: %v", err)
}
// Test GetReplicaInfo
replica, err := server.GetReplicaInfo("replica1")
if err != nil {
t.Errorf("GetReplicaInfo failed: %v", err)
}
if replica.Status != StatusReady {
t.Errorf("Expected status to be StatusReady, got %v", replica.Status)
}
if replica.CurrentLSN != 100 {
t.Errorf("Expected LSN to be 100, got %d", replica.CurrentLSN)
}
// Test ListReplicas
replicas, err := server.ListReplicas()
if err != nil {
t.Errorf("ListReplicas failed: %v", err)
}
if len(replicas) != 1 {
t.Errorf("Expected 1 replica, got %d", len(replicas))
}
// Test StreamWALEntriesToReplica
ctx := context.Background()
err = server.StreamWALEntriesToReplica(ctx, "replica1", 0)
if err != nil {
t.Errorf("StreamWALEntriesToReplica failed: %v", err)
}
if !server.streamingReplicas["replica1"] {
t.Errorf("Expected replica1 to be streaming")
}
// Test Stop
err = server.Stop(ctx)
if err != nil {
t.Errorf("Stop failed: %v", err)
}
if !server.stopped {
t.Errorf("Expected server to be stopped")
}
}