Skip to content

Commit 329821f

Browse files
authored
add option to stop mailbox after receiving all elements from queue (#60)
1 parent 1f1cbcc commit 329821f

File tree

8 files changed

+140
-39
lines changed

8 files changed

+140
-39
lines changed

.golangci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ linters-settings:
102102
- github.com/vladopajic/go-actor
103103
forbidigo:
104104
forbid:
105-
- 'time.Sleep*(# Do not sleep)?'
105+
- 'time\.Sleep*(# Do not sleep)?'
106106
- 'panic*(# Do not panic)?'
107-
- 'os.Exit*(# Do not exit)?'
107+
- 'os\.Exit*(# Do not exit)?'
108+
- p: ^fmt\.Print*$
109+
msg: Do not commit print statements.
108110
gomoddirectives:
109111
retract-allow-no-explanation: true
110112
maintidx:

actor/actor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ func (w *worker) DoWork(c Context) WorkerStatus {
8282
func New(w Worker, opt ...Option) Actor {
8383
return &actor{
8484
worker: w,
85-
options: newOptions(opt),
85+
options: newOptions(opt).Actor,
8686
}
8787
}
8888

8989
type actor struct {
9090
worker Worker
91-
options options
91+
options optionsActor
9292
ctx *context
9393
workEndedSigC chan struct{}
9494
workerRunning bool
@@ -152,7 +152,7 @@ func (a *actor) onStart() {
152152
w.OnStart(a.ctx)
153153
}
154154

155-
if fn := a.options.Actor.OnStartFunc; fn != nil {
155+
if fn := a.options.OnStartFunc; fn != nil {
156156
fn(a.ctx)
157157
}
158158
}
@@ -162,7 +162,7 @@ func (a *actor) onStop() {
162162
w.OnStop()
163163
}
164164

165-
if fn := a.options.Actor.OnStopFunc; fn != nil {
165+
if fn := a.options.OnStopFunc; fn != nil {
166166
fn()
167167
}
168168
}

actor/combine.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ func wrapActors(
114114
onStopFunc func(),
115115
) []Actor {
116116
wrapActorStruct := func(a *actor) *actor {
117-
prevOnStopFunc := a.options.Actor.OnStopFunc
117+
prevOnStopFunc := a.options.OnStopFunc
118118

119-
a.options.Actor.OnStopFunc = func() {
119+
a.options.OnStopFunc = func() {
120120
if prevOnStopFunc != nil {
121121
prevOnStopFunc()
122122
}

actor/export_test.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ const (
44
MinQueueCapacity = minQueueCapacity
55
)
66

7-
type ActorImpl = actor
7+
type (
8+
ActorImpl = actor
9+
OptionsMailbox = optionsMailbox
10+
)
811

912
func NewActorImpl(w Worker, opt ...Option) *ActorImpl {
1013
a := New(w, opt...)
@@ -38,9 +41,13 @@ func NewZeroOptions() options {
3841
func NewMailboxWorker[T any](
3942
sendC,
4043
receiveC chan T,
41-
queue *queue[T],
44+
mOpts optionsMailbox,
4245
) *mailboxWorker[T] {
43-
return newMailboxWorker(sendC, receiveC, queue)
46+
return newMailboxWorker(sendC, receiveC, mOpts)
47+
}
48+
49+
func (w *mailboxWorker[T]) Queue() *queue[T] {
50+
return w.queue
4451
}
4552

4653
func NewQueue[T any](capacity, minimum int) *queue[T] {

actor/mailbox.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ func NewMailboxes[T any](count int, opt ...MailboxOption) []Mailbox[T] {
6060
// will never block, all messages are going to be queued and Actors on
6161
// receiving end of the Mailbox will get all messages in FIFO order.
6262
func NewMailbox[T any](opt ...MailboxOption) Mailbox[T] {
63-
mOpts := newOptions(opt).Mailbox
63+
options := newOptions(opt).Mailbox
6464

65-
if mOpts.AsChan {
66-
c := make(chan T, mOpts.Capacity)
65+
if options.AsChan {
66+
c := make(chan T, options.Capacity)
6767

6868
return &mailbox[T]{
6969
Actor: Idle(OptOnStop(func() { close(c) })),
@@ -75,8 +75,7 @@ func NewMailbox[T any](opt ...MailboxOption) Mailbox[T] {
7575
var (
7676
sendC = make(chan T)
7777
receiveC = make(chan T)
78-
queue = newQueue[T](mOpts.Capacity, mOpts.MinCapacity)
79-
w = newMailboxWorker(sendC, receiveC, queue)
78+
w = newMailboxWorker(sendC, receiveC, options)
8079
)
8180

8281
return &mailbox[T]{
@@ -109,47 +108,60 @@ type mailboxWorker[T any] struct {
109108
receiveC chan T
110109
sendC chan T
111110
queue *queue[T]
111+
options optionsMailbox
112112
}
113113

114114
func newMailboxWorker[T any](
115115
sendC,
116116
receiveC chan T,
117-
queue *queue[T],
117+
options optionsMailbox,
118118
) *mailboxWorker[T] {
119+
queue := newQueue[T](options.Capacity, options.MinCapacity)
120+
119121
return &mailboxWorker[T]{
120122
sendC: sendC,
121123
receiveC: receiveC,
122124
queue: queue,
125+
options: options,
123126
}
124127
}
125128

126129
func (w *mailboxWorker[T]) DoWork(c Context) WorkerStatus {
127130
if w.queue.IsEmpty() {
128131
select {
132+
case <-c.Done():
133+
return WorkerEnd
134+
129135
case value := <-w.sendC:
130136
w.queue.PushBack(value)
131137
return WorkerContinue
132-
133-
case <-c.Done():
134-
return WorkerEnd
135138
}
136139
}
137140

138141
select {
142+
case <-c.Done():
143+
return WorkerEnd
144+
139145
case w.receiveC <- w.queue.Front():
140146
w.queue.PopFront()
141147
return WorkerContinue
142148

143149
case value := <-w.sendC:
144150
w.queue.PushBack(value)
145151
return WorkerContinue
146-
147-
case <-c.Done():
148-
return WorkerEnd
149152
}
150153
}
151154

152155
func (w *mailboxWorker[T]) OnStop() {
156+
// close sendC to prevent anyone from writing to this mailbox
153157
close(w.sendC)
158+
159+
// close receiveC channel after all data from queue is received
160+
if w.options.StopAfterReceivingAll {
161+
for !w.queue.IsEmpty() {
162+
w.receiveC <- w.queue.PopFront()
163+
}
164+
}
165+
154166
close(w.receiveC)
155167
}

actor/mailbox_test.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ func Test_MailboxWorker_EndSignal(t *testing.T) {
1515

1616
sendC := make(chan any)
1717
receiveC := make(chan any)
18-
q := NewQueue[any](0, 0)
18+
options := OptionsMailbox{}
1919

20-
w := NewMailboxWorker(sendC, receiveC, q)
20+
w := NewMailboxWorker(sendC, receiveC, options)
2121
assert.NotNil(t, w)
2222

2323
// Worker should signal end with empty queue
2424
assert.Equal(t, WorkerEnd, w.DoWork(ContextEnded()))
2525

2626
// Worker should signal end with non-empty queue
27-
q.PushBack(`🌹`)
27+
w.Queue().PushBack(`🌹`)
2828
assert.Equal(t, WorkerEnd, w.DoWork(ContextEnded()))
2929
}
3030

@@ -181,6 +181,65 @@ func Test_MailboxOptAsChan(t *testing.T) {
181181
})
182182
}
183183

184+
// This test asserts that Mailbox will end only after all messages have been received.
185+
func Test_Mailbox_OptEndAferReceivingAll(t *testing.T) {
186+
t.Parallel()
187+
188+
const messagesCount = 1000
189+
190+
sendMessages := func(m Mailbox[any]) {
191+
t.Helper()
192+
193+
for i := 0; i < messagesCount; i++ {
194+
assert.NoError(t, m.Send(ContextStarted(), `🥥`))
195+
}
196+
}
197+
assertGotAllMessages := func(m Mailbox[any]) {
198+
t.Helper()
199+
200+
gotMessages := 0
201+
202+
for msg := range m.ReceiveC() {
203+
assert.Equal(t, `🥥`, msg)
204+
gotMessages++
205+
}
206+
207+
assert.Equal(t, messagesCount, gotMessages)
208+
}
209+
210+
t.Run("the-best-way", func(t *testing.T) {
211+
t.Parallel()
212+
213+
m := NewMailbox[any](OptStopAfterReceivingAll())
214+
m.Start()
215+
sendMessages(m)
216+
217+
// Stop has to be called in goroutine because Stop is blocking until
218+
// actor (mailbox) has fully ended. And current thread of execution is needed
219+
// to read data from mailbox.
220+
go m.Stop()
221+
222+
assertGotAllMessages(m)
223+
})
224+
225+
t.Run("suboptimal-way", func(t *testing.T) {
226+
t.Parallel()
227+
228+
m := NewMailbox[any](OptStopAfterReceivingAll())
229+
m.Start()
230+
sendMessages(m)
231+
232+
// This time we start gorotune which will read all messages from mailbox instead of
233+
// stopping in separate goroutine.
234+
// There are no guaranees that this gorutine will finish after Stop is called, so
235+
// it could be the case that this gorotuine has received all messages from mailbox,
236+
// even before mailbox was stopped. Which wouldn't correctly assert this feature.
237+
go assertGotAllMessages(m)
238+
239+
m.Stop()
240+
})
241+
}
242+
184243
func assertSendReceive(t *testing.T, m Mailbox[any], val any) {
185244
t.Helper()
186245

actor/options.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ func OptAsChan() MailboxOption {
5454
}
5555
}
5656

57+
// OptStopAfterReceivingAll will close ReceiveC channel of Mailbox
58+
// after all messages have been received from this channel.
59+
func OptStopAfterReceivingAll() MailboxOption {
60+
return func(o *options) {
61+
o.Mailbox.StopAfterReceivingAll = true
62+
}
63+
}
64+
5765
// OptStopTogether will stop all actors when any of combined
5866
// actors is stopped.
5967
func OptStopTogether() CombinedOption {
@@ -78,21 +86,26 @@ type (
7886
)
7987

8088
type options struct {
81-
Actor struct {
82-
OnStartFunc func(Context)
83-
OnStopFunc func()
84-
}
89+
Actor optionsActor
90+
Combined optionsCombined
91+
Mailbox optionsMailbox
92+
}
8593

86-
Combined struct {
87-
StopTogether bool
88-
OnStopFunc func()
89-
}
94+
type optionsActor struct {
95+
OnStartFunc func(Context)
96+
OnStopFunc func()
97+
}
9098

91-
Mailbox struct {
92-
AsChan bool
93-
Capacity int
94-
MinCapacity int
95-
}
99+
type optionsCombined struct {
100+
StopTogether bool
101+
OnStopFunc func()
102+
}
103+
104+
type optionsMailbox struct {
105+
AsChan bool
106+
Capacity int
107+
MinCapacity int
108+
StopAfterReceivingAll bool
96109
}
97110

98111
func newOptions[T ~func(o *options)](opts []T) options {

actor/options_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ func testMailboxOptions(t *testing.T) {
9696
assert.Empty(t, opts.Actor)
9797
assert.Empty(t, opts.Combined)
9898
}
99+
100+
{ // Assert that OptStopAfterReceivingAll will be set
101+
opts := NewOptions(OptStopAfterReceivingAll())
102+
assert.True(t, opts.Mailbox.StopAfterReceivingAll)
103+
104+
assert.Empty(t, opts.Actor)
105+
assert.Empty(t, opts.Combined)
106+
}
99107
}
100108

101109
func testCombinedOptions(t *testing.T) {

0 commit comments

Comments
 (0)