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 CHANGED
@@ -1,5 +1,7 @@
1
1
  source :rubygems
2
2
 
3
+ gem "rake"
4
+
3
5
  # Specify your gem's dependencies in fairway.gemspec
4
6
  gemspec
5
7
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fairway (0.1.3)
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  module Fairway
2
2
  class Config
3
3
  attr_accessor :namespace
4
- attr_reader :defined_queues
4
+ attr_reader :defined_queues, :redis_options
5
5
 
6
6
  DEFAULT_FACET = "default"
7
7
 
@@ -14,7 +14,6 @@ module Fairway
14
14
 
15
15
  work = ::Sidekiq.redis do |conn|
16
16
  script = <<-SCRIPT
17
- -- take advantage of non-blocking scripts
18
17
  for i = 1, #KEYS do
19
18
  local work = redis.call('rpop', KEYS[i]);
20
19
 
@@ -1,3 +1,3 @@
1
1
  module Fairway
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.4
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-05-29 00:00:00.000000000 Z
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