Skip to content

Commit 1a375f8

Browse files
authored
Merge pull request #31 from saiya/feature/multiplexer-long-polling-early-return
Storage multiplexer improvements
2 parents a6521c6 + 81799a7 commit 1a375f8

File tree

5 files changed

+280
-51
lines changed

5 files changed

+280
-51
lines changed

server/domain/mock/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.go

server/domain/storage.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func IsStorageNonFatalError(err error) bool {
2424
return errors.Is(err, ErrInvalidChannel) || errors.Is(err, ErrSubscriptionNotFound) || errors.Is(err, ErrMalformedAckHandle)
2525
}
2626

27+
//go:generate mockgen -source=${GOFILE} -package=mock -destination=./mock/${GOFILE}
28+
2729
// Storage interface is an abstraction layer of storage implementations
2830
type Storage interface {
2931
String() string // Stringer

server/storage/multiplex/pubsub_messaging.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package multiplex
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"sync"
8+
"time"
79

810
"github.com/saiya/dsps/server/domain"
911
"github.com/saiya/dsps/server/logger"
1012
)
1113

14+
const parallelFetchEarlyReturnWindow = 300 * time.Millisecond
15+
1216
func (s *storageMultiplexer) PublishMessages(ctx context.Context, msgs []domain.Message) error {
1317
_, err := s.parallelAtLeastOneSuccess(ctx, "PublishMessages", func(ctx context.Context, _ domain.StorageID, child domain.Storage) (interface{}, error) {
1418
if child := child.AsPubSubStorage(); child != nil {
@@ -25,20 +29,41 @@ func (s *storageMultiplexer) FetchMessages(ctx context.Context, sl domain.Subscr
2529
moreMessages bool
2630
ackHandle domain.AckHandle
2731
}
28-
results, err := s.parallelAtLeastOneSuccess(ctx, "FetchMessages", func(ctx context.Context, _ domain.StorageID, child domain.Storage) (interface{}, error) {
32+
parallelCtx, parallelCtxCancel := context.WithCancel(ctx)
33+
defer parallelCtxCancel()
34+
subscriptionMissingCh := make(chan domain.StorageID, len(s.children))
35+
results, err := s.parallelAtLeastOneSuccess(parallelCtx, "FetchMessages", func(ctx context.Context, storageID domain.StorageID, child domain.Storage) (interface{}, error) {
2936
if child := child.AsPubSubStorage(); child != nil {
3037
msgs, moreMsgs, ackHandle, err := child.FetchMessages(ctx, sl, max, waituntil)
3138
if err != nil {
39+
if errors.Is(err, domain.ErrSubscriptionNotFound) || errors.Is(err, domain.ErrInvalidChannel) {
40+
subscriptionMissingCh <- storageID
41+
}
3242
return nil, err
3343
}
44+
if len(msgs) > 0 {
45+
// If one or more storage returns messages, multiplexer should immediately return them even if other storages still polling.
46+
time.AfterFunc(parallelFetchEarlyReturnWindow, parallelCtxCancel)
47+
}
3448
return fetchResult{msgs: msgs, moreMessages: moreMsgs, ackHandle: ackHandle}, nil
3549
}
3650
return nil, errMultiplexSkipped
3751
})
52+
close(subscriptionMissingCh)
3853
if err != nil {
3954
return nil, false, domain.AckHandle{}, err
4055
}
4156

57+
for id := range subscriptionMissingCh {
58+
// Subscriber missing on this storage.
59+
// This situation could occur if the storage had been temporary unavailable when subscriber created.
60+
// So that automatically create subscriber to receive future messages.
61+
logger.Of(ctx).Debugf(`Auto-creating (recovering) subscriber %v on storage '%s' because fetch succeeded in the multiplexer but this storage reported the subscriber does not exist.`, sl, id)
62+
if err := s.children[id].AsPubSubStorage().NewSubscriber(ctx, sl); err != nil {
63+
logger.Of(ctx).WarnError(fmt.Sprintf("Failed to auto-create (recover) subscriber %v on storage '%s': %%w", sl, id), err)
64+
}
65+
}
66+
4267
// Note that this merge logic honors message ordering as possible.
4368
// Only exception is that storages returns messages by different ordering, possible cause of such case is that client retry to publish messages.
4469
// If client retried publish, no need to guarantee ordering of the messages sent concurrently with the retry.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package multiplex_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/saiya/dsps/server/config"
9+
"github.com/saiya/dsps/server/domain"
10+
. "github.com/saiya/dsps/server/storage/multiplex"
11+
"github.com/saiya/dsps/server/storage/onmemory"
12+
. "github.com/saiya/dsps/server/storage/testing"
13+
)
14+
15+
var onmemoryMultiplexCtor = func(onmemConfigs ...config.OnmemoryStorageConfig) StorageCtor {
16+
return func(ctx context.Context, systemClock domain.SystemClock, channelProvider domain.ChannelProvider) (domain.Storage, error) {
17+
storages := map[domain.StorageID]domain.Storage{}
18+
for i := range onmemConfigs {
19+
storage, err := onmemory.NewOnmemoryStorage(context.Background(), &(onmemConfigs[i]), systemClock, channelProvider)
20+
if err != nil {
21+
return nil, err
22+
}
23+
storages[domain.StorageID(fmt.Sprintf("storage%d", i+1))] = storage
24+
}
25+
return NewStorageMultiplexer(storages)
26+
}
27+
}
28+
29+
func TestCoreFunction(t *testing.T) {
30+
CoreFunctionTest(t, onmemoryMultiplexCtor(
31+
config.OnmemoryStorageConfig{
32+
DisablePubSub: true,
33+
DisableJwt: true,
34+
},
35+
config.OnmemoryStorageConfig{
36+
DisablePubSub: true,
37+
DisableJwt: true,
38+
},
39+
))
40+
}
41+
42+
func TestPubSub(t *testing.T) {
43+
PubSubTest(t, onmemoryMultiplexCtor(
44+
config.OnmemoryStorageConfig{
45+
DisableJwt: true,
46+
},
47+
config.OnmemoryStorageConfig{
48+
DisablePubSub: true, // Storage without feature support
49+
DisableJwt: true,
50+
},
51+
config.OnmemoryStorageConfig{
52+
DisableJwt: true,
53+
},
54+
))
55+
}
56+
57+
func TestJwt(t *testing.T) {
58+
JwtTest(t, onmemoryMultiplexCtor(
59+
config.OnmemoryStorageConfig{
60+
DisablePubSub: true,
61+
},
62+
config.OnmemoryStorageConfig{
63+
DisablePubSub: true,
64+
DisableJwt: true, // Storage without feature support
65+
},
66+
config.OnmemoryStorageConfig{
67+
DisablePubSub: true,
68+
},
69+
))
70+
}

0 commit comments

Comments
 (0)