@@ -3,13 +3,71 @@ package storage
33import (
44 "fmt"
55 "log"
6+ "time"
67
78 r "gopkg.in/redis.v3"
89)
910
11+ type message struct {
12+ app string
13+ messageBody string
14+ }
15+
16+ func newMessage (app string , messageBody string ) * message {
17+ return & message {
18+ app : app ,
19+ messageBody : messageBody ,
20+ }
21+ }
22+
23+ type messagePipeliner struct {
24+ bufferSize int
25+ messageCount int
26+ pipeline * r.Pipeline
27+ timeoutTicker * time.Ticker
28+ queuedApps map [string ]bool
29+ errCh chan error
30+ }
31+
32+ func newMessagePipeliner (bufferSize int , redisClient * r.Client , errCh chan error ) * messagePipeliner {
33+ return & messagePipeliner {
34+ bufferSize : bufferSize ,
35+ pipeline : redisClient .Pipeline (),
36+ timeoutTicker : time .NewTicker (time .Second ),
37+ queuedApps : map [string ]bool {},
38+ errCh : errCh ,
39+ }
40+ }
41+
42+ func (mp * messagePipeliner ) addMessage (message * message ) {
43+ if err := mp .pipeline .RPush (message .app , message .messageBody ).Err (); err == nil {
44+ mp .queuedApps [message .app ] = true
45+ mp .messageCount ++
46+ } else {
47+ mp .errCh <- fmt .Errorf ("Error adding rpush to %s to the pipeline: %s" , message .app , err )
48+ }
49+ }
50+
51+ func (mp messagePipeliner ) execPipeline () {
52+ for app := range mp .queuedApps {
53+ if err := mp .pipeline .LTrim (app , int64 (- 1 * mp .bufferSize ), - 1 ).Err (); err != nil {
54+ mp .errCh <- fmt .Errorf ("Error adding ltrim of %s to the pipeline: %s" , app , err )
55+ }
56+ }
57+ go func () {
58+ defer mp .pipeline .Close ()
59+ if _ , err := mp .pipeline .Exec (); err != nil {
60+ mp .errCh <- fmt .Errorf ("Error executing pipeline: %s" , err )
61+ }
62+ }()
63+ }
64+
1065type redisAdapter struct {
11- bufferSize int
12- redisClient * r.Client
66+ started bool
67+ bufferSize int
68+ redisClient * r.Client
69+ messageChannel chan * message
70+ stopCh chan struct {}
1371}
1472
1573// NewRedisStorageAdapter returns a pointer to a new instance of a redis-based storage.Adapter.
@@ -24,35 +82,59 @@ func NewRedisStorageAdapter(bufferSize int) (*redisAdapter, error) {
2482 if err != nil {
2583 return nil , err
2684 }
27- return & redisAdapter {
85+ rsa := & redisAdapter {
2886 bufferSize : bufferSize ,
2987 redisClient : r .NewClient (& r.Options {
3088 Addr : fmt .Sprintf ("%s:%d" , cfg .RedisHost , cfg .RedisPort ),
3189 Password : cfg .RedisPassword , // "" == no password
3290 DB : int64 (cfg .RedisDB ),
3391 }),
34- }, nil
92+ messageChannel : make (chan * message ),
93+ stopCh : make (chan struct {}),
94+ }
95+ return rsa , nil
3596}
3697
37- // Write adds a log message to to an app-specific list in redis using ring-buffer-like semantics
38- func (a * redisAdapter ) Write (app string , message string ) error {
39- // Note: Deliberately NOT using MULTI / transactions here since in this implementation of the
40- // redis client, MULTI is not safe for concurrent use by multiple goroutines. It's been advised
41- // by the authors of the gopkg.in/redis.v3 package to just use pipelining when possible...
42- // and here that is technically possible. In the WORST case scenario, not having transactions
43- // means we may momentarily have more than the desired number of log entries in the list /
44- // buffer, but an LTRIM will eventually correct that, bringing the list / buffer back down to
45- // its desired max size.
46- pipeline := a .redisClient .Pipeline ()
47- if err := pipeline .RPush (app , message ).Err (); err != nil {
48- return err
49- }
50- if err := pipeline .LTrim (app , int64 (- 1 * a .bufferSize ), - 1 ).Err (); err != nil {
51- return err
52- }
53- if _ , err := pipeline .Exec (); err != nil {
54- return err
98+ // Start the storage adapter. Invocations of this function are not concurrency safe and multiple
99+ // serialized invocations have no effect.
100+ func (a * redisAdapter ) Start () {
101+ if ! a .started {
102+ a .started = true
103+ errCh := make (chan error )
104+ mp := newMessagePipeliner (a .bufferSize , a .redisClient , errCh )
105+ go func () {
106+ for {
107+ select {
108+ case err := <- errCh :
109+ log .Println (err )
110+ case <- a .stopCh :
111+ return
112+ }
113+ }
114+ }()
115+ go func () {
116+ for {
117+ select {
118+ case message := <- a .messageChannel :
119+ mp .addMessage (message )
120+ if mp .messageCount == 50 {
121+ mp .execPipeline ()
122+ mp = newMessagePipeliner (a .bufferSize , a .redisClient , errCh )
123+ }
124+ case <- mp .timeoutTicker .C :
125+ mp .execPipeline ()
126+ mp = newMessagePipeliner (a .bufferSize , a .redisClient , errCh )
127+ case <- a .stopCh :
128+ return
129+ }
130+ }
131+ }()
55132 }
133+ }
134+
135+ // Write adds a log message to to an app-specific list in redis using ring-buffer-like semantics
136+ func (a * redisAdapter ) Write (app string , messageBody string ) error {
137+ a .messageChannel <- newMessage (app , messageBody )
56138 return nil
57139}
58140
@@ -77,7 +159,12 @@ func (a *redisAdapter) Destroy(app string) error {
77159 return nil
78160}
79161
162+ // Reopen the storage adapter-- in the case of this implementation, a no-op
80163func (a * redisAdapter ) Reopen () error {
81- // No-op
82164 return nil
83165}
166+
167+ // Stop the storage adapter. Additional writes may not be performed after stopping.
168+ func (a * redisAdapter ) Stop () {
169+ close (a .stopCh )
170+ }
0 commit comments