Skip to content

Commit f5f277b

Browse files
authored
fix: batch insert logs (#48)
* test: add benchmark for session log writer * feat: implement batch import * chore: remove debug fmt.Println
1 parent 0947ae5 commit f5f277b

6 files changed

Lines changed: 156 additions & 48 deletions

File tree

server/internal/db/dbext/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type kqlParse struct {
1616

1717
// Parse implements the ku_parse function.
1818
//
19-
// select json_extract(ku_parse(lines, '.*'), '$.foo') as foo from source
19+
// select json_extract(ku_parse(lines, '.*'), '$.foo') as foo from source
2020
func (p *kqlParse) Parse(fieldValue string, regexpPattern string) (string, error) {
2121
re, err := regexp.Compile("(?m)" + regexpPattern)
2222
if err != nil {

server/internal/db/session_io.go

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import (
99
"time"
1010
)
1111

12+
// TODO(hbc): tune based on usage?
13+
const (
14+
sessionLogBuffer = 500
15+
sessionLogSendInterval = 50 * time.Millisecond
16+
)
17+
18+
type sendSessionLog struct {
19+
Line string
20+
ScanErr error
21+
}
22+
1223
type sessionLogWriter struct {
1324
Session
1425

@@ -17,12 +28,15 @@ type sessionLogWriter struct {
1728

1829
scannerInput io.WriteCloser
1930
scanner *bufio.Scanner
20-
scannerErr error
2131
scanning *sync.WaitGroup
32+
33+
sendLogsChan chan sendSessionLog
34+
sending *sync.WaitGroup
35+
sendErr error
2236
}
2337

2438
// SessionLogWriteCloser wraps the session as a io.WriteCloser instance.
25-
// The behavior of concurrency write to this writer is undefined.
39+
// It's not allowed to call Write() concurrently.
2640
func SessionLogWriteCloser(
2741
rootCtx context.Context,
2842
session Session,
@@ -40,59 +54,114 @@ func SessionLogWriteCloser(
4054
scannerInput: pw,
4155
scanner: bufio.NewScanner(pr),
4256
scanning: new(sync.WaitGroup),
57+
58+
sendLogsChan: make(chan sendSessionLog, sessionLogBuffer),
59+
sending: new(sync.WaitGroup),
4360
}
4461

4562
rv.scanning.Add(1)
4663
go rv.scan()
4764

65+
rv.sending.Add(1)
66+
go rv.sendLogs()
67+
4868
return rv
4969
}
5070

5171
var _ io.WriteCloser = (*sessionLogWriter)(nil)
5272

53-
func (swr *sessionLogWriter) writeLogLine(logLine string) error {
54-
ctx, cancel := swr.createCtx()
55-
defer cancel()
73+
func (swr *sessionLogWriter) sendLogs() {
74+
defer swr.sending.Done()
5675

57-
err := swr.Session.WriteLogLine(
58-
ctx,
59-
WriteLogLinePayload{
76+
sendTicker := time.NewTicker(sessionLogSendInterval)
77+
defer sendTicker.Stop()
78+
79+
sendLogs := func(lines []string) error {
80+
if len(lines) == 0 {
81+
return nil
82+
}
83+
84+
ctx, cancel := swr.createCtx()
85+
defer cancel()
86+
87+
err := swr.Session.WriteLogLinesBatch(ctx, WriteLogLinesBatchPayload{
88+
Lines: lines,
6089
Timestamp: swr.nowFn(),
61-
Line: logLine,
62-
},
63-
)
64-
if err != nil {
65-
return fmt.Errorf("WriteLogLine: %w", err)
90+
})
91+
if err != nil {
92+
return fmt.Errorf("WriteLogLinesBatch: %w", err)
93+
}
94+
95+
return nil
6696
}
6797

68-
return nil
98+
var lines []string
99+
for {
100+
select {
101+
case sendLog, ok := <-swr.sendLogsChan:
102+
if !ok || sendLog.ScanErr != nil {
103+
// channel closed or scan failed, send all logs and store error
104+
sendErr := sendLogs(lines)
105+
if sendErr != nil {
106+
swr.sendErr = sendErr
107+
} else if sendLog.ScanErr != nil {
108+
swr.sendErr = sendLog.ScanErr
109+
}
110+
return
111+
}
112+
113+
// append logs to send buffer
114+
lines = append(lines, sendLog.Line)
115+
case <-sendTicker.C:
116+
// send ticker fired, send all buffered logs
117+
if err := sendLogs(lines); err != nil {
118+
swr.sendErr = err
119+
return
120+
}
121+
lines = nil
122+
}
123+
}
69124
}
70125

71126
func (swr *sessionLogWriter) scan() {
72127
defer swr.scanning.Done()
73128

74129
for swr.scanner.Scan() {
75-
if err := swr.writeLogLine(swr.scanner.Text()); err != nil {
76-
swr.scannerErr = err
77-
return
130+
swr.sendLogsChan <- sendSessionLog{
131+
Line: swr.scanner.Text(),
78132
}
79133
}
80134

81135
if err := swr.scanner.Err(); err != nil {
82-
swr.scannerErr = err
136+
swr.sendLogsChan <- sendSessionLog{
137+
ScanErr: err,
138+
}
83139
}
84140
}
85141

86142
func (swr *sessionLogWriter) Write(p []byte) (int, error) {
143+
// FIXME(hbc): race on the sendErr
144+
if swr.sendErr != nil {
145+
return 0, swr.sendErr
146+
}
147+
87148
return swr.scannerInput.Write(p)
88149
}
89150

90151
func (swr *sessionLogWriter) Close() error {
152+
// stop scanner
91153
if err := swr.scannerInput.Close(); err != nil {
92154
return err
93155
}
94-
95-
// wait for scanner to finish
96156
swr.scanning.Wait()
97-
return swr.scannerErr
157+
158+
// stop logs sender
159+
close(swr.sendLogsChan)
160+
swr.sending.Wait()
161+
162+
if swr.sendErr != nil {
163+
return swr.sendErr
164+
}
165+
166+
return nil
98167
}

server/internal/db/session_io_test.go

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,24 @@ package db
22

33
import (
44
"context"
5+
"path/filepath"
56
"testing"
67
"time"
78

89
"github.com/stretchr/testify/require"
910
)
1011

1112
type mockSession struct {
12-
WriteLogLineFuc func(ctx context.Context, payload WriteLogLinePayload) error
13+
WriteLogLinesBatchFunc func(ctx context.Context, payload WriteLogLinesBatchPayload) error
1314
}
1415

1516
var _ Session = (*mockSession)(nil)
1617

17-
func (s *mockSession) WriteLogLine(
18+
func (s *mockSession) WriteLogLinesBatch(
1819
ctx context.Context,
19-
payload WriteLogLinePayload,
20+
payload WriteLogLinesBatchPayload,
2021
) error {
21-
return s.WriteLogLineFuc(ctx, payload)
22+
return s.WriteLogLinesBatchFunc(ctx, payload)
2223
}
2324

2425
func TestSessionLogWriter(t *testing.T) {
@@ -32,11 +33,11 @@ func TestSessionLogWriter(t *testing.T) {
3233
}
3334

3435
t.Run("write serial", func(t *testing.T) {
35-
var wrote []WriteLogLinePayload
36+
var wrote []string
3637

3738
mockSession := &mockSession{
38-
WriteLogLineFuc: func(ctx context.Context, payload WriteLogLinePayload) error {
39-
wrote = append(wrote, payload)
39+
WriteLogLinesBatchFunc: func(ctx context.Context, payload WriteLogLinesBatchPayload) error {
40+
wrote = append(wrote, payload.Lines...)
4041

4142
return nil
4243
},
@@ -53,16 +54,16 @@ func TestSessionLogWriter(t *testing.T) {
5354
require.NoError(t, w.Close())
5455

5556
require.Len(t, wrote, 2)
56-
require.Equal(t, "hello", wrote[0].Line)
57-
require.Equal(t, "world", wrote[1].Line)
57+
require.Equal(t, "hello", wrote[0])
58+
require.Equal(t, "world", wrote[1])
5859
})
5960

6061
t.Run("write partial", func(t *testing.T) {
61-
var wrote []WriteLogLinePayload
62+
var wrote []string
6263

6364
mockSession := &mockSession{
64-
WriteLogLineFuc: func(ctx context.Context, payload WriteLogLinePayload) error {
65-
wrote = append(wrote, payload)
65+
WriteLogLinesBatchFunc: func(ctx context.Context, payload WriteLogLinesBatchPayload) error {
66+
wrote = append(wrote, payload.Lines...)
6667

6768
return nil
6869
},
@@ -81,8 +82,39 @@ func TestSessionLogWriter(t *testing.T) {
8182
require.NoError(t, w.Close())
8283

8384
require.Len(t, wrote, 3)
84-
require.Equal(t, "hello", wrote[0].Line)
85-
require.Equal(t, "world", wrote[1].Line)
86-
require.Equal(t, "foo", wrote[2].Line)
85+
require.Equal(t, "hello", wrote[0])
86+
require.Equal(t, "world", wrote[1])
87+
require.Equal(t, "foo", wrote[2])
8788
})
8889
}
90+
91+
// go test -run=^$ -benchtime 30s -bench ^BenchmarkSessionLogWriter$ github.com/b4fun/ku/server/internal/db
92+
func BenchmarkSessionLogWriter(b *testing.B) {
93+
run := func() {
94+
ctx, cancel := context.WithCancel(context.Background())
95+
defer cancel()
96+
97+
p := b.TempDir()
98+
dbFile := filepath.Join(p, "test.db")
99+
dbProvider, err := NewSqliteProvider(dbFile)
100+
require.NoError(b, err)
101+
102+
_, session, err := dbProvider.CreateSession(ctx, &CreateSessionOpts{Prefix: "test"})
103+
require.NoError(b, err)
104+
105+
writer := SessionLogWriteCloser(ctx, session, 100*time.Millisecond)
106+
107+
for i := 0; i < 10000; i++ {
108+
content := []byte("hello world\n")
109+
110+
_, err := writer.Write(content)
111+
require.NoError(b, err)
112+
}
113+
114+
require.NoError(b, writer.Close())
115+
}
116+
117+
for i := 0; i < b.N; i++ {
118+
run()
119+
}
120+
}

server/internal/db/sqlite_session.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,19 @@ func (s *SqliteSession) dbTableName(tableName string) string {
7979
return fmt.Sprintf("%s_%s", s.sessionID, tableName)
8080
}
8181

82-
func (s *SqliteSession) WriteLogLine(ctx context.Context, payload WriteLogLinePayload) error {
83-
const insertTmpl = `INSERT INTO %s (ts, lines) VALUES (?, ?)`
82+
func (s *SqliteSession) WriteLogLinesBatch(ctx context.Context, payload WriteLogLinesBatchPayload) error {
83+
const insertTmpl = `INSERT INTO %s (ts, lines) VALUES (:ts, :lines)`
84+
85+
insertPayload := make([]map[string]interface{}, len(payload.Lines))
86+
for i, line := range payload.Lines {
87+
insertPayload[i] = map[string]interface{}{
88+
"ts": payload.Timestamp,
89+
"lines": line,
90+
}
91+
}
8492

8593
stmt := fmt.Sprintf(insertTmpl, s.dbTableName(tableNameRaw))
86-
if _, err := s.db.ExecContext(ctx, stmt, payload.Timestamp, payload.Line); err != nil {
94+
if _, err := s.db.NamedExecContext(ctx, stmt, insertPayload); err != nil {
8795
return fmt.Errorf("failed to insert log line: %w", err)
8896
}
8997

server/internal/db/sqlite_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ func TestSqliteProvider(t *testing.T) {
5858
require.NotEmpty(tc, sessionID)
5959
require.NotNil(tc, session)
6060

61-
err = session.WriteLogLine(ctx, WriteLogLinePayload{
61+
err = session.WriteLogLinesBatch(ctx, WriteLogLinesBatchPayload{
6262
Timestamp: time.Now(),
63-
Line: "hello",
63+
Lines: []string{"hello"},
6464
})
6565
require.NoError(tc, err)
66-
err = session.WriteLogLine(ctx, WriteLogLinePayload{
66+
err = session.WriteLogLinesBatch(ctx, WriteLogLinesBatchPayload{
6767
Timestamp: time.Now(),
68-
Line: "world",
68+
Lines: []string{"world"},
6969
})
7070
require.NoError(tc, err)
7171
sqliteSession := session.(*SqliteSession)
@@ -119,7 +119,6 @@ func TestSqliteProvider(t *testing.T) {
119119
require.NotNil(tc, sessionUpdated)
120120
require.Len(tc, sessionUpdated.Tables, 2)
121121
for _, table := range sessionUpdated.Tables {
122-
fmt.Println(table.Columns)
123122
require.NotEmpty(tc, table.Columns)
124123
}
125124
})

server/internal/db/types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import (
77
v1 "github.com/b4fun/ku/protos/api/v1"
88
)
99

10-
type WriteLogLinePayload struct {
10+
type WriteLogLinesBatchPayload struct {
1111
Timestamp time.Time
12-
Line string
12+
Lines []string
1313
}
1414

1515
type Session interface {
16-
WriteLogLine(ctx context.Context, payload WriteLogLinePayload) error
16+
WriteLogLinesBatch(ctx context.Context, payload WriteLogLinesBatchPayload) error
1717
}
1818

1919
type CreateSessionOpts struct {

0 commit comments

Comments
 (0)