Skip to content

Commit dbac8f6

Browse files
(fix): cleanup event processor (#167)
* cleanup event processor * better swap. used defer (debated on that one). * add unit tests to cover changes * use an atomic property for things that are accessed in different threads. * fix lint errors * fix lint errors * fix atomic property with semaphore
1 parent 5a32678 commit dbac8f6

File tree

2 files changed

+122
-18
lines changed

2 files changed

+122
-18
lines changed

pkg/event/processor.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"sync"
2424
"time"
2525

26+
"golang.org/x/sync/semaphore"
27+
2628
"github.com/optimizely/go-sdk/pkg/logging"
2729
"github.com/optimizely/go-sdk/pkg/notification"
2830
"github.com/optimizely/go-sdk/pkg/registry"
@@ -41,9 +43,10 @@ type BatchEventProcessor struct {
4143
FlushInterval time.Duration // in milliseconds
4244
BatchSize int
4345
Q Queue
44-
Mux sync.Mutex
46+
flushLock sync.Mutex
4547
Ticker *time.Ticker
4648
EventDispatcher Dispatcher
49+
processing *semaphore.Weighted
4750
}
4851

4952
// DefaultBatchSize holds the default value for the batch size
@@ -55,6 +58,8 @@ const DefaultEventQueueSize = 100
5558
// DefaultEventFlushInterval holds the default value for the event flush interval
5659
const DefaultEventFlushInterval = 30 * time.Second
5760

61+
const maxFlushWorkers = 1
62+
5863
var pLogger = logging.GetLogger("EventProcessor")
5964

6065
// BPOptionConfig is the BatchProcessor options that give you the ability to add one more more options before the processor is initialized.
@@ -105,7 +110,7 @@ func WithSDKKey(sdkKey string) BPOptionConfig {
105110

106111
// NewBatchEventProcessor returns a new instance of BatchEventProcessor with queueSize and flushInterval
107112
func NewBatchEventProcessor(options ...BPOptionConfig) *BatchEventProcessor {
108-
p := &BatchEventProcessor{}
113+
p := &BatchEventProcessor{processing:semaphore.NewWeighted(int64(maxFlushWorkers))}
109114

110115
for _, opt := range options {
111116
opt(p)
@@ -123,6 +128,11 @@ func NewBatchEventProcessor(options ...BPOptionConfig) *BatchEventProcessor {
123128
p.BatchSize = DefaultBatchSize
124129
}
125130

131+
if p.BatchSize > p.MaxQueueSize {
132+
pLogger.Warning("Batch size is larger than queue size. Swapping")
133+
p.BatchSize, p.MaxQueueSize = p.MaxQueueSize, p.BatchSize
134+
}
135+
126136
if p.Q == nil {
127137
p.Q = NewInMemoryQueue(p.MaxQueueSize)
128138
}
@@ -142,13 +152,28 @@ func (p *BatchEventProcessor) Start(exeCtx utils.ExecutionCtx) {
142152

143153
// ProcessEvent processes the given impression event
144154
func (p *BatchEventProcessor) ProcessEvent(event UserEvent) {
145-
p.Q.Add(event)
146155

147156
if p.Q.Size() >= p.MaxQueueSize {
157+
pLogger.Warning("MaxQueueSize has been met. Discarding event")
158+
return
159+
}
160+
161+
p.Q.Add(event)
162+
163+
if p.Q.Size() < p.BatchSize {
164+
return
165+
}
166+
167+
if p.processing.TryAcquire(1) {
168+
// it doesn't matter if the timer has kicked in here.
169+
// we just want to start one go routine when the batch size is met.
170+
pLogger.Debug("batch size reached. Flushing routine being called")
148171
go func() {
149172
p.FlushEvents()
173+
p.processing.Release(1)
150174
}()
151175
}
176+
152177
}
153178

154179
// EventsCount returns size of an event queue
@@ -171,7 +196,7 @@ func (p *BatchEventProcessor) startTicker(exeCtx utils.ExecutionCtx) {
171196
if p.Ticker != nil {
172197
return
173198
}
174-
p.Ticker = time.NewTicker(p.FlushInterval * time.Millisecond)
199+
p.Ticker = time.NewTicker(p.FlushInterval)
175200
wg := exeCtx.GetWaitSync()
176201
wg.Add(1)
177202
go func() {
@@ -214,7 +239,9 @@ func (p *BatchEventProcessor) addToBatch(current *Batch, visitor Visitor) {
214239
func (p *BatchEventProcessor) FlushEvents() {
215240
// we flush when queue size is reached.
216241
// however, if there is a ticker cycle already processing, we should wait
217-
p.Mux.Lock()
242+
p.flushLock.Lock()
243+
defer p.flushLock.Unlock()
244+
218245
var batchEvent Batch
219246
var batchEventCount = 0
220247
var failedToSend = false
@@ -272,7 +299,6 @@ func (p *BatchEventProcessor) FlushEvents() {
272299
}
273300
}
274301
}
275-
p.Mux.Unlock()
276302
}
277303

278304
// OnEventDispatch registers a handler for LogEvent notifications

pkg/event/processor_test.go

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ type MockDispatcher struct {
3232
Events Queue
3333
}
3434

35-
func (f *MockDispatcher) DispatchEvent(event LogEvent) (bool, error) {
36-
if f.ShouldFail {
35+
func (m *MockDispatcher) DispatchEvent(event LogEvent) (bool, error) {
36+
if m.ShouldFail {
3737
return false, errors.New("Failed to dispatch")
3838
}
3939

40-
f.Events.Add(event)
40+
m.Events.Add(event)
4141
return true, nil
4242
}
4343

@@ -116,10 +116,44 @@ func TestDefaultEventProcessor_LogEventNotification(t *testing.T) {
116116
assert.Nil(t, err)
117117
}
118118

119+
func TestDefaultEventProcessor_DefaultConfig(t *testing.T) {
120+
exeCtx := utils.NewCancelableExecutionCtx()
121+
processor := NewBatchEventProcessor(WithEventDispatcher(NewMockDispatcher(100, false)))
122+
processor.Start(exeCtx)
123+
124+
impression := BuildTestImpressionEvent()
125+
conversion := BuildTestConversionEvent()
126+
127+
processor.ProcessEvent(impression)
128+
processor.ProcessEvent(impression)
129+
processor.ProcessEvent(conversion)
130+
processor.ProcessEvent(conversion)
131+
132+
assert.Equal(t, 4, processor.EventsCount())
133+
134+
time.Sleep(31 * time.Second)
135+
136+
assert.NotNil(t, processor.Ticker)
137+
138+
assert.Equal(t, 0, processor.EventsCount())
139+
140+
result, ok := (processor.EventDispatcher).(*MockDispatcher)
141+
142+
if ok {
143+
assert.Equal(t, 1, result.Events.Size())
144+
evs := result.Events.Get(1)
145+
logEvent, _ := evs[0].(LogEvent)
146+
assert.Equal(t, 4, len(logEvent.Event.Visitors))
147+
}
148+
}
149+
119150
func TestDefaultEventProcessor_ProcessBatch(t *testing.T) {
120151
exeCtx := utils.NewCancelableExecutionCtx()
121-
processor := NewBatchEventProcessor(WithFlushInterval(100), WithQueueSize(100),
122-
WithQueue(NewInMemoryQueue(100)), WithEventDispatcher(NewMockDispatcher(100, false)))
152+
processor := NewBatchEventProcessor(
153+
WithFlushInterval(1 * time.Second),
154+
WithQueueSize(100),
155+
WithQueue(NewInMemoryQueue(100)),
156+
WithEventDispatcher(NewMockDispatcher(100, false)))
123157
processor.Start(exeCtx)
124158

125159
impression := BuildTestImpressionEvent()
@@ -132,7 +166,7 @@ func TestDefaultEventProcessor_ProcessBatch(t *testing.T) {
132166

133167
assert.Equal(t, 4, processor.EventsCount())
134168

135-
exeCtx.TerminateAndWait()
169+
time.Sleep(1500 * time.Millisecond)
136170

137171
assert.NotNil(t, processor.Ticker)
138172

@@ -148,10 +182,13 @@ func TestDefaultEventProcessor_ProcessBatch(t *testing.T) {
148182
}
149183
}
150184

151-
func TestDefaultEventProcessor_QSizeMet(t *testing.T) {
185+
func TestDefaultEventProcessor_BatchSizeMet(t *testing.T) {
152186
exeCtx := utils.NewCancelableExecutionCtx()
153-
processor := NewBatchEventProcessor(WithQueueSize(2), WithFlushInterval(1000),
154-
WithQueue(NewInMemoryQueue(2)), WithEventDispatcher(NewMockDispatcher(100, false)))
187+
processor := NewBatchEventProcessor(
188+
WithBatchSize(2),
189+
WithFlushInterval(1000 * time.Millisecond),
190+
WithQueue(NewInMemoryQueue(2)),
191+
WithEventDispatcher(NewMockDispatcher(100, false)))
155192
processor.Start(exeCtx)
156193

157194
impression := BuildTestImpressionEvent()
@@ -190,10 +227,49 @@ func TestDefaultEventProcessor_QSizeMet(t *testing.T) {
190227
}
191228
}
192229

230+
func TestDefaultEventProcessor_BatchSizeLessThanQSize(t *testing.T) {
231+
processor := NewBatchEventProcessor(
232+
WithQueueSize(2),
233+
WithFlushInterval(1000 * time.Millisecond),
234+
WithQueue(NewInMemoryQueue(100)),
235+
WithEventDispatcher(NewMockDispatcher(100, false)))
236+
237+
assert.Equal(t, 2, processor.BatchSize)
238+
assert.Equal(t, 10, processor.MaxQueueSize)
239+
240+
}
241+
242+
func TestDefaultEventProcessor_QSizeExceeded(t *testing.T) {
243+
exeCtx := utils.NewCancelableExecutionCtx()
244+
processor := NewBatchEventProcessor(
245+
WithQueueSize(2),
246+
WithBatchSize(2),
247+
WithFlushInterval(1000 * time.Millisecond),
248+
WithQueue(NewInMemoryQueue(2)),
249+
WithEventDispatcher(NewMockDispatcher(100, true)))
250+
processor.Start(exeCtx)
251+
252+
impression := BuildTestImpressionEvent()
253+
254+
processor.ProcessEvent(impression)
255+
processor.ProcessEvent(impression)
256+
257+
assert.Equal(t, 2, processor.EventsCount())
258+
259+
processor.ProcessEvent(impression)
260+
processor.ProcessEvent(impression)
261+
262+
assert.Equal(t, 2, processor.EventsCount())
263+
264+
}
265+
193266
func TestDefaultEventProcessor_FailedDispatch(t *testing.T) {
194267
exeCtx := utils.NewCancelableExecutionCtx()
195-
processor := NewBatchEventProcessor(WithQueueSize(100), WithFlushInterval(100),
196-
WithQueue(NewInMemoryQueue(100)), WithEventDispatcher(&MockDispatcher{ShouldFail: true, Events: NewInMemoryQueue(100)}))
268+
processor := NewBatchEventProcessor(
269+
WithQueueSize(100),
270+
WithFlushInterval(100),
271+
WithQueue(NewInMemoryQueue(100)),
272+
WithEventDispatcher(&MockDispatcher{ShouldFail: true, Events: NewInMemoryQueue(100)}))
197273
processor.Start(exeCtx)
198274

199275
impression := BuildTestImpressionEvent()
@@ -221,7 +297,9 @@ func TestDefaultEventProcessor_FailedDispatch(t *testing.T) {
221297

222298
func TestBatchEventProcessor_FlushesOnClose(t *testing.T) {
223299
exeCtx := utils.NewCancelableExecutionCtx()
224-
processor := NewBatchEventProcessor(WithQueueSize(100), WithQueue(NewInMemoryQueue(100)),
300+
processor := NewBatchEventProcessor(
301+
WithQueueSize(100),
302+
WithQueue(NewInMemoryQueue(100)),
225303
WithEventDispatcher(NewMockDispatcher(100, false)))
226304
processor.Start(exeCtx)
227305

0 commit comments

Comments
 (0)