noeq 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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