github.com/benz9527/toy-box/algo@v0.0.0-20240221120937-66c0c6bd5abd/pubsub/x_sp_disruptor_test.go (about)

     1  package pubsub
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"github.com/benz9527/toy-box/algo/bit"
     7  	"github.com/benz9527/toy-box/algo/bitmap"
     8  	"github.com/stretchr/testify/assert"
     9  	"log/slog"
    10  	"math/rand"
    11  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"sync"
    15  	"sync/atomic"
    16  	"testing"
    17  	"time"
    18  )
    19  
    20  func TestCeilCapacity(t *testing.T) {
    21  	testcases := []struct {
    22  		capacity uint64
    23  		ceil     uint64
    24  	}{
    25  		{0, 2},
    26  		{1, 2},
    27  		{2, 2},
    28  		{3, 4},
    29  		{4, 4},
    30  		{7, 8},
    31  		{8, 8},
    32  		{9, 16},
    33  		{16, 16},
    34  		{31, 32},
    35  		{32, 32},
    36  		{58, 64},
    37  		{64, 64},
    38  	}
    39  	for _, tc := range testcases {
    40  		t.Run(fmt.Sprintf("capacity: %d, ceil: %d", tc.capacity, tc.ceil), func(t *testing.T) {
    41  			assert.Equal(t, tc.ceil, bit.RoundupPowOf2ByCeil(tc.capacity))
    42  		})
    43  	}
    44  }
    45  
    46  func testXSinglePipelineDisruptorUint64(
    47  	t *testing.T, gTotal, tasks int, capacity uint64,
    48  	bs BlockStrategy, bitmapCheck bool,
    49  	reportFile *os.File, errorCounter *atomic.Uint64,
    50  ) {
    51  	var (
    52  		counter = &atomic.Int64{}
    53  		bm      bitmap.Bitmap
    54  		checkBM bitmap.Bitmap
    55  	)
    56  	checkBM = bitmap.NewX32Bitmap(uint64(gTotal * tasks))
    57  	if bitmapCheck {
    58  		bm = bitmap.NewX32Bitmap(uint64(gTotal * tasks))
    59  	}
    60  	wg := &sync.WaitGroup{}
    61  	wg.Add(gTotal)
    62  	rwg := &sync.WaitGroup{}
    63  	rwg.Add(gTotal * tasks)
    64  	disruptor := NewXSinglePipelineDisruptor[uint64](capacity,
    65  		bs,
    66  		func(event uint64) error {
    67  			defer rwg.Done()
    68  			counter.Add(1)
    69  			if bitmapCheck {
    70  				bm.SetBit(event, true)
    71  			}
    72  			return nil
    73  		},
    74  	)
    75  	if err := disruptor.Start(); err != nil {
    76  		t.Fatalf("disruptor start failed, err: %v", err)
    77  	}
    78  	for i := 0; i < gTotal; i++ {
    79  		for j := 0; j < tasks; j++ {
    80  			checkBM.SetBit(uint64(i*tasks+j), true)
    81  		}
    82  	}
    83  	beginTs := time.Now()
    84  	for i := 0; i < gTotal; i++ {
    85  		go func(idx int) {
    86  			defer wg.Done()
    87  			for j := 0; j < tasks; j++ {
    88  				if _, _, err := disruptor.Publish(uint64(idx*tasks + j)); err != nil {
    89  					t.Logf("publish failed, err: %v", err)
    90  					if errorCounter != nil {
    91  						errorCounter.Add(1)
    92  					}
    93  					break
    94  				}
    95  			}
    96  		}(i)
    97  	}
    98  	wg.Wait()
    99  	diff := time.Now().Sub(beginTs)
   100  	summary := fmt.Sprintf("published total: %d, tasks: %d, cost: %v, tps: %v/s", gTotal, tasks, diff, float64(gTotal*tasks)/diff.Seconds())
   101  	t.Log(summary)
   102  	if reportFile != nil {
   103  		_, _ = reportFile.WriteString(summary + "\n")
   104  	}
   105  	rwg.Wait()
   106  	if reportFile == nil {
   107  		time.Sleep(time.Second)
   108  		assert.Equal(t, int64(gTotal*tasks), counter.Load())
   109  	}
   110  	err := disruptor.Stop()
   111  	assert.NoError(t, err)
   112  	if bitmapCheck {
   113  		if reportFile != nil {
   114  			_, _ = reportFile.WriteString(fmt.Sprintf("gTotal(%d), tasks(%d):\n", gTotal, tasks))
   115  		}
   116  		bm1bits := bm.GetBits()
   117  		bm2bits := checkBM.GetBits()
   118  		if !bm.EqualTo(checkBM) {
   119  			if reportFile != nil {
   120  				_, _ = reportFile.WriteString("bitmap check failed by not equal!\n")
   121  			}
   122  			if errorCounter != nil {
   123  				errorCounter.Add(1)
   124  			}
   125  			for i := 0; i < len(bm1bits); i++ {
   126  				if bytes.Compare(bm1bits[i:i+1], bm2bits[i:i+1]) != 0 {
   127  					if reportFile != nil {
   128  						_, _ = reportFile.WriteString(fmt.Sprintf("idx: %d, bm1: %08b, bm2: %08b\n", i, bm1bits[i:i+1], bm2bits[i:i+1]))
   129  					}
   130  					t.Logf("idx: %d, bm1: %08b, bm2: %08b\n", i, bm1bits[i:i+1], bm2bits[i:i+1])
   131  				}
   132  			}
   133  		}
   134  		// check store whether contains zero bits
   135  		if reportFile != nil {
   136  			_, _ = reportFile.WriteString("check store whether contains zero bits(exclude the last one):\n")
   137  			for i := 0; i < len(bm2bits)-1; i++ {
   138  				if bm2bits[i]&0xf != 0xf {
   139  					_, _ = reportFile.WriteString(fmt.Sprintf("store idx: %d, bm2: %08b\n", i, bm2bits[i:i+1]))
   140  				}
   141  			}
   142  			_, _ = reportFile.WriteString("====== end report ======\n")
   143  		}
   144  	}
   145  	if bm != nil {
   146  		bm.Free()
   147  	}
   148  	if checkBM != nil {
   149  		checkBM.Free()
   150  	}
   151  }
   152  
   153  func testXSinglePipelineDisruptorString(t *testing.T, gTotal, tasks int, capacity uint64, bs BlockStrategy, bitmapCheck bool, reportFile *os.File, errorCounter *atomic.Uint64) {
   154  	var (
   155  		counter = &atomic.Int64{}
   156  		bm      bitmap.Bitmap
   157  		checkBM bitmap.Bitmap
   158  	)
   159  	if bitmapCheck {
   160  		bm = bitmap.NewX32Bitmap(uint64(gTotal * tasks))
   161  		checkBM = bitmap.NewX32Bitmap(uint64(gTotal * tasks))
   162  	}
   163  	wg := &sync.WaitGroup{}
   164  	wg.Add(gTotal)
   165  	rwg := &sync.WaitGroup{}
   166  	rwg.Add(gTotal * tasks)
   167  	disruptor := NewXSinglePipelineDisruptor[string](capacity,
   168  		bs,
   169  		func(event string) error {
   170  			defer func() {
   171  				if r := recover(); r != nil {
   172  					t.Logf("error panic: %v", r)
   173  					if reportFile != nil {
   174  						_, _ = reportFile.WriteString(fmt.Sprintf("error panic: %v\n", r))
   175  					}
   176  					if errorCounter != nil {
   177  						errorCounter.Add(1)
   178  					}
   179  				}
   180  				rwg.Done()
   181  			}()
   182  			counter.Add(1)
   183  			if bitmapCheck {
   184  				e, err := strconv.ParseUint(event, 10, 64)
   185  				if err != nil {
   186  					t.Logf("error parse uint64 failed, err: %v", err)
   187  					if reportFile != nil {
   188  						_, _ = reportFile.WriteString(fmt.Sprintf("error parse uint64 failed, err: %v\n", err))
   189  					}
   190  				}
   191  				bm.SetBit(e, true)
   192  			}
   193  			if event == "" {
   194  				t.Logf("error event is empty, counter: %d", counter.Load())
   195  			}
   196  			return nil
   197  		},
   198  	)
   199  	if err := disruptor.Start(); err != nil {
   200  		t.Fatalf("disruptor start failed, err: %v", err)
   201  	}
   202  	for i := 0; i < gTotal; i++ {
   203  		for j := 0; j < tasks; j++ {
   204  			checkBM.SetBit(uint64(i*tasks+j), true)
   205  		}
   206  	}
   207  	beginTs := time.Now()
   208  	for i := 0; i < gTotal; i++ {
   209  		go func(idx int) {
   210  			defer wg.Done()
   211  			for j := 0; j < tasks; j++ {
   212  				if _, _, err := disruptor.Publish(fmt.Sprintf("%d", idx*tasks+j)); err != nil {
   213  					t.Logf("publish failed, err: %v", err)
   214  					if errorCounter != nil {
   215  						errorCounter.Add(1)
   216  					}
   217  					break
   218  				}
   219  			}
   220  		}(i)
   221  	}
   222  	wg.Wait()
   223  	diff := time.Now().Sub(beginTs)
   224  	summary := fmt.Sprintf("published total: %d, tasks: %d, cost: %v, tps: %v/s", gTotal, tasks, diff, float64(gTotal*tasks)/diff.Seconds())
   225  	t.Log(summary)
   226  	if reportFile != nil {
   227  		_, _ = reportFile.WriteString(summary + "\n")
   228  	}
   229  	rwg.Wait()
   230  	if reportFile == nil {
   231  		time.Sleep(time.Second)
   232  		assert.Equal(t, int64(gTotal*tasks), counter.Load())
   233  	}
   234  	err := disruptor.Stop()
   235  	assert.NoError(t, err)
   236  	if bitmapCheck {
   237  		if reportFile != nil {
   238  			_, _ = reportFile.WriteString(fmt.Sprintf("gTotal(%d), tasks(%d):\n", gTotal, tasks))
   239  		}
   240  		bm1bits := bm.GetBits()
   241  		bm2bits := checkBM.GetBits()
   242  		if !bm.EqualTo(checkBM) {
   243  			if reportFile != nil {
   244  				_, _ = reportFile.WriteString("bitmap check failed by not equal!\n")
   245  			}
   246  			if errorCounter != nil {
   247  				errorCounter.Add(1)
   248  			}
   249  			for i := 0; i < len(bm1bits); i++ {
   250  				if bytes.Compare(bm1bits[i:i+1], bm2bits[i:i+1]) != 0 {
   251  					if reportFile != nil {
   252  						_, _ = reportFile.WriteString(fmt.Sprintf("error store idx: %d, bm1: %08b, bm2: %08b\n", i, bm1bits[i:i+1], bm2bits[i:i+1]))
   253  					}
   254  					t.Logf("idx: %d, bm1: %08b, bm2: %08b\n", i, bm1bits[i:i+1], bm2bits[i:i+1])
   255  				}
   256  			}
   257  		}
   258  		// check store whether contains zero bits
   259  		if reportFile != nil {
   260  			_, _ = reportFile.WriteString("check store whether contains zero bits(exclude the last one):\n")
   261  			for i := 0; i < len(bm2bits)-1; i++ {
   262  				if bm2bits[i]&0xf != 0xf {
   263  					_, _ = reportFile.WriteString(fmt.Sprintf("error store idx: %d, bm2: %08b\n", i, bm2bits[i:i+1]))
   264  				}
   265  			}
   266  			_, _ = reportFile.WriteString("====== end report ======\n")
   267  		}
   268  	}
   269  	if bm != nil {
   270  		bm.Free()
   271  	}
   272  	if checkBM != nil {
   273  		checkBM.Free()
   274  	}
   275  }
   276  
   277  func TestXSinglePipelineDisruptor(t *testing.T) {
   278  	testcases := []struct {
   279  		name   string
   280  		gTotal int
   281  		tasks  int
   282  		bs     BlockStrategy
   283  	}{
   284  		{"gosched 10*100", 10, 100, NewXGoSchedBlockStrategy()},
   285  		{"gosched 100*10000", 100, 10000, NewXGoSchedBlockStrategy()},
   286  		{"gosched 500*10000", 500, 10000, NewXGoSchedBlockStrategy()},
   287  		{"gosched 1000*10000", 1000, 10000, NewXGoSchedBlockStrategy()},
   288  		{"gosched 5000*10000", 5000, 10000, NewXGoSchedBlockStrategy()},
   289  		{"gosched 10000*10000", 10000, 10000, NewXGoSchedBlockStrategy()},
   290  		{"nochan 5000*10000", 5000, 10000, NewXCacheChannelBlockStrategy()},
   291  		{"cond 5000*10000", 5000, 10000, NewXCondBlockStrategy()},
   292  	}
   293  	for _, tc := range testcases {
   294  		t.Run(tc.name, func(t *testing.T) {
   295  			testXSinglePipelineDisruptorUint64(t, tc.gTotal, tc.tasks, 1024*1024, tc.bs, false, nil, nil)
   296  		})
   297  	}
   298  }
   299  
   300  func TestXSinglePipelineDisruptorWithBitmapCheck(t *testing.T) {
   301  	testcases := []struct {
   302  		name   string
   303  		gTotal int
   304  		tasks  int
   305  		bs     BlockStrategy
   306  	}{
   307  		{"gosched 1*10000", 1, 10000, NewXGoSchedBlockStrategy()},
   308  		{"nocachech 1*10000", 1, 10000, NewXCacheChannelBlockStrategy()},
   309  		{"cond 1*10000", 1, 10000, NewXCondBlockStrategy()},
   310  		//{"gosched 10*100", 10, 100, NewXGoSchedBlockStrategy()},
   311  		//{"gosched 100*10000", 100, 10000, NewXGoSchedBlockStrategy()},
   312  		//{"gosched 500*10000", 500, 10000, NewXGoSchedBlockStrategy()},
   313  		//{"gosched 1000*10000", 1000, 10000, NewXGoSchedBlockStrategy()},
   314  		//{"gosched 5000*10000", 5000, 10000, NewXGoSchedBlockStrategy()},
   315  		//{"gosched 10000*10000", 10000, 10000, NewXGoSchedBlockStrategy()},
   316  		//{"nochan 5000*10000", 5000, 10000, NewXCacheChannelBlockStrategy()},
   317  		//{"cond 5000*10000", 5000, 10000, NewXCondBlockStrategy()},
   318  	}
   319  	for _, tc := range testcases {
   320  		t.Run(tc.name, func(t *testing.T) {
   321  			testXSinglePipelineDisruptorUint64(t, tc.gTotal, tc.tasks, 1024*1024, tc.bs, true, nil, nil)
   322  		})
   323  	}
   324  }
   325  
   326  func TestXSinglePipelineDisruptorWithBitmapCheckAndReport(t *testing.T) {
   327  	errorCounter := &atomic.Uint64{}
   328  	reportFile, err := os.OpenFile(filepath.Join(os.TempDir(), "pubsub-report-"+time.Now().Format(time.RFC3339)+".txt"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   329  	defer func() {
   330  		if reportFile != nil {
   331  			_ = reportFile.Close()
   332  		}
   333  	}()
   334  	assert.NoError(t, err)
   335  	testcases := []struct {
   336  		name     string
   337  		loop     int
   338  		gTotal   int
   339  		tasks    int
   340  		capacity uint64
   341  		bs       BlockStrategy
   342  	}{
   343  		{"gosched 1*10000", 10000, 1, 10000, 1024, NewXGoSchedBlockStrategy()},
   344  		{"gosched 10*100", 1000, 10, 100, 512, NewXGoSchedBlockStrategy()},
   345  		{"gosched 10*100", 1000, 10, 100, 1024, NewXGoSchedBlockStrategy()},
   346  		{"gosched 100*10000", 200, 100, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   347  		{"gosched 500*10000", 10, 500, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   348  		{"gosched 1000*10000", 10, 1000, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   349  		{"gosched 5000*10000", 10, 5000, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   350  		{"gosched 10000*10000", 5, 10000, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   351  		//{"chan 1*10000", 10000, 1, 10000, 1024, NewXCacheChannelBlockStrategy()},
   352  		//{"chan 10*100", 1000, 10, 100, 512, NewXCacheChannelBlockStrategy()},
   353  		//{"chan 10*100", 1000, 10, 100, 1024, NewXCacheChannelBlockStrategy()},
   354  		//{"chan 100*10000", 200, 100, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   355  		//{"chan 500*10000", 10, 500, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   356  		//{"chan 1000*10000", 10, 1000, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   357  		//{"chan 5000*10000", 10, 5000, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   358  		//{"chan 10000*10000", 5, 10000, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   359  		//{"cond 1*10000", 10000, 1, 10000, 1024, NewXCondBlockStrategy()},
   360  	}
   361  	for _, tc := range testcases {
   362  		t.Run(tc.name, func(t *testing.T) {
   363  			for i := 0; i < tc.loop; i++ {
   364  				_, _ = reportFile.WriteString(fmt.Sprintf("\n====== begin uint64 report(%s, %d) ======\n", tc.name, i))
   365  				testXSinglePipelineDisruptorUint64(t, tc.gTotal, tc.tasks, tc.capacity, tc.bs, true, reportFile, errorCounter)
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestXSinglePipelineDisruptorWithBitmapCheckAndReport_str(t *testing.T) {
   372  	errorCounter := &atomic.Uint64{}
   373  	reportFile, err := os.OpenFile(filepath.Join(os.TempDir(), "pubsub-report-str-"+time.Now().Format(time.RFC3339)+".txt"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   374  	defer func() {
   375  		if reportFile != nil {
   376  			_ = reportFile.Close()
   377  		}
   378  	}()
   379  	assert.NoError(t, err)
   380  	testcases := []struct {
   381  		name     string
   382  		loop     int
   383  		gTotal   int
   384  		tasks    int
   385  		capacity uint64
   386  		bs       BlockStrategy
   387  	}{
   388  		{"gosched 1*10000 str", 1000, 1, 10000, 1024, NewXGoSchedBlockStrategy()},
   389  		{"gosched 10*100 str", 1000, 10, 100, 512, NewXGoSchedBlockStrategy()},
   390  		{"gosched 10*100 str", 1000, 10, 100, 1024, NewXGoSchedBlockStrategy()},
   391  		{"gosched 100*10000 str", 1000, 100, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   392  		{"gosched 500*10000 str", 100, 500, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   393  		{"gosched 1000*10000 str", 10, 1000, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   394  		{"gosched 5000*10000 str", 10, 5000, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   395  		{"gosched 10000*10000 str", 5, 10000, 10000, 1024 * 1024, NewXGoSchedBlockStrategy()},
   396  		//{"chan 1*10000 str", 1000, 1, 10000, 1024, NewXCacheChannelBlockStrategy()},
   397  		//{"chan 10*100 str", 1000, 10, 100, 512, NewXCacheChannelBlockStrategy()},
   398  		//{"chan 10*100 str", 1000, 10, 100, 1024, NewXCacheChannelBlockStrategy()},
   399  		//{"chan 100*10000 str", 1000, 100, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   400  		//{"chan 500*10000 str", 10, 500, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   401  		//{"chan 1000*10000 str", 10, 1000, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   402  		//{"chan 5000*10000 str", 10, 5000, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   403  		//{"chan 10000*10000 str", 5, 10000, 10000, 1024 * 1024, NewXCacheChannelBlockStrategy()},
   404  		//{"cond 1*10000 str", 1000, 1, 10000, 1024, NewXCondBlockStrategy()},
   405  	}
   406  	for _, tc := range testcases {
   407  		t.Run(tc.name, func(t *testing.T) {
   408  			for i := 0; i < tc.loop; i++ {
   409  				_, _ = reportFile.WriteString(fmt.Sprintf("\n====== begin string report(%s, %d) ======\n", tc.name, i))
   410  				testXSinglePipelineDisruptorString(t, tc.gTotal, tc.tasks, tc.capacity, tc.bs, true, reportFile, errorCounter)
   411  			}
   412  		})
   413  	}
   414  	t.Logf("errors: %d\n", errorCounter.Load())
   415  }
   416  
   417  func testNoCacheChannel(t *testing.T, chSize, gTotal, tasks int) {
   418  	counter := &atomic.Int64{}
   419  	wg := &sync.WaitGroup{}
   420  	wg.Add(gTotal)
   421  	var ch chan int
   422  	if chSize > 0 {
   423  		ch = make(chan int, chSize)
   424  	} else {
   425  		ch = make(chan int)
   426  	}
   427  	go func() {
   428  		for range ch {
   429  			counter.Add(1)
   430  		}
   431  	}()
   432  	beginTs := time.Now()
   433  	for i := 0; i < gTotal; i++ {
   434  		go func() {
   435  			defer wg.Done()
   436  			for j := 0; j < tasks; j++ {
   437  				ch <- j
   438  			}
   439  		}()
   440  	}
   441  	wg.Wait()
   442  	diff := time.Now().Sub(beginTs)
   443  	t.Logf("total: %d, tasks: %d, cost: %v, tps: %v/s", gTotal, tasks, diff, float64(gTotal*tasks)/diff.Seconds())
   444  	time.Sleep(time.Second)
   445  	assert.Equal(t, int64(gTotal*tasks), counter.Load())
   446  }
   447  
   448  func TestNoCacheChannel(t *testing.T) {
   449  	testcases := []struct {
   450  		name   string
   451  		gTotal int
   452  		tasks  int
   453  	}{
   454  		{"nochan 10*100", 10, 100},
   455  		{"nochan 100*10000", 100, 10000},
   456  		{"nochan 500*10000", 500, 10000},
   457  		{"nochan 1000*10000", 1000, 10000},
   458  		{"nochan 5000*10000", 5000, 10000},
   459  		{"nochan 10000*10000", 10000, 10000},
   460  	}
   461  	for _, tc := range testcases {
   462  		t.Run(tc.name, func(t *testing.T) {
   463  			testNoCacheChannel(t, 0, tc.gTotal, tc.tasks)
   464  		})
   465  	}
   466  }
   467  
   468  func TestCacheChannel(t *testing.T) {
   469  	testcases := []struct {
   470  		name   string
   471  		gTotal int
   472  		tasks  int
   473  	}{
   474  		{"cachechan 10*100", 10, 100},
   475  		{"cachechan 100*10000", 100, 10000},
   476  		{"cachechan 500*10000", 500, 10000},
   477  		{"cachechan 1000*10000", 1000, 10000},
   478  		{"cachechan 5000*10000", 5000, 10000},
   479  		{"cachechan 10000*10000", 10000, 10000},
   480  	}
   481  	for _, tc := range testcases {
   482  		t.Run(tc.name, func(t *testing.T) {
   483  			testNoCacheChannel(t, 1024*1024, tc.gTotal, tc.tasks)
   484  		})
   485  	}
   486  }
   487  
   488  func testXSinglePipelineDisruptorWithRandomSleep(t *testing.T, num, capacity int) {
   489  	wg := &sync.WaitGroup{}
   490  	wg.Add(num)
   491  	results := map[string]struct{}{}
   492  	disruptor := NewXSinglePipelineDisruptor[string](uint64(capacity),
   493  		NewXCacheChannelBlockStrategy(),
   494  		func(event string) error {
   495  			nextInt := rand.Intn(100)
   496  			time.Sleep(time.Duration(nextInt) * time.Millisecond)
   497  			results[event] = struct{}{}
   498  			wg.Done()
   499  			return nil
   500  		},
   501  	)
   502  	if err := disruptor.Start(); err != nil {
   503  		t.Fatalf("disruptor start failed, err: %v", err)
   504  	}
   505  	for i := 0; i < num; i++ {
   506  		if _, _, err := disruptor.Publish(fmt.Sprintf("event-%d", i)); err != nil {
   507  			t.Logf("publish failed, err: %v", err)
   508  		}
   509  	}
   510  	wg.Wait()
   511  	err := disruptor.Stop()
   512  	assert.NoError(t, err)
   513  	assert.Equal(t, num, len(results))
   514  	for i := 0; i < num; i++ {
   515  		assert.Contains(t, results, fmt.Sprintf("event-%d", i))
   516  	}
   517  }
   518  
   519  func TestXSinglePipelineDisruptorWithRandomSleepEvent(t *testing.T) {
   520  	testcases := []struct {
   521  		num      int
   522  		capacity int
   523  	}{
   524  		{10, 2},
   525  		{100, 4},
   526  		{200, 10},
   527  		{500, 20},
   528  	}
   529  	loops := 2
   530  	for i := 0; i < loops; i++ {
   531  		for _, tc := range testcases {
   532  			t.Run(fmt.Sprintf("num: %d, capacity: %d", tc.num, tc.capacity), func(t *testing.T) {
   533  				testXSinglePipelineDisruptorWithRandomSleep(t, tc.num, tc.capacity)
   534  			})
   535  		}
   536  	}
   537  }
   538  
   539  func TestXSinglePipelineDisruptor_PublishTimeout(t *testing.T) {
   540  	num := 10
   541  	disruptor := NewXSinglePipelineDisruptor[string](2,
   542  		NewXGoSchedBlockStrategy(),
   543  		func(event string) error {
   544  			nextInt := rand.Intn(10)
   545  			if nextInt == 0 {
   546  				nextInt = 2
   547  			}
   548  			time.Sleep(time.Duration(nextInt) * time.Millisecond)
   549  			slog.Info("handle event details", "name", event)
   550  			return nil
   551  		},
   552  	)
   553  	if err := disruptor.Start(); err != nil {
   554  		t.Fatalf("disruptor start failed, err: %v", err)
   555  	}
   556  	for i := 0; i < num; i++ {
   557  		event := fmt.Sprintf("event-%d", i)
   558  		disruptor.PublishTimeout(event, 5*time.Millisecond)
   559  	}
   560  	time.Sleep(500 * time.Millisecond)
   561  	err := disruptor.Stop()
   562  	assert.NoError(t, err)
   563  }