fairway 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +3 -1
- data/Rakefile +21 -0
- data/go/all_specs_test.go +35 -0
- data/go/channeled_connection.go +23 -0
- data/go/channeled_connection_test.go +41 -0
- data/go/config.go +50 -0
- data/go/config_test.go +53 -0
- data/go/connection.go +54 -0
- data/go/connection_test.go +125 -0
- data/go/fairway_deliver.go +67 -0
- data/go/fairway_destroy.go +36 -0
- data/go/fairway_peek.go +22 -0
- data/go/fairway_priority.go +36 -0
- data/go/fairway_pull.go +99 -0
- data/go/message.go +38 -0
- data/go/message_test.go +34 -0
- data/go/queue.go +14 -0
- data/go/queue_test.go +85 -0
- data/go/scripts.go +85 -0
- data/lib/fairway/config.rb +1 -1
- data/lib/fairway/sidekiq/basic_fetch.rb +0 -1
- data/lib/fairway/version.rb +1 -1
- metadata +19 -2
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fairway (0.1.
|
4
|
+
fairway (0.1.4)
|
5
5
|
activesupport
|
6
6
|
redis
|
7
7
|
redis-namespace (>= 1.3.0)
|
@@ -27,6 +27,7 @@ GEM
|
|
27
27
|
facter (1.6.17)
|
28
28
|
i18n (0.6.1)
|
29
29
|
multi_json (1.5.0)
|
30
|
+
rake (10.1.0)
|
30
31
|
redis (3.0.4)
|
31
32
|
redis-namespace (1.3.0)
|
32
33
|
redis (~> 3.0.0)
|
@@ -52,5 +53,6 @@ PLATFORMS
|
|
52
53
|
DEPENDENCIES
|
53
54
|
debugger
|
54
55
|
fairway!
|
56
|
+
rake
|
55
57
|
rspec
|
56
58
|
sidekiq
|
data/Rakefile
CHANGED
@@ -1 +1,22 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
require "active_support/core_ext"
|
3
|
+
|
4
|
+
task "sync:go" do
|
5
|
+
Dir.glob("redis/*") do |src|
|
6
|
+
script = File.read(src)
|
7
|
+
|
8
|
+
name = /redis\/(.*)\.lua/.match(src)[1]
|
9
|
+
|
10
|
+
File.open("go/#{name}.go", "w") do |dest|
|
11
|
+
dest.puts <<-EOF
|
12
|
+
package fairway
|
13
|
+
|
14
|
+
func #{name.camelize}() string {
|
15
|
+
return `
|
16
|
+
#{script}
|
17
|
+
`
|
18
|
+
}
|
19
|
+
EOF
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/customerio/gospec"
|
5
|
+
"github.com/garyburd/redigo/redis"
|
6
|
+
"testing"
|
7
|
+
)
|
8
|
+
|
9
|
+
// You will need to list every spec in a TestXxx method like this,
|
10
|
+
// so that gotest can be used to run the specs. Later GoSpec might
|
11
|
+
// get its own command line tool similar to gotest, but for now this
|
12
|
+
// is the way to go. This shouldn't require too much typing, because
|
13
|
+
// there will be typically only one top-level spec per class/feature.
|
14
|
+
|
15
|
+
func TestAllSpecs(t *testing.T) {
|
16
|
+
r := gospec.NewRunner()
|
17
|
+
|
18
|
+
r.Parallel = false
|
19
|
+
|
20
|
+
r.BeforeEach = func() {
|
21
|
+
// Load test instance of redis on port 6400
|
22
|
+
conn, _ := redis.Dial("tcp", "localhost:6400")
|
23
|
+
conn.Do("flushdb")
|
24
|
+
}
|
25
|
+
|
26
|
+
// List all specs here
|
27
|
+
r.AddSpec(ConfigSpec)
|
28
|
+
r.AddSpec(ConnectionSpec)
|
29
|
+
r.AddSpec(ChanneledConnectionSpec)
|
30
|
+
r.AddSpec(MsgSpec)
|
31
|
+
r.AddSpec(QueueSpec)
|
32
|
+
|
33
|
+
// Run GoSpec and report any errors to gotest's `testing.T` instance
|
34
|
+
gospec.MainGoTest(r, t)
|
35
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
type channeledConn struct {
|
4
|
+
*conn
|
5
|
+
channel func(message *Msg) string
|
6
|
+
}
|
7
|
+
|
8
|
+
func (c *channeledConn) Channel(msg *Msg) string {
|
9
|
+
return c.channel(msg)
|
10
|
+
}
|
11
|
+
|
12
|
+
func (c *channeledConn) Deliver(msg *Msg) error {
|
13
|
+
channel := c.Channel(msg)
|
14
|
+
facet := c.config.Facet(msg)
|
15
|
+
return c.scripts.deliver(channel, facet, msg)
|
16
|
+
}
|
17
|
+
|
18
|
+
func NewChanneledConnection(config *Config, channelFunc func(message *Msg) string) Connection {
|
19
|
+
return &channeledConn{
|
20
|
+
NewConnection(config).(*conn),
|
21
|
+
channelFunc,
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"fmt"
|
5
|
+
"github.com/customerio/gospec"
|
6
|
+
. "github.com/customerio/gospec"
|
7
|
+
"github.com/garyburd/redigo/redis"
|
8
|
+
)
|
9
|
+
|
10
|
+
func ChanneledConnectionSpec(c gospec.Context) {
|
11
|
+
// Load test instance of redis on port 6400
|
12
|
+
config := NewConfig("localhost:6400", 2)
|
13
|
+
config.AddQueue("myqueue", "typea")
|
14
|
+
config.AddQueue("myqueue2", "typeb")
|
15
|
+
|
16
|
+
conn := NewChanneledConnection(config, func(message *Msg) string {
|
17
|
+
channel, _ := message.Get("type").String()
|
18
|
+
return fmt.Sprint("channel:type", channel, ":channel")
|
19
|
+
})
|
20
|
+
|
21
|
+
c.Specify("Deliver", func() {
|
22
|
+
c.Specify("only queues up message for matching queues", func() {
|
23
|
+
r := config.redisPool.Get()
|
24
|
+
defer r.Close()
|
25
|
+
|
26
|
+
count, _ := redis.Int(r.Do("llen", "fairway:myqueue:default"))
|
27
|
+
c.Expect(count, Equals, 0)
|
28
|
+
count, _ = redis.Int(r.Do("llen", "fairway:myqueue2:default"))
|
29
|
+
c.Expect(count, Equals, 0)
|
30
|
+
|
31
|
+
msg, _ := NewMsg(map[string]string{"type": "a"})
|
32
|
+
|
33
|
+
conn.Deliver(msg)
|
34
|
+
|
35
|
+
count, _ = redis.Int(r.Do("llen", "fairway:myqueue:default"))
|
36
|
+
c.Expect(count, Equals, 1)
|
37
|
+
count, _ = redis.Int(r.Do("llen", "fairway:myqueue2:default"))
|
38
|
+
c.Expect(count, Equals, 0)
|
39
|
+
})
|
40
|
+
})
|
41
|
+
}
|
data/go/config.go
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/garyburd/redigo/redis"
|
5
|
+
"time"
|
6
|
+
)
|
7
|
+
|
8
|
+
type QueueDefinition struct {
|
9
|
+
name string
|
10
|
+
channel string
|
11
|
+
}
|
12
|
+
|
13
|
+
type Config struct {
|
14
|
+
Namespace string
|
15
|
+
Facet func(message *Msg) string
|
16
|
+
queues []*QueueDefinition
|
17
|
+
redisPool *redis.Pool
|
18
|
+
}
|
19
|
+
|
20
|
+
func (c *Config) AddQueue(name, channel string) {
|
21
|
+
c.queues = append(c.queues, &QueueDefinition{name, channel})
|
22
|
+
}
|
23
|
+
|
24
|
+
func (c *Config) scripts() *scripts {
|
25
|
+
return newScripts(c)
|
26
|
+
}
|
27
|
+
|
28
|
+
func NewConfig(server string, poolSize int) *Config {
|
29
|
+
return &Config{
|
30
|
+
"fairway",
|
31
|
+
func(message *Msg) string { return "default" },
|
32
|
+
[]*QueueDefinition{},
|
33
|
+
&redis.Pool{
|
34
|
+
MaxIdle: poolSize,
|
35
|
+
MaxActive: poolSize,
|
36
|
+
IdleTimeout: 240 * time.Second,
|
37
|
+
Dial: func() (redis.Conn, error) {
|
38
|
+
c, err := redis.Dial("tcp", server)
|
39
|
+
if err != nil {
|
40
|
+
return nil, err
|
41
|
+
}
|
42
|
+
return c, err
|
43
|
+
},
|
44
|
+
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
45
|
+
_, err := c.Do("PING")
|
46
|
+
return err
|
47
|
+
},
|
48
|
+
},
|
49
|
+
}
|
50
|
+
}
|
data/go/config_test.go
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/customerio/gospec"
|
5
|
+
. "github.com/customerio/gospec"
|
6
|
+
)
|
7
|
+
|
8
|
+
func ConfigSpec(c gospec.Context) {
|
9
|
+
// Load test instance of redis on port 6400
|
10
|
+
config := NewConfig("localhost:6400", 10)
|
11
|
+
|
12
|
+
c.Specify("NewConfig", func() {
|
13
|
+
c.Specify("namespace is fairway", func() {
|
14
|
+
c.Expect(config.Namespace, Equals, "fairway")
|
15
|
+
})
|
16
|
+
|
17
|
+
c.Specify("sets the facet to always return 'default'", func() {
|
18
|
+
msg, _ := NewMsg(make([]string, 0))
|
19
|
+
c.Expect(config.Facet(msg), Equals, "default")
|
20
|
+
})
|
21
|
+
|
22
|
+
c.Specify("doesn't have any defined queues", func() {
|
23
|
+
c.Expect(len(config.queues), Equals, 0)
|
24
|
+
})
|
25
|
+
})
|
26
|
+
|
27
|
+
c.Specify("sets redis pool size", func() {
|
28
|
+
c.Expect(config.redisPool.MaxIdle, Equals, 10)
|
29
|
+
c.Expect(config.redisPool.MaxActive, Equals, 10)
|
30
|
+
config = NewConfig("localhost:6400", 20)
|
31
|
+
c.Expect(config.redisPool.MaxIdle, Equals, 20)
|
32
|
+
c.Expect(config.redisPool.MaxActive, Equals, 20)
|
33
|
+
})
|
34
|
+
|
35
|
+
c.Specify("can specify custom namespace", func() {
|
36
|
+
config.Namespace = "mynamespace"
|
37
|
+
c.Expect(config.Namespace, Equals, "mynamespace")
|
38
|
+
})
|
39
|
+
|
40
|
+
c.Specify("can specify custom facet", func() {
|
41
|
+
config.Facet = func(message *Msg) string {
|
42
|
+
return "myfacet"
|
43
|
+
}
|
44
|
+
msg, _ := NewMsg(make([]string, 0))
|
45
|
+
c.Expect(config.Facet(msg), Equals, "myfacet")
|
46
|
+
})
|
47
|
+
|
48
|
+
c.Specify("can define a queue", func() {
|
49
|
+
config.AddQueue("myqueue", "default")
|
50
|
+
c.Expect(len(config.queues), Equals, 1)
|
51
|
+
c.Expect(*config.queues[0], Equals, QueueDefinition{"myqueue", "default"})
|
52
|
+
})
|
53
|
+
}
|
data/go/connection.go
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
type Connection interface {
|
4
|
+
RegisterQueues()
|
5
|
+
Queues() []*Queue
|
6
|
+
Channel(*Msg) string
|
7
|
+
Deliver(*Msg) error
|
8
|
+
Configuration() *Config
|
9
|
+
}
|
10
|
+
|
11
|
+
type conn struct {
|
12
|
+
config *Config
|
13
|
+
scripts *scripts
|
14
|
+
}
|
15
|
+
|
16
|
+
func (c *conn) RegisterQueues() {
|
17
|
+
for _, definition := range c.config.queues {
|
18
|
+
c.scripts.registerQueue(definition)
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
func (c *conn) Queues() []*Queue {
|
23
|
+
registered, _ := c.scripts.registeredQueues()
|
24
|
+
queues := make([]*Queue, len(registered))
|
25
|
+
|
26
|
+
for i, queue := range registered {
|
27
|
+
queues[i] = NewQueue(c, queue)
|
28
|
+
}
|
29
|
+
|
30
|
+
return queues
|
31
|
+
}
|
32
|
+
|
33
|
+
func (c *conn) Channel(msg *Msg) string {
|
34
|
+
return "default"
|
35
|
+
}
|
36
|
+
|
37
|
+
func (c *conn) Deliver(msg *Msg) error {
|
38
|
+
channel := c.Channel(msg)
|
39
|
+
facet := c.config.Facet(msg)
|
40
|
+
return c.scripts.deliver(channel, facet, msg)
|
41
|
+
}
|
42
|
+
|
43
|
+
func (c *conn) Configuration() *Config {
|
44
|
+
return c.config
|
45
|
+
}
|
46
|
+
|
47
|
+
func NewConnection(config *Config) Connection {
|
48
|
+
c := &conn{
|
49
|
+
config,
|
50
|
+
config.scripts(),
|
51
|
+
}
|
52
|
+
c.RegisterQueues()
|
53
|
+
return c
|
54
|
+
}
|
@@ -0,0 +1,125 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/customerio/gospec"
|
5
|
+
. "github.com/customerio/gospec"
|
6
|
+
"github.com/garyburd/redigo/redis"
|
7
|
+
)
|
8
|
+
|
9
|
+
func ConnectionSpec(c gospec.Context) {
|
10
|
+
// Load test instance of redis on port 6400
|
11
|
+
config := NewConfig("localhost:6400", 2)
|
12
|
+
config.AddQueue("myqueue", ".*")
|
13
|
+
conn := NewConnection(config)
|
14
|
+
|
15
|
+
c.Specify("NewConnection", func() {
|
16
|
+
c.Specify("registers any queues defined in configuration", func() {
|
17
|
+
c.Expect(len(conn.Queues()), Equals, 1)
|
18
|
+
config.AddQueue("myqueue2", ".*")
|
19
|
+
conn.RegisterQueues()
|
20
|
+
c.Expect(len(conn.Queues()), Equals, 2)
|
21
|
+
})
|
22
|
+
|
23
|
+
c.Specify("stores registered queues in redis", func() {
|
24
|
+
r := config.redisPool.Get()
|
25
|
+
defer r.Close()
|
26
|
+
|
27
|
+
values, _ := redis.Strings(r.Do("hgetall", "fairway:registered_queues"))
|
28
|
+
|
29
|
+
expected := []string{"myqueue", ".*"}
|
30
|
+
|
31
|
+
for i, str := range values {
|
32
|
+
c.Expect(str, Equals, expected[i])
|
33
|
+
}
|
34
|
+
})
|
35
|
+
})
|
36
|
+
|
37
|
+
c.Specify("Queues", func() {
|
38
|
+
c.Specify("returns a Queue for every currently registered queue", func() {
|
39
|
+
c.Expect(*conn.Queues()[0], Equals, *NewQueue(conn, "myqueue"))
|
40
|
+
})
|
41
|
+
})
|
42
|
+
|
43
|
+
c.Specify("Deliver", func() {
|
44
|
+
c.Specify("adds message to the facet for the queue", func() {
|
45
|
+
r := config.redisPool.Get()
|
46
|
+
defer r.Close()
|
47
|
+
|
48
|
+
count, _ := redis.Int(r.Do("llen", "fairway:myqueue:default"))
|
49
|
+
c.Expect(count, Equals, 0)
|
50
|
+
|
51
|
+
msg, _ := NewMsg(map[string]string{"name": "mymessage"})
|
52
|
+
|
53
|
+
conn.Deliver(msg)
|
54
|
+
|
55
|
+
count, _ = redis.Int(r.Do("llen", "fairway:myqueue:default"))
|
56
|
+
c.Expect(count, Equals, 1)
|
57
|
+
|
58
|
+
value, _ := redis.String(r.Do("lindex", "fairway:myqueue:default", 0))
|
59
|
+
c.Expect(value, Equals, msg.json())
|
60
|
+
})
|
61
|
+
|
62
|
+
c.Specify("adds facets to the list of active facets", func() {
|
63
|
+
r := config.redisPool.Get()
|
64
|
+
defer r.Close()
|
65
|
+
|
66
|
+
facets, _ := redis.Strings(r.Do("smembers", "fairway:myqueue:active_facets"))
|
67
|
+
c.Expect(len(facets), Equals, 0)
|
68
|
+
|
69
|
+
msg, _ := NewMsg(map[string]string{})
|
70
|
+
|
71
|
+
conn.Deliver(msg)
|
72
|
+
|
73
|
+
facets, _ = redis.Strings(r.Do("smembers", "fairway:myqueue:active_facets"))
|
74
|
+
c.Expect(len(facets), Equals, 1)
|
75
|
+
c.Expect(facets[0], Equals, "default")
|
76
|
+
})
|
77
|
+
|
78
|
+
c.Specify("pushes facet onto the facet queue", func() {
|
79
|
+
r := config.redisPool.Get()
|
80
|
+
defer r.Close()
|
81
|
+
|
82
|
+
count, _ := redis.Int(r.Do("llen", "fairway:myqueue:facet_queue"))
|
83
|
+
c.Expect(count, Equals, 0)
|
84
|
+
|
85
|
+
msg, _ := NewMsg(map[string]string{})
|
86
|
+
|
87
|
+
conn.Deliver(msg)
|
88
|
+
|
89
|
+
count, _ = redis.Int(r.Do("llen", "fairway:myqueue:facet_queue"))
|
90
|
+
c.Expect(count, Equals, 1)
|
91
|
+
|
92
|
+
value, _ := redis.String(r.Do("lindex", "fairway:myqueue:facet_queue", 0))
|
93
|
+
c.Expect(value, Equals, "default")
|
94
|
+
})
|
95
|
+
|
96
|
+
c.Specify("doesn't push facet if already active", func() {
|
97
|
+
r := config.redisPool.Get()
|
98
|
+
defer r.Close()
|
99
|
+
|
100
|
+
r.Do("sadd", "fairway:myqueue:active_facets", "default")
|
101
|
+
|
102
|
+
msg, _ := NewMsg(map[string]string{})
|
103
|
+
|
104
|
+
conn.Deliver(msg)
|
105
|
+
|
106
|
+
count, _ := redis.Int(r.Do("llen", "fairway:myqueue:facet_queue"))
|
107
|
+
c.Expect(count, Equals, 0)
|
108
|
+
})
|
109
|
+
|
110
|
+
c.Specify("returns nil if delivery succeeds", func() {
|
111
|
+
msg, _ := NewMsg(map[string]string{})
|
112
|
+
err := conn.Deliver(msg)
|
113
|
+
c.Expect(err, IsNil)
|
114
|
+
})
|
115
|
+
|
116
|
+
c.Specify("returns error if delivery fails", func() {
|
117
|
+
config := NewConfig("localhost:9999", 2)
|
118
|
+
conn := NewConnection(config)
|
119
|
+
|
120
|
+
msg, _ := NewMsg(map[string]string{})
|
121
|
+
err := conn.Deliver(msg)
|
122
|
+
c.Expect(err.Error(), Equals, "dial tcp 127.0.0.1:9999: connection refused")
|
123
|
+
})
|
124
|
+
})
|
125
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
func FairwayDeliver() string {
|
4
|
+
return `
|
5
|
+
local namespace = KEYS[1];
|
6
|
+
local topic = ARGV[1];
|
7
|
+
local facet = ARGV[2];
|
8
|
+
local message = ARGV[3];
|
9
|
+
|
10
|
+
local k = function (queue, subkey)
|
11
|
+
return namespace .. queue .. ':' .. subkey;
|
12
|
+
end
|
13
|
+
|
14
|
+
local registered_queues_key = namespace .. 'registered_queues';
|
15
|
+
local registered_queues = redis.call('hgetall', registered_queues_key);
|
16
|
+
|
17
|
+
-- Determine whether or not the message should
|
18
|
+
-- be delivered to each registered queue.
|
19
|
+
for i = 1, #registered_queues, 2 do
|
20
|
+
local queue = registered_queues[i];
|
21
|
+
local queue_topic = registered_queues[i+1];
|
22
|
+
|
23
|
+
-- If the message topic matches the queue topic,
|
24
|
+
-- we deliver the message to the queue.
|
25
|
+
if string.find(topic, queue_topic) then
|
26
|
+
local priorities = k(queue, 'priorities');
|
27
|
+
local active_facets = k(queue, 'active_facets');
|
28
|
+
local round_robin = k(queue, 'facet_queue');
|
29
|
+
local facet_pool = k(queue, 'facet_pool');
|
30
|
+
|
31
|
+
-- Delivering the message to a queue is as simple as
|
32
|
+
-- pushing it onto the facet's message list, and
|
33
|
+
-- incrementing the length of the queue itself.
|
34
|
+
redis.call('lpush', k(queue, facet), message)
|
35
|
+
redis.call('incr', k(queue, 'length'));
|
36
|
+
|
37
|
+
-- If the facet just became active, we need to add
|
38
|
+
-- the facet to the round-robin queue, so it's
|
39
|
+
-- messages will be processed.
|
40
|
+
if redis.call('sadd', active_facets, facet) == 1 then
|
41
|
+
local priority = tonumber(redis.call('hget', priorities, facet)) or 1
|
42
|
+
|
43
|
+
-- If the facet currently has a priority
|
44
|
+
-- we need to jump start the facet by adding
|
45
|
+
-- it to the round-robin queue and updating
|
46
|
+
-- the current priority.
|
47
|
+
if priority > 0 then
|
48
|
+
redis.call('lpush', round_robin, facet);
|
49
|
+
redis.call('hset', facet_pool, facet, 1);
|
50
|
+
|
51
|
+
-- If the facet has no set priority, just set the
|
52
|
+
-- current priority to zero. Since the facet just
|
53
|
+
-- became active, we can be sure it's already zero
|
54
|
+
-- or undefined.
|
55
|
+
else
|
56
|
+
redis.call('hset', facet_pool, facet, 0);
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
-- For any clients listening over pub/sub,
|
63
|
+
-- we should publish the message.
|
64
|
+
redis.call('publish', namespace .. topic, message);
|
65
|
+
|
66
|
+
`
|
67
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
func FairwayDestroy() string {
|
4
|
+
return `
|
5
|
+
local namespace = KEYS[1];
|
6
|
+
|
7
|
+
local k = function (queue, subkey)
|
8
|
+
return namespace .. queue .. ':' .. subkey;
|
9
|
+
end
|
10
|
+
|
11
|
+
-- Multiple queues can be passed through
|
12
|
+
-- fairway_destroy. We'll loop through all
|
13
|
+
-- provided queues, and delete related keys
|
14
|
+
-- for each queue.
|
15
|
+
for i, queue in ipairs(ARGV) do
|
16
|
+
local priorities = k(queue, 'priorities');
|
17
|
+
local active_facets = k(queue, 'active_facets');
|
18
|
+
local round_robin = k(queue, 'facet_queue');
|
19
|
+
local facet_pool = k(queue, 'facet_pool');
|
20
|
+
local length = k(queue, 'length');
|
21
|
+
|
22
|
+
local facets = redis.call('smembers', active_facets);
|
23
|
+
|
24
|
+
for i = 1, #facets, 1 do
|
25
|
+
redis.call('del', k(queue, facets[i]));
|
26
|
+
end
|
27
|
+
|
28
|
+
redis.call('del', priorities);
|
29
|
+
redis.call('del', active_facets);
|
30
|
+
redis.call('del', round_robin);
|
31
|
+
redis.call('del', facet_pool);
|
32
|
+
redis.call('del', length);
|
33
|
+
end
|
34
|
+
|
35
|
+
`
|
36
|
+
}
|
data/go/fairway_peek.go
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
func FairwayPeek() string {
|
4
|
+
return `
|
5
|
+
local namespace = KEYS[1];
|
6
|
+
|
7
|
+
for index, queue_name in ipairs(ARGV) do
|
8
|
+
local active_facets = namespace .. queue_name .. ':active_facets';
|
9
|
+
local facet_queue = namespace .. queue_name .. ':facet_queue';
|
10
|
+
|
11
|
+
local facet = redis.call('lrange', facet_queue, -1, -1)[1];
|
12
|
+
|
13
|
+
if facet then
|
14
|
+
local message_queue = namespace .. queue_name .. ':' .. facet;
|
15
|
+
local message = redis.call('lrange', message_queue, -1, -1)[1];
|
16
|
+
|
17
|
+
return {queue_name, message};
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
`
|
22
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
func FairwayPriority() string {
|
4
|
+
return `
|
5
|
+
local namespace = KEYS[1];
|
6
|
+
local queue = ARGV[1];
|
7
|
+
local facet = ARGV[2];
|
8
|
+
local new_priority = tonumber(ARGV[3]);
|
9
|
+
|
10
|
+
local k = function (queue, subkey)
|
11
|
+
return namespace .. queue .. ':' .. subkey;
|
12
|
+
end
|
13
|
+
|
14
|
+
local priorities = k(queue, 'priorities');
|
15
|
+
local round_robin = k(queue, 'facet_queue');
|
16
|
+
local facet_pool = k(queue, 'facet_pool');
|
17
|
+
|
18
|
+
-- Find the current state of the facet for the queue
|
19
|
+
local priority = tonumber(redis.call('hget', priorities, facet)) or 1;
|
20
|
+
local current = tonumber(redis.call('hget', facet_pool, facet));
|
21
|
+
|
22
|
+
-- If priority is currently zero, we need to jump
|
23
|
+
-- start the facet by adding it to the round-robin
|
24
|
+
-- queue and updating the current priority.
|
25
|
+
if new_priority > 0 and priority == 0 and current == 0 then
|
26
|
+
redis.call('lpush', round_robin, facet);
|
27
|
+
redis.call('hset', facet_pool, facet, 1);
|
28
|
+
end
|
29
|
+
|
30
|
+
-- Other than the 0 priority case, we can just
|
31
|
+
-- set the new priority, and the real priority
|
32
|
+
-- will update lazily on pull.
|
33
|
+
redis.call('hset', priorities, facet, new_priority);
|
34
|
+
|
35
|
+
`
|
36
|
+
}
|
data/go/fairway_pull.go
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
func FairwayPull() string {
|
4
|
+
return `
|
5
|
+
local namespace = KEYS[1];
|
6
|
+
|
7
|
+
local k = function (queue, subkey)
|
8
|
+
return namespace .. queue .. ':' .. subkey;
|
9
|
+
end
|
10
|
+
|
11
|
+
-- Multiple queues can be passed through
|
12
|
+
-- fairway_pull. We'll loop through all
|
13
|
+
-- provided queues, and return a message
|
14
|
+
-- from the first one that isn't empty.
|
15
|
+
for i, queue in ipairs(ARGV) do
|
16
|
+
local priorities = k(queue, 'priorities');
|
17
|
+
local active_facets = k(queue, 'active_facets');
|
18
|
+
local round_robin = k(queue, 'facet_queue');
|
19
|
+
local facet_pool = k(queue, 'facet_pool');
|
20
|
+
|
21
|
+
-- Pull a facet from the round-robin list.
|
22
|
+
-- This list guarantees each active facet will have a
|
23
|
+
-- message pulled from the queue every time through..
|
24
|
+
local facet = redis.call('rpop', round_robin);
|
25
|
+
|
26
|
+
if facet then
|
27
|
+
-- If we found an active facet, we know the facet
|
28
|
+
-- has at least one message available to be pulled
|
29
|
+
-- from it's message queue.
|
30
|
+
local messages = k(queue, facet);
|
31
|
+
local message = redis.call('rpop', messages);
|
32
|
+
|
33
|
+
if message then
|
34
|
+
redis.call('decr', k(queue, 'length'));
|
35
|
+
end
|
36
|
+
|
37
|
+
local length = redis.call('llen', messages);
|
38
|
+
|
39
|
+
-- If the length of the facet's message queue
|
40
|
+
-- is empty, then it is no longer active as
|
41
|
+
-- it no longer has any messages.
|
42
|
+
if length == 0 then
|
43
|
+
-- We remove the facet from the set of active
|
44
|
+
-- facets and don't push the facet back on the
|
45
|
+
-- round-robin queue.
|
46
|
+
redis.call('srem', active_facets, facet);
|
47
|
+
|
48
|
+
-- If the facet still has messages to process,
|
49
|
+
-- it remains in the active facet set, and is
|
50
|
+
-- pushed back on the round-robin queue.
|
51
|
+
--
|
52
|
+
-- Additionally, the priority of the facet may
|
53
|
+
-- have changed, so we'll check and update the
|
54
|
+
-- current facet's priority if needed.
|
55
|
+
else
|
56
|
+
local priority = tonumber(redis.call('hget', priorities, facet)) or 1
|
57
|
+
local current = tonumber(redis.call('hget', facet_pool, facet)) or 1
|
58
|
+
|
59
|
+
-- If the current priority is less than the
|
60
|
+
-- desired priority, let's increase the priority
|
61
|
+
-- by pushing the current facet on the round-robin
|
62
|
+
-- queue twice, and incrementing the current
|
63
|
+
-- priority.
|
64
|
+
--
|
65
|
+
-- Note: If there aren't enough messages left
|
66
|
+
-- on the facet, we don't increase priority.
|
67
|
+
if current < priority and length > current then
|
68
|
+
redis.call('lpush', round_robin, facet);
|
69
|
+
redis.call('lpush', round_robin, facet);
|
70
|
+
redis.call('hset', facet_pool, facet, current + 1);
|
71
|
+
|
72
|
+
-- If the current priority is greater than the
|
73
|
+
-- desired priority, let's decrease the priority
|
74
|
+
-- by not pushing the current facet on the round-robin
|
75
|
+
-- queue, and decrementing the current priority.
|
76
|
+
--
|
77
|
+
-- Note: Also decrement priority if there aren't
|
78
|
+
-- enough messages for the current priority. This
|
79
|
+
-- ensures priority (entries in the round-robin queue)
|
80
|
+
-- never exceeds the number of messages for a given
|
81
|
+
-- facet.
|
82
|
+
elseif current > priority or current > length then
|
83
|
+
redis.call('hset', facet_pool, facet, current - 1);
|
84
|
+
|
85
|
+
-- If the current priority is equals the
|
86
|
+
-- desired priority, let's maintain the current priority
|
87
|
+
-- by pushing the current facet on the round-robin
|
88
|
+
-- queue once.
|
89
|
+
else
|
90
|
+
redis.call('lpush', round_robin, facet);
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
return {queue, message};
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
`
|
99
|
+
}
|
data/go/message.go
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"encoding/json"
|
5
|
+
"github.com/bitly/go-simplejson"
|
6
|
+
)
|
7
|
+
|
8
|
+
type Msg struct {
|
9
|
+
*simplejson.Json
|
10
|
+
}
|
11
|
+
|
12
|
+
func NewMsg(body interface{}) (*Msg, error) {
|
13
|
+
bytes, err := json.Marshal(body)
|
14
|
+
if err != nil {
|
15
|
+
return nil, err
|
16
|
+
}
|
17
|
+
|
18
|
+
simplej, err := simplejson.NewJson(bytes)
|
19
|
+
if err != nil {
|
20
|
+
return nil, err
|
21
|
+
}
|
22
|
+
|
23
|
+
return &Msg{simplej}, nil
|
24
|
+
}
|
25
|
+
|
26
|
+
func NewMsgFromString(body string) (*Msg, error) {
|
27
|
+
simplej, err := simplejson.NewJson([]byte(body))
|
28
|
+
if err != nil {
|
29
|
+
return nil, err
|
30
|
+
}
|
31
|
+
|
32
|
+
return &Msg{simplej}, nil
|
33
|
+
}
|
34
|
+
|
35
|
+
func (m *Msg) json() string {
|
36
|
+
j, _ := m.MarshalJSON()
|
37
|
+
return string(j)
|
38
|
+
}
|
data/go/message_test.go
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/customerio/gospec"
|
5
|
+
. "github.com/customerio/gospec"
|
6
|
+
)
|
7
|
+
|
8
|
+
func MsgSpec(c gospec.Context) {
|
9
|
+
c.Specify("NewMsg", func() {
|
10
|
+
c.Specify("returns a new message with body as the content", func() {
|
11
|
+
msg, _ := NewMsg(map[string]string{"hello": "world"})
|
12
|
+
c.Expect(msg.json(), Equals, "{\"hello\":\"world\"}")
|
13
|
+
})
|
14
|
+
|
15
|
+
c.Specify("returns err if couldn't convert object", func() {
|
16
|
+
msg, err := NewMsg(func() {})
|
17
|
+
c.Expect(msg, IsNil)
|
18
|
+
c.Expect(err, Not(IsNil))
|
19
|
+
})
|
20
|
+
})
|
21
|
+
|
22
|
+
c.Specify("NewMsgFromString", func() {
|
23
|
+
c.Specify("returns a new message with string as the content", func() {
|
24
|
+
msg, _ := NewMsgFromString("{\"hello\":\"world\"}")
|
25
|
+
c.Expect(msg.json(), Equals, "{\"hello\":\"world\"}")
|
26
|
+
})
|
27
|
+
|
28
|
+
c.Specify("returns err if couldn't convert string", func() {
|
29
|
+
msg, err := NewMsgFromString("not json")
|
30
|
+
c.Expect(msg, IsNil)
|
31
|
+
c.Expect(err, Not(IsNil))
|
32
|
+
})
|
33
|
+
})
|
34
|
+
}
|
data/go/queue.go
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
type Queue struct {
|
4
|
+
conn Connection
|
5
|
+
name string
|
6
|
+
}
|
7
|
+
|
8
|
+
func NewQueue(conn Connection, name string) *Queue {
|
9
|
+
return &Queue{conn, name}
|
10
|
+
}
|
11
|
+
|
12
|
+
func (q *Queue) Pull() (string, *Msg) {
|
13
|
+
return q.conn.Configuration().scripts().pull(q.name)
|
14
|
+
}
|
data/go/queue_test.go
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"github.com/customerio/gospec"
|
5
|
+
. "github.com/customerio/gospec"
|
6
|
+
"github.com/garyburd/redigo/redis"
|
7
|
+
)
|
8
|
+
|
9
|
+
func QueueSpec(c gospec.Context) {
|
10
|
+
// Load test instance of redis on port 6400
|
11
|
+
config := NewConfig("localhost:6400", 2)
|
12
|
+
config.AddQueue("myqueue", ".*")
|
13
|
+
conn := NewConnection(config)
|
14
|
+
queue := NewQueue(conn, "myqueue")
|
15
|
+
|
16
|
+
c.Specify("NewQueue", func() {
|
17
|
+
})
|
18
|
+
|
19
|
+
c.Specify("Pull", func() {
|
20
|
+
c.Specify("pulls a message off the queue using FIFO", func() {
|
21
|
+
msg1, _ := NewMsg(map[string]string{"name": "mymessage1"})
|
22
|
+
msg2, _ := NewMsg(map[string]string{"name": "mymessage2"})
|
23
|
+
|
24
|
+
conn.Deliver(msg1)
|
25
|
+
conn.Deliver(msg2)
|
26
|
+
|
27
|
+
queueName, message := queue.Pull()
|
28
|
+
c.Expect(queueName, Equals, "myqueue")
|
29
|
+
c.Expect(message.json(), Equals, msg1.json())
|
30
|
+
|
31
|
+
queueName, message = queue.Pull()
|
32
|
+
c.Expect(queueName, Equals, "myqueue")
|
33
|
+
c.Expect(message.json(), Equals, msg2.json())
|
34
|
+
})
|
35
|
+
|
36
|
+
c.Specify("pulls from facets of the queue in round-robin", func() {
|
37
|
+
config.Facet = func(msg *Msg) string {
|
38
|
+
str, _ := msg.Get("facet").String()
|
39
|
+
return str
|
40
|
+
}
|
41
|
+
|
42
|
+
msg1, _ := NewMsg(map[string]string{"facet": "1", "name": "mymessage1"})
|
43
|
+
msg2, _ := NewMsg(map[string]string{"facet": "1", "name": "mymessage2"})
|
44
|
+
msg3, _ := NewMsg(map[string]string{"facet": "2", "name": "mymessage3"})
|
45
|
+
|
46
|
+
conn.Deliver(msg1)
|
47
|
+
conn.Deliver(msg2)
|
48
|
+
conn.Deliver(msg3)
|
49
|
+
|
50
|
+
_, message := queue.Pull()
|
51
|
+
c.Expect(message.json(), Equals, msg1.json())
|
52
|
+
_, message = queue.Pull()
|
53
|
+
c.Expect(message.json(), Equals, msg3.json())
|
54
|
+
_, message = queue.Pull()
|
55
|
+
c.Expect(message.json(), Equals, msg2.json())
|
56
|
+
})
|
57
|
+
|
58
|
+
c.Specify("removes facet from active list if it becomes empty", func() {
|
59
|
+
r := config.redisPool.Get()
|
60
|
+
defer r.Close()
|
61
|
+
|
62
|
+
msg, _ := NewMsg(map[string]string{})
|
63
|
+
conn.Deliver(msg)
|
64
|
+
|
65
|
+
count, _ := redis.Int(r.Do("scard", "fairway:myqueue:active_facets"))
|
66
|
+
c.Expect(count, Equals, 1)
|
67
|
+
|
68
|
+
queue.Pull()
|
69
|
+
|
70
|
+
count, _ = redis.Int(r.Do("scard", "fairway:myqueue:active_facets"))
|
71
|
+
c.Expect(count, Equals, 0)
|
72
|
+
})
|
73
|
+
|
74
|
+
c.Specify("returns nil if there are no messages to receive", func() {
|
75
|
+
msg, _ := NewMsg(map[string]string{})
|
76
|
+
conn.Deliver(msg)
|
77
|
+
|
78
|
+
queueName, message := queue.Pull()
|
79
|
+
c.Expect(queueName, Equals, "myqueue")
|
80
|
+
queueName, message = queue.Pull()
|
81
|
+
c.Expect(queueName, Equals, "")
|
82
|
+
c.Expect(message, IsNil)
|
83
|
+
})
|
84
|
+
})
|
85
|
+
}
|
data/go/scripts.go
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
package fairway
|
2
|
+
|
3
|
+
import (
|
4
|
+
"fmt"
|
5
|
+
"github.com/garyburd/redigo/redis"
|
6
|
+
)
|
7
|
+
|
8
|
+
type scripts struct {
|
9
|
+
config *Config
|
10
|
+
data map[string]*redis.Script
|
11
|
+
}
|
12
|
+
|
13
|
+
func newScripts(config *Config) *scripts {
|
14
|
+
return &scripts{config, make(map[string]*redis.Script)}
|
15
|
+
}
|
16
|
+
|
17
|
+
func (s *scripts) namespace() string {
|
18
|
+
namespace := s.config.Namespace
|
19
|
+
|
20
|
+
if len(namespace) > 0 {
|
21
|
+
namespace = fmt.Sprint(namespace, ":")
|
22
|
+
}
|
23
|
+
|
24
|
+
return namespace
|
25
|
+
}
|
26
|
+
|
27
|
+
func (s *scripts) registeredQueuesKey() string {
|
28
|
+
return fmt.Sprint(s.namespace(), "registered_queues")
|
29
|
+
}
|
30
|
+
|
31
|
+
func (s *scripts) registerQueue(queue *QueueDefinition) {
|
32
|
+
conn := s.config.redisPool.Get()
|
33
|
+
defer conn.Close()
|
34
|
+
|
35
|
+
_, err := redis.Bool(conn.Do("hset", s.registeredQueuesKey(), queue.name, queue.channel))
|
36
|
+
|
37
|
+
if err != nil {
|
38
|
+
panic(err)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
func (s *scripts) registeredQueues() ([]string, error) {
|
43
|
+
conn := s.config.redisPool.Get()
|
44
|
+
defer conn.Close()
|
45
|
+
return redis.Strings(conn.Do("hkeys", s.registeredQueuesKey()))
|
46
|
+
}
|
47
|
+
|
48
|
+
func (s *scripts) deliver(channel, facet string, msg *Msg) error {
|
49
|
+
conn := s.config.redisPool.Get()
|
50
|
+
defer conn.Close()
|
51
|
+
|
52
|
+
script := s.findScript(FairwayDeliver, 1)
|
53
|
+
|
54
|
+
_, err := script.Do(conn, s.namespace(), channel, facet, msg.json())
|
55
|
+
|
56
|
+
return err
|
57
|
+
}
|
58
|
+
|
59
|
+
func (s *scripts) pull(queueName string) (string, *Msg) {
|
60
|
+
conn := s.config.redisPool.Get()
|
61
|
+
defer conn.Close()
|
62
|
+
|
63
|
+
script := s.findScript(FairwayPull, 1)
|
64
|
+
|
65
|
+
result, err := redis.Strings(script.Do(conn, s.namespace(), queueName))
|
66
|
+
|
67
|
+
if err != nil {
|
68
|
+
return "", nil
|
69
|
+
}
|
70
|
+
|
71
|
+
queue := result[0]
|
72
|
+
message, _ := NewMsgFromString(result[1])
|
73
|
+
|
74
|
+
return queue, message
|
75
|
+
}
|
76
|
+
|
77
|
+
func (s *scripts) findScript(script func() string, keyCount int) *redis.Script {
|
78
|
+
content := script()
|
79
|
+
|
80
|
+
if s.data[content] == nil {
|
81
|
+
s.data[content] = redis.NewScript(keyCount, content)
|
82
|
+
}
|
83
|
+
|
84
|
+
return s.data[content]
|
85
|
+
}
|
data/lib/fairway/config.rb
CHANGED
data/lib/fairway/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fairway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-10-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -74,6 +74,23 @@ files:
|
|
74
74
|
- Rakefile
|
75
75
|
- boot.rb
|
76
76
|
- fairway.gemspec
|
77
|
+
- go/all_specs_test.go
|
78
|
+
- go/channeled_connection.go
|
79
|
+
- go/channeled_connection_test.go
|
80
|
+
- go/config.go
|
81
|
+
- go/config_test.go
|
82
|
+
- go/connection.go
|
83
|
+
- go/connection_test.go
|
84
|
+
- go/fairway_deliver.go
|
85
|
+
- go/fairway_destroy.go
|
86
|
+
- go/fairway_peek.go
|
87
|
+
- go/fairway_priority.go
|
88
|
+
- go/fairway_pull.go
|
89
|
+
- go/message.go
|
90
|
+
- go/message_test.go
|
91
|
+
- go/queue.go
|
92
|
+
- go/queue_test.go
|
93
|
+
- go/scripts.go
|
77
94
|
- lib/fairway.rb
|
78
95
|
- lib/fairway/channeled_connection.rb
|
79
96
|
- lib/fairway/config.rb
|