fairway 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|