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 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