You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2025/08/21 11:16:11 UTC

(skywalking-banyandb) 03/06: Update introducer test cases

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch sidx/snapshot
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit 2ef0390599f19cfbcc8531f117cca5f8a8c7269d
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Thu Aug 21 15:30:14 2025 +0700

    Update introducer test cases
    
    Signed-off-by: Gao Hongtao <ha...@gmail.com>
---
 banyand/internal/sidx/TODO.md            |  18 +-
 banyand/internal/sidx/introducer_test.go | 410 +++++++++++++++++++++++++++++++
 2 files changed, 419 insertions(+), 9 deletions(-)

diff --git a/banyand/internal/sidx/TODO.md b/banyand/internal/sidx/TODO.md
index 506f6dc8..22f082bf 100644
--- a/banyand/internal/sidx/TODO.md
+++ b/banyand/internal/sidx/TODO.md
@@ -279,15 +279,15 @@ This document tracks the implementation progress of the Secondary Index File Sys
   - [x] Graceful shutdown handling
   - [x] No deadlocks in channel communication
 
-### 5.3 Introduction Types (`introducer.go`)
-- [ ] memIntroduction, flusherIntroduction, mergerIntroduction
-- [ ] Object pooling for introduction structures
-- [ ] Channel synchronization with applied notifications
-- [ ] **Test Cases**:
-  - [ ] Introduction pooling reduces allocations
-  - [ ] Channel synchronization correctness
-  - [ ] Applied notifications work reliably
-  - [ ] Introduction reset for reuse
+### 5.3 Introduction Types (`introducer.go`) ✅
+- [x] memIntroduction, flusherIntroduction, mergerIntroduction
+- [x] Object pooling for introduction structures
+- [x] Channel synchronization with applied notifications
+- [x] **Test Cases**:
+  - [x] Introduction pooling reduces allocations
+  - [x] Channel synchronization correctness
+  - [x] Applied notifications work reliably
+  - [x] Introduction reset for reuse
 
 ### 5.4 Snapshot Replacement (`snapshot.go`)
 - [ ] Atomic updates with reference counting
diff --git a/banyand/internal/sidx/introducer_test.go b/banyand/internal/sidx/introducer_test.go
new file mode 100644
index 00000000..df4fb04c
--- /dev/null
+++ b/banyand/internal/sidx/introducer_test.go
@@ -0,0 +1,410 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package sidx
+
+import (
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIntroductionPooling(t *testing.T) {
+	t.Run("introduction pooling reduces allocations", func(t *testing.T) {
+		// Test that the pool works by verifying reuse
+		intro1 := generateIntroduction()
+		releaseIntroduction(intro1)
+
+		intro2 := generateIntroduction()
+		// With pooling, we might get the same instance back
+		require.NotNil(t, intro2, "pool should provide introduction instance")
+		releaseIntroduction(intro2)
+
+		// Test multiple allocations work correctly
+		var intros []*introduction
+		for i := 0; i < 10; i++ {
+			intro := generateIntroduction()
+			intro.memPart = &memPart{}
+			intro.applied = make(chan struct{})
+			intros = append(intros, intro)
+		}
+
+		for _, intro := range intros {
+			releaseIntroduction(intro)
+		}
+	})
+
+	t.Run("flusher introduction pooling reduces allocations", func(t *testing.T) {
+		intro1 := generateFlusherIntroduction()
+		releaseFlusherIntroduction(intro1)
+
+		intro2 := generateFlusherIntroduction()
+		require.NotNil(t, intro2, "pool should provide flusher introduction instance")
+		require.NotNil(t, intro2.flushed, "flushed map should be initialized")
+		releaseFlusherIntroduction(intro2)
+
+		// Test multiple allocations
+		var intros []*flusherIntroduction
+		for i := 0; i < 10; i++ {
+			intro := generateFlusherIntroduction()
+			intro.flushed[uint64(i)] = &part{}
+			intro.applied = make(chan struct{})
+			intros = append(intros, intro)
+		}
+
+		for _, intro := range intros {
+			releaseFlusherIntroduction(intro)
+		}
+	})
+
+	t.Run("merger introduction pooling reduces allocations", func(t *testing.T) {
+		intro1 := generateMergerIntroduction()
+		releaseMergerIntroduction(intro1)
+
+		intro2 := generateMergerIntroduction()
+		require.NotNil(t, intro2, "pool should provide merger introduction instance")
+		require.NotNil(t, intro2.merged, "merged map should be initialized")
+		releaseMergerIntroduction(intro2)
+
+		// Test multiple allocations
+		var intros []*mergerIntroduction
+		for i := 0; i < 10; i++ {
+			intro := generateMergerIntroduction()
+			intro.merged[uint64(i)] = struct{}{}
+			intro.newPart = &part{}
+			intro.applied = make(chan struct{})
+			intros = append(intros, intro)
+		}
+
+		for _, intro := range intros {
+			releaseMergerIntroduction(intro)
+		}
+	})
+}
+
+func TestIntroductionReset(t *testing.T) {
+	t.Run("introduction reset for reuse", func(t *testing.T) {
+		intro := generateIntroduction()
+
+		// Set up introduction with data
+		intro.memPart = &memPart{}
+		intro.applied = make(chan struct{})
+
+		// Reset the introduction
+		intro.reset()
+
+		// Verify all fields are cleared
+		assert.Nil(t, intro.memPart, "memPart should be nil")
+		assert.Nil(t, intro.applied, "applied channel should be nil")
+
+		releaseIntroduction(intro)
+	})
+
+	t.Run("flusher introduction reset for reuse", func(t *testing.T) {
+		intro := generateFlusherIntroduction()
+
+		// Set up flusher introduction with data
+		intro.flushed[1] = &part{}
+		intro.flushed[2] = &part{}
+		intro.applied = make(chan struct{})
+
+		// Reset the flusher introduction
+		intro.reset()
+
+		// Verify all fields are cleared
+		assert.Len(t, intro.flushed, 0, "flushed map should be empty")
+		assert.Nil(t, intro.applied, "applied channel should be nil")
+
+		releaseFlusherIntroduction(intro)
+	})
+
+	t.Run("merger introduction reset for reuse", func(t *testing.T) {
+		intro := generateMergerIntroduction()
+
+		// Set up merger introduction with data
+		intro.merged[1] = struct{}{}
+		intro.merged[2] = struct{}{}
+		intro.newPart = &part{}
+		intro.applied = make(chan struct{})
+
+		// Reset the merger introduction
+		intro.reset()
+
+		// Verify all fields are cleared
+		assert.Len(t, intro.merged, 0, "merged map should be empty")
+		assert.Nil(t, intro.newPart, "newPart should be nil")
+		assert.Nil(t, intro.applied, "applied channel should be nil")
+
+		releaseMergerIntroduction(intro)
+	})
+}
+
+func TestChannelSynchronization(t *testing.T) {
+	t.Run("applied notifications work reliably", func(t *testing.T) {
+		intro := generateIntroduction()
+		intro.applied = make(chan struct{})
+
+		// Start a goroutine that waits for the notification
+		done := make(chan bool)
+		go func() {
+			select {
+			case <-intro.applied:
+				done <- true
+			case <-time.After(100 * time.Millisecond):
+				done <- false
+			}
+		}()
+
+		// Close the applied channel to simulate completion
+		close(intro.applied)
+
+		// Check that the notification was received
+		result := <-done
+		assert.True(t, result, "applied notification should be received")
+
+		releaseIntroduction(intro)
+	})
+
+	t.Run("multiple waiters on applied channel", func(t *testing.T) {
+		intro := generateFlusherIntroduction()
+		intro.applied = make(chan struct{})
+
+		const numWaiters = 5
+		var wg sync.WaitGroup
+		results := make(chan bool, numWaiters)
+
+		// Start multiple waiters
+		for i := 0; i < numWaiters; i++ {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				select {
+				case <-intro.applied:
+					results <- true
+				case <-time.After(100 * time.Millisecond):
+					results <- false
+				}
+			}()
+		}
+
+		// Close the applied channel
+		close(intro.applied)
+		wg.Wait()
+		close(results)
+
+		// All waiters should receive the notification
+		successCount := 0
+		for result := range results {
+			if result {
+				successCount++
+			}
+		}
+		assert.Equal(t, numWaiters, successCount, "all waiters should receive notification")
+
+		releaseFlusherIntroduction(intro)
+	})
+
+	t.Run("channel synchronization correctness", func(t *testing.T) {
+		intro := generateMergerIntroduction()
+		intro.applied = make(chan struct{})
+
+		// Test that reading from a closed channel doesn't block
+		close(intro.applied)
+
+		// Multiple reads should work
+		for i := 0; i < 3; i++ {
+			select {
+			case <-intro.applied:
+				// Should not block
+			default:
+				t.Errorf("reading from closed channel should not block")
+			}
+		}
+
+		releaseMergerIntroduction(intro)
+	})
+}
+
+func TestIntroductionGeneration(t *testing.T) {
+	t.Run("introduction generation creates valid instances", func(t *testing.T) {
+		intro := generateIntroduction()
+		require.NotNil(t, intro, "generated introduction should not be nil")
+		assert.Nil(t, intro.memPart, "initial memPart should be nil")
+		assert.Nil(t, intro.applied, "initial applied channel should be nil")
+		releaseIntroduction(intro)
+	})
+
+	t.Run("flusher introduction generation creates valid instances", func(t *testing.T) {
+		intro := generateFlusherIntroduction()
+		require.NotNil(t, intro, "generated flusher introduction should not be nil")
+		require.NotNil(t, intro.flushed, "flushed map should be initialized")
+		assert.Len(t, intro.flushed, 0, "flushed map should be empty")
+		assert.Nil(t, intro.applied, "initial applied channel should be nil")
+		releaseFlusherIntroduction(intro)
+	})
+
+	t.Run("merger introduction generation creates valid instances", func(t *testing.T) {
+		intro := generateMergerIntroduction()
+		require.NotNil(t, intro, "generated merger introduction should not be nil")
+		require.NotNil(t, intro.merged, "merged map should be initialized")
+		assert.Len(t, intro.merged, 0, "merged map should be empty")
+		assert.Nil(t, intro.newPart, "initial newPart should be nil")
+		assert.Nil(t, intro.applied, "initial applied channel should be nil")
+		releaseMergerIntroduction(intro)
+	})
+}
+
+func TestConcurrentPoolAccess(t *testing.T) {
+	t.Run("concurrent pool access is safe", func(t *testing.T) {
+		const numGoroutines = 10
+		const operationsPerGoroutine = 100
+
+		var wg sync.WaitGroup
+
+		// Test concurrent access to introduction pool
+		for i := 0; i < numGoroutines; i++ {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				for j := 0; j < operationsPerGoroutine; j++ {
+					intro := generateIntroduction()
+					intro.memPart = &memPart{}
+					intro.applied = make(chan struct{})
+					releaseIntroduction(intro)
+				}
+			}()
+		}
+
+		wg.Wait()
+		// Test should complete without data races
+	})
+
+	t.Run("concurrent flusher introduction pool access", func(t *testing.T) {
+		const numGoroutines = 10
+		const operationsPerGoroutine = 100
+
+		var wg sync.WaitGroup
+
+		for i := 0; i < numGoroutines; i++ {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				for j := 0; j < operationsPerGoroutine; j++ {
+					intro := generateFlusherIntroduction()
+					intro.flushed[uint64(j)] = &part{}
+					intro.applied = make(chan struct{})
+					releaseFlusherIntroduction(intro)
+				}
+			}()
+		}
+
+		wg.Wait()
+	})
+
+	t.Run("concurrent merger introduction pool access", func(t *testing.T) {
+		const numGoroutines = 10
+		const operationsPerGoroutine = 100
+
+		var wg sync.WaitGroup
+
+		for i := 0; i < numGoroutines; i++ {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				for j := 0; j < operationsPerGoroutine; j++ {
+					intro := generateMergerIntroduction()
+					intro.merged[uint64(j)] = struct{}{}
+					intro.newPart = &part{}
+					intro.applied = make(chan struct{})
+					releaseMergerIntroduction(intro)
+				}
+			}()
+		}
+
+		wg.Wait()
+	})
+}
+
+func TestIntroductionMapOperations(t *testing.T) {
+	t.Run("flusher introduction map operations", func(t *testing.T) {
+		intro := generateFlusherIntroduction()
+
+		// Add parts to flushed map
+		part1 := &part{}
+		part2 := &part{}
+		intro.flushed[1] = part1
+		intro.flushed[2] = part2
+
+		// Verify map contents
+		assert.Equal(t, part1, intro.flushed[1], "part1 should be retrievable")
+		assert.Equal(t, part2, intro.flushed[2], "part2 should be retrievable")
+		assert.Len(t, intro.flushed, 2, "flushed map should have 2 entries")
+
+		// Reset should clear the map
+		intro.reset()
+		assert.Len(t, intro.flushed, 0, "flushed map should be empty after reset")
+
+		releaseFlusherIntroduction(intro)
+	})
+
+	t.Run("merger introduction map operations", func(t *testing.T) {
+		intro := generateMergerIntroduction()
+
+		// Add IDs to merged map
+		intro.merged[1] = struct{}{}
+		intro.merged[2] = struct{}{}
+		intro.merged[3] = struct{}{}
+
+		// Verify map contents
+		_, exists1 := intro.merged[1]
+		_, exists2 := intro.merged[2]
+		_, exists3 := intro.merged[3]
+		_, exists4 := intro.merged[4]
+
+		assert.True(t, exists1, "ID 1 should exist in merged map")
+		assert.True(t, exists2, "ID 2 should exist in merged map")
+		assert.True(t, exists3, "ID 3 should exist in merged map")
+		assert.False(t, exists4, "ID 4 should not exist in merged map")
+		assert.Len(t, intro.merged, 3, "merged map should have 3 entries")
+
+		// Reset should clear the map
+		intro.reset()
+		assert.Len(t, intro.merged, 0, "merged map should be empty after reset")
+
+		releaseMergerIntroduction(intro)
+	})
+}
+
+func TestNilSafetyForIntroductions(t *testing.T) {
+	t.Run("release nil introduction", func(_ *testing.T) {
+		// Should not panic
+		releaseIntroduction(nil)
+	})
+
+	t.Run("release nil flusher introduction", func(_ *testing.T) {
+		// Should not panic
+		releaseFlusherIntroduction(nil)
+	})
+
+	t.Run("release nil merger introduction", func(_ *testing.T) {
+		// Should not panic
+		releaseMergerIntroduction(nil)
+	})
+}
\ No newline at end of file