noeq 0.1.0 → 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.
Files changed (6) hide show
  1. data/README.md +13 -0
  2. data/TODO +2 -0
  3. data/lib/noeq.rb +73 -8
  4. data/noeq.gemspec +1 -1
  5. data/test/noeq_test.rb +89 -0
  6. metadata +6 -3
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  noeq-rb is a [noeqd](https://github.com/bmizerany/noeqd) GUID client in Ruby.
4
4
 
5
+ [Annotated source code is available](http://titanous.com/noeq-rb/).
6
+
5
7
  ## Installation
6
8
 
7
9
  ```
@@ -26,3 +28,14 @@ noeq = Noeq.new('idserver.local')
26
28
  noeq.generate #=> 142692638036852736
27
29
  noeq.generate(5) #=> [142692782450933760, 142692782450933761, 142692782450933762, 142692782450933763, 142692782450933764]
28
30
  ```
31
+
32
+ ### Async usage
33
+
34
+ ```ruby
35
+ require 'noeq'
36
+
37
+ noeq = Noeq.new('localhost', 4444, :async => true)
38
+ noeq.request_id
39
+ # do some things
40
+ noeq.fetch_id #=> 142692638036852736
41
+ ```
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ * generate can probably block forever in specific failure cases
2
+ * multiple server failover support
@@ -1,6 +1,13 @@
1
+ # **Noeq** generates GUIDs using [noeqd](https://github.com/bmizerany/noeqd).
2
+
3
+ # `noeqd` uses a simple TCP wire protocol, so let's require our only dependency,
4
+ # `socket`.
1
5
  require 'socket'
2
6
 
3
7
  class Noeq
8
+
9
+ # If you just want to test out `noeq` or need to use it in a one-off script,
10
+ # this method allows for very simple usage.
4
11
  def self.generate(n=1)
5
12
  noeq = new
6
13
  ids = noeq.generate(n)
@@ -8,36 +15,94 @@ class Noeq
8
15
  ids
9
16
  end
10
17
 
11
- def initialize(server = 'localhost', port = 4444)
12
- @server, @port, = server, port
18
+ # `Noeq.new` defaults to connecting to `localhost:4444` with async off.
19
+ # The `options` hash is used so that we are verbose when turning async on.
20
+ def initialize(host = 'localhost', port = 4444, options = {})
21
+ @host, @port, @async = host, port, options[:async]
13
22
  connect
14
23
  end
15
24
 
25
+ # The first thing that we need to do is connect to the `noeqd` server.
16
26
  def connect
17
- @socket = TCPSocket.new @server, @port
27
+ # We create a new TCP `STREAM` socket. There are a few other types of
28
+ # sockets, but this is the most common.
29
+ @socket = Socket.new(:INET, :STREAM)
30
+
31
+ # In order to create a socket connection we need an address object.
32
+ address = Socket.sockaddr_in(@port, @host)
33
+
34
+ # If async is enabled, we establish the connection in nonblocking mode,
35
+ # otherwise we connect normally, which will wait until the connection is
36
+ # established.
37
+ @async ? @socket.connect_nonblock(address) : @socket.connect(address)
38
+
39
+ # `Socket.connect_nonblock` raises `Errno::EINPROGRESS` if the socket isn't
40
+ # connected instantly. It will be connected in the background, so we ignore
41
+ # the exception
42
+ rescue Errno::EINPROGRESS
18
43
  end
19
44
 
20
45
  def disconnect
46
+ # If the socket has already been closed by the other side, `close` will
47
+ # raise, so we rescue it.
21
48
  @socket.close rescue false
22
49
  end
23
50
 
51
+ # The workhorse generate method. Defaults to one id, but up to 255 can be
52
+ # requested.
24
53
  def generate(n=1)
25
- @socket.send [n].pack('c'), 0
26
- ids = (1..n).map { get_id }.compact
27
- ids.length > 1 ? ids : ids.first
54
+ request_id(n)
55
+ fetch_id(n)
56
+
57
+ # If something goes wrong, we reconnect and retry. There is a slim chance
58
+ # that this will result in an infinite loop, but most errors are raised in
59
+ # the reconnect step and won't get re-rescued here.
28
60
  rescue
29
61
  disconnect
30
62
  connect
31
63
  retry
32
64
  end
33
65
 
66
+ def request_id(n=1)
67
+ # The integer is packed into a binary byte and sent to the `noeqd` server.
68
+ # The second argument to `BasicSocket#send` is a bitmask of flags, we don't
69
+ # need anything special, so it is set to zero.
70
+ @socket.send [n].pack('c'), 0
71
+ end
72
+ alias :request_ids :request_id
73
+
74
+ def fetch_id(n=1)
75
+ # We collect the ids from the `noeqd` server.
76
+ ids = (1..n).map { get_id }.compact
77
+
78
+ # If we have more than one id, we return the array, otherwise we return the
79
+ # single id.
80
+ ids.length > 1 ? ids : ids.first
81
+ end
82
+ alias :fetch_ids :fetch_id
83
+
34
84
  private
35
85
 
36
86
  def get_id
37
- (read_long << 32) + read_long
87
+ # `noeqd` sends us a 64-bit unsigned integer in network (big-endian) byte
88
+ # order, but Ruby 1.8 doesn't have a native unpack directive for this, so we
89
+ # do it manually by shifting the high bits and adding the low bits.
90
+ high, low = read_long, read_long
91
+ return unless high && low
92
+ (high << 32) + low
38
93
  end
39
94
 
40
95
  def read_long
41
- @socket.read(4).unpack("N").first
96
+ # `IO.select` blocks until one of the sockets passed in has an event
97
+ # or a timeout is reached (the fourth argument). We don't do the `select`
98
+ # if we are in async mode.
99
+ IO.select([@socket], nil, nil, 0.1) unless @async
100
+
101
+ # Since `select` has already blocked for us, we are pretty sure that
102
+ # there is data available on the socket, so we try to fetch 4 bytes and
103
+ # unpack them as a 32-bit big-endian unsigned integer. If there is no data
104
+ # available this will raise `Errno::EAGAIN` which will propagate up and
105
+ # could cause a retry.
106
+ @socket.recv_nonblock(4).unpack("N").first
42
107
  end
43
108
  end
@@ -12,5 +12,5 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
13
13
  gem.name = "noeq"
14
14
  gem.require_paths = ["lib"]
15
- gem.version = "0.1.0"
15
+ gem.version = "0.2.0"
16
16
  end
@@ -0,0 +1,89 @@
1
+ require 'test/unit'
2
+ require './lib/noeq'
3
+
4
+ class NoeqTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ FakeNoeqd.start
8
+ end
9
+
10
+ def teardown
11
+ FakeNoeqd.stop
12
+ end
13
+
14
+ def test_simple_generate
15
+ assert_equal expected_id, Noeq.generate
16
+ end
17
+
18
+ def test_multiple_generate
19
+ noeq = Noeq.new
20
+ assert_equal [expected_id]*3, noeq.generate(3)
21
+ end
22
+
23
+ def test_different_port
24
+ FakeNoeqd.stop
25
+ FakeNoeqd.start(4545)
26
+
27
+ noeq = Noeq.new('localhost', 4545)
28
+ assert_equal expected_id, noeq.generate
29
+ end
30
+
31
+ def test_reconnect
32
+ noeq = Noeq.new
33
+ assert noeq.generate
34
+
35
+ FakeNoeqd.stop
36
+ FakeNoeqd.start
37
+
38
+ assert_equal expected_id, noeq.generate
39
+ end
40
+
41
+ def test_async_generate
42
+ noeq = Noeq.new('localhost', 4444, :async => true)
43
+ noeq.request_id
44
+ sleep 0.0001
45
+ assert_equal expected_id, noeq.fetch_id
46
+ end
47
+
48
+ def test_async_request_with_disconnected_server_raises
49
+ noeq = Noeq.new('localhost', 4444, :async => true)
50
+ FakeNoeqd.stop
51
+ assert_raises(Errno::EPIPE) { noeq.request_id }
52
+ end
53
+
54
+ private
55
+
56
+ def expected_id
57
+ 144897448664367104
58
+ end
59
+
60
+ end
61
+
62
+ class FakeNoeqd
63
+
64
+ def self.start(port = 4444)
65
+ @server = new(port)
66
+ Thread.new { @server.accept_connections }
67
+ end
68
+
69
+ def self.stop
70
+ @server.stop
71
+ end
72
+
73
+ def initialize(port)
74
+ @socket = TCPServer.new(port)
75
+ end
76
+
77
+ def stop
78
+ @socket.close rescue true
79
+ end
80
+
81
+ def accept_connections
82
+ while conn = @socket.accept
83
+ while n = conn.read(1)
84
+ conn.send "\x02\x02\xC7v<\x80\x00\x00" * n.unpack('c')[0], 0
85
+ end
86
+ end
87
+ end
88
+
89
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noeq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.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: 2011-12-02 00:00:00.000000000 Z
12
+ date: 2011-12-09 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Ruby noeqd GUID client
15
15
  email:
@@ -22,8 +22,10 @@ files:
22
22
  - LICENSE
23
23
  - README.md
24
24
  - Rakefile
25
+ - TODO
25
26
  - lib/noeq.rb
26
27
  - noeq.gemspec
28
+ - test/noeq_test.rb
27
29
  homepage: http://github.com/titanous/noeq-rb
28
30
  licenses: []
29
31
  post_install_message:
@@ -48,4 +50,5 @@ rubygems_version: 1.8.11
48
50
  signing_key:
49
51
  specification_version: 3
50
52
  summary: Ruby noeqd GUID client
51
- test_files: []
53
+ test_files:
54
+ - test/noeq_test.rb