fix(go): singleflight tdengine child table ensures
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -318,6 +319,47 @@ func TestSchemaStatementsCreateRawFramePayloadChunks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterEnsuresSameChildTableOnceUnderConcurrency(t *testing.T) {
|
||||
exec := newBlockingExec("CREATE TABLE IF NOT EXISTS raw_")
|
||||
writer := NewWriter(exec)
|
||||
first := sampleEnvelope()
|
||||
second := sampleEnvelope()
|
||||
second.Sequence = 2
|
||||
second.EventID = "second"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, 2)
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs <- writer.AppendAll(context.Background(), first)
|
||||
}()
|
||||
<-exec.blocked
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errs <- writer.AppendAll(context.Background(), second)
|
||||
}()
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
exec.release <- struct{}{}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("AppendAll() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if got := exec.count("CREATE TABLE IF NOT EXISTS raw_"); got != 1 {
|
||||
t.Fatalf("raw child CREATE count = %d, want 1; calls=%#v", got, exec.calls())
|
||||
}
|
||||
if got := exec.count("ALTER TABLE raw_"); got != 1 {
|
||||
t.Fatalf("raw child ALTER count = %d, want 1; calls=%#v", got, exec.calls())
|
||||
}
|
||||
if got := exec.count("INSERT INTO raw_"); got != 2 {
|
||||
t.Fatalf("raw insert count = %d, want 2; calls=%#v", got, exec.calls())
|
||||
}
|
||||
}
|
||||
|
||||
func sampleEnvelope() envelope.FrameEnvelope {
|
||||
return envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
@@ -392,3 +434,45 @@ func (e *recordingExec) ExecContext(_ context.Context, query string, args ...any
|
||||
e.calls = append(e.calls, execCall{query: query, args: args})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type blockingExec struct {
|
||||
mu sync.Mutex
|
||||
recorded []execCall
|
||||
blockPattern string
|
||||
blocked chan struct{}
|
||||
release chan struct{}
|
||||
blockOnce sync.Once
|
||||
}
|
||||
|
||||
func newBlockingExec(blockPattern string) *blockingExec {
|
||||
return &blockingExec{
|
||||
blockPattern: blockPattern,
|
||||
blocked: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *blockingExec) ExecContext(_ context.Context, query string, args ...any) (sql.Result, error) {
|
||||
e.mu.Lock()
|
||||
e.recorded = append(e.recorded, execCall{query: query, args: args})
|
||||
e.mu.Unlock()
|
||||
if strings.Contains(query, e.blockPattern) {
|
||||
e.blockOnce.Do(func() {
|
||||
close(e.blocked)
|
||||
<-e.release
|
||||
})
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *blockingExec) calls() []execCall {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
out := make([]execCall, len(e.recorded))
|
||||
copy(out, e.recorded)
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *blockingExec) count(pattern string) int {
|
||||
return countSQL(e.calls(), pattern)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user