reliable-msg 1.0.1 → 1.1.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/README +5 -1
- data/Rakefile +30 -23
- data/changelog.txt +32 -0
- data/lib/reliable-msg.rb +10 -5
- data/lib/reliable-msg/cli.rb +47 -3
- data/lib/reliable-msg/client.rb +213 -0
- data/lib/reliable-msg/message-store.rb +128 -49
- data/lib/reliable-msg/mysql.sql +7 -1
- data/lib/reliable-msg/queue-manager.rb +263 -58
- data/lib/reliable-msg/queue.rb +100 -253
- data/lib/reliable-msg/rails.rb +114 -0
- data/lib/reliable-msg/selector.rb +65 -75
- data/lib/reliable-msg/topic.rb +215 -0
- data/test/test-queue.rb +35 -5
- data/test/test-rails.rb +59 -0
- data/test/test-topic.rb +102 -0
- metadata +54 -41
- data/lib/uuid.rb +0 -384
- data/test/test-uuid.rb +0 -48
data/test/test-queue.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
#
|
2
|
-
# = test-queue.rb - Queue
|
2
|
+
# = test-queue.rb - Queue API test cases
|
3
3
|
#
|
4
4
|
# Author:: Assaf Arkin assaf@labnotes.org
|
5
|
-
# Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/
|
5
|
+
# Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/ReliableMessaging
|
6
6
|
# Copyright:: Copyright (c) 2005 Assaf Arkin
|
7
7
|
# License:: MIT and/or Creative Commons Attribution-ShareAlike
|
8
8
|
#
|
@@ -84,8 +84,12 @@ class TestQueue < Test::Unit::TestCase
|
|
84
84
|
restart.call if restart
|
85
85
|
msg = @queue.get(ReliableMsg::Queue.selector { name == 'bar' })
|
86
86
|
assert msg && msg.id == id2, "Failed to retrieve message by selector"
|
87
|
+
msg = @queue.get(ReliableMsg::Queue.selector { name == 'baz' })
|
88
|
+
assert msg.nil?, "Retrieved non-existent message" # Tests reloading selector
|
87
89
|
msg = @queue.get(ReliableMsg::Queue.selector { name == 'foo' })
|
88
90
|
assert msg && msg.id == id1, "Failed to retrieve message by selector"
|
91
|
+
msg = @queue.get(ReliableMsg::Queue.selector { name == 'baz' })
|
92
|
+
assert msg.nil?, "Retrieved non-existent message" # Tests reloading selector
|
89
93
|
assert @queue.get.nil?, "Phantom message in queue"
|
90
94
|
end
|
91
95
|
|
@@ -113,7 +117,7 @@ class TestQueue < Test::Unit::TestCase
|
|
113
117
|
|
114
118
|
# Test that we can receive message more than once, but once we try more than
|
115
119
|
# max_deliveries, the message moves to the DLQ.
|
116
|
-
id1 = @queue.put 'test message', :
|
120
|
+
id1 = @queue.put 'test message', :max_deliveries=>2, :delivery=>:repeated
|
117
121
|
restart.call if restart
|
118
122
|
begin
|
119
123
|
@queue.get do |msg|
|
@@ -136,7 +140,7 @@ class TestQueue < Test::Unit::TestCase
|
|
136
140
|
assert msg && msg.id == id1, "Message not moved to DLQ"
|
137
141
|
|
138
142
|
# Test that message discarded when delivery mode is best_effort.
|
139
|
-
id1 = @queue.put 'test message', :
|
143
|
+
id1 = @queue.put 'test message', :max_deliveries=>1, :delivery=>:best_effort
|
140
144
|
restart.call if restart
|
141
145
|
begin
|
142
146
|
@queue.get do |msg|
|
@@ -150,7 +154,7 @@ class TestQueue < Test::Unit::TestCase
|
|
150
154
|
assert @dlq.get.nil?, "Message incorrectly moved to DLQ"
|
151
155
|
|
152
156
|
# Test that message is moved to DLQ when delivery mode is exactly_once.
|
153
|
-
id1 = @queue.put 'test message', :
|
157
|
+
id1 = @queue.put 'test message', :max_deliveries=>2, :delivery=>:once
|
154
158
|
restart.call if restart
|
155
159
|
begin
|
156
160
|
@queue.get do |msg|
|
@@ -166,6 +170,32 @@ class TestQueue < Test::Unit::TestCase
|
|
166
170
|
assert msg && msg.id == id1, "Message not moved to DLQ"
|
167
171
|
end
|
168
172
|
|
173
|
+
|
174
|
+
def test_backout
|
175
|
+
@queue.put "backout test", :delivery=>:repeated
|
176
|
+
backout = [ 1, 2, 3, 4 ]
|
177
|
+
redelivery = nil
|
178
|
+
done = false
|
179
|
+
while !done
|
180
|
+
selector = ReliableMsg::Queue::selector { redelivery.nil? || backout[redelivery - 1] + created <= now }
|
181
|
+
begin
|
182
|
+
more = @queue.get selector do |msg|
|
183
|
+
assert redelivery == msg.headers[:redelivery], "Unexpected redelivery header"
|
184
|
+
redelivery = (redelivery || 0) + 1
|
185
|
+
raise "Reject message"
|
186
|
+
true
|
187
|
+
end
|
188
|
+
rescue Exception=>error
|
189
|
+
assert error.message == "Reject message", "Unexpected ... exception"
|
190
|
+
more = true
|
191
|
+
end while more
|
192
|
+
while msg = @dlq.get
|
193
|
+
done = true
|
194
|
+
end
|
195
|
+
sleep 2
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
169
199
|
private
|
170
200
|
def clear complain
|
171
201
|
# Empty test queue and DLQ.
|
data/test/test-rails.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#
|
2
|
+
# = test-rails.rb - Rails integration test cases
|
3
|
+
#
|
4
|
+
# Author:: Assaf Arkin assaf@labnotes.org
|
5
|
+
# Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/ReliableMessaging
|
6
|
+
# Copyright:: Copyright (c) 2005 Assaf Arkin
|
7
|
+
# License:: MIT and/or Creative Commons Attribution-ShareAlike
|
8
|
+
#
|
9
|
+
#--
|
10
|
+
#++
|
11
|
+
|
12
|
+
require 'test/unit'
|
13
|
+
require 'reliable-msg'
|
14
|
+
|
15
|
+
class TestRails < Test::Unit::TestCase
|
16
|
+
|
17
|
+
begin
|
18
|
+
class TestController < ActionController::Base
|
19
|
+
queue :queue1
|
20
|
+
queue :queue2, "queue2"
|
21
|
+
queue "queue3"
|
22
|
+
|
23
|
+
topic :topic1
|
24
|
+
topic :topic2, "topic2"
|
25
|
+
topic "topic3"
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def setup
|
30
|
+
@controller = TestController.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_queue
|
34
|
+
queue = @controller.queue1
|
35
|
+
assert queue.instance_of?(ReliableMsg::Queue) && queue.name == "queue1", "Queue1 not set propertly"
|
36
|
+
queue = @controller.queue2
|
37
|
+
assert queue.instance_of?(ReliableMsg::Queue) && queue.name == "queue2", "Queue2 not set propertly"
|
38
|
+
queue = @controller.queue
|
39
|
+
assert queue.instance_of?(ReliableMsg::Queue) && queue.name == "queue3", "Queue3 not set propertly"
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_topic
|
43
|
+
topic = @controller.topic1
|
44
|
+
assert topic.instance_of?(ReliableMsg::Topic) && topic.name == "topic1", "Topic1 not set propertly"
|
45
|
+
topic = @controller.topic2
|
46
|
+
assert topic.instance_of?(ReliableMsg::Topic) && topic.name == "topic2", "Topic2 not set propertly"
|
47
|
+
topic = @controller.topic
|
48
|
+
assert topic.instance_of?(ReliableMsg::Topic) && topic.name == "topic3", "Topic3 not set propertly"
|
49
|
+
end
|
50
|
+
rescue NameError => error
|
51
|
+
raise error unless error.name == :ActionController
|
52
|
+
|
53
|
+
def test_nothing
|
54
|
+
puts "ActionController not found: Rails integration test not run"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
data/test/test-topic.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#
|
2
|
+
# = test-topic.rb - Topic API test cases
|
3
|
+
#
|
4
|
+
# Author:: Assaf Arkin assaf@labnotes.org
|
5
|
+
# Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/ReliableMessaging
|
6
|
+
# Copyright:: Copyright (c) 2005 Assaf Arkin
|
7
|
+
# License:: MIT and/or Creative Commons Attribution-ShareAlike
|
8
|
+
#
|
9
|
+
#--
|
10
|
+
#++
|
11
|
+
|
12
|
+
require 'test/unit'
|
13
|
+
require 'reliable-msg'
|
14
|
+
|
15
|
+
class TestTopic < Test::Unit::TestCase
|
16
|
+
|
17
|
+
class AbortTransaction < Exception
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup
|
21
|
+
@topic = ReliableMsg::Topic.new 'test-topic'
|
22
|
+
@manager = ReliableMsg::QueueManager.new
|
23
|
+
@manager.start
|
24
|
+
@restart = proc do
|
25
|
+
@manager.stop
|
26
|
+
@manager.start
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def teardown
|
31
|
+
@manager.stop
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_single
|
35
|
+
# Run test case without restrating queue manager (from cache).
|
36
|
+
_test_single
|
37
|
+
# Run test case by restarting queue manager (test recovery).
|
38
|
+
_test_single @restart
|
39
|
+
end
|
40
|
+
|
41
|
+
def _test_single restart = nil
|
42
|
+
# Put one message, check that we can retrieve it. Check that we
|
43
|
+
# can only retrieve it once. Then put another message, repeat.
|
44
|
+
msg1 = UUID.new
|
45
|
+
msg2 = UUID.new
|
46
|
+
@topic.put msg1
|
47
|
+
restart.call if restart
|
48
|
+
msg = @topic.get
|
49
|
+
assert msg && msg.object == msg1, "Failed to retrieve last message on topic"
|
50
|
+
assert @topic.get.nil?, "Retrieved last message on topic twice"
|
51
|
+
@topic.put msg2
|
52
|
+
restart.call if restart
|
53
|
+
msg = @topic.get
|
54
|
+
assert msg && msg.object == msg2, "Failed to retrieve last message on topic"
|
55
|
+
assert @topic.get.nil?, "Retrieved last message on topic twice"
|
56
|
+
# Test that someone else can retrieve the message.
|
57
|
+
new_topic = ReliableMsg::Topic.new @topic.name
|
58
|
+
msg = new_topic.get
|
59
|
+
assert msg && msg.object == msg2, "Failed to retrieve last message on topic"
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_selector
|
63
|
+
# Run test case without restrating queue manager (from cache).
|
64
|
+
_test_selector
|
65
|
+
# Run test case by restarting queue manager (test recovery).
|
66
|
+
_test_selector @restart
|
67
|
+
end
|
68
|
+
|
69
|
+
def _test_selector restart = nil
|
70
|
+
msg1 = UUID.new
|
71
|
+
@topic.put msg1, :name=>"foo"
|
72
|
+
restart.call if restart
|
73
|
+
msg = @topic.get(ReliableMsg::Queue.selector { name == 'bar' })
|
74
|
+
assert msg.nil?, "Retrieve message with non-matching selector"
|
75
|
+
msg = @topic.get(ReliableMsg::Queue.selector { name == 'foo' })
|
76
|
+
assert msg && msg.object == msg1, "Failed to retrieve message with selector"
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_non_expires
|
80
|
+
# Run test case without restrating queue manager (from cache).
|
81
|
+
_test_non_expires
|
82
|
+
# Run test case by restarting queue manager (test recovery).
|
83
|
+
_test_non_expires @restart
|
84
|
+
end
|
85
|
+
|
86
|
+
def _test_non_expires restart = nil
|
87
|
+
# Test that we can receive message that has not yet expired (30 second delay),
|
88
|
+
# but cannot receive message that has expires (1 second, we wait for 2).
|
89
|
+
msg1 = UUID.new
|
90
|
+
msg2 = UUID.new
|
91
|
+
@topic.put msg1, :expires=>30
|
92
|
+
restart.call if restart
|
93
|
+
msg = @topic.get
|
94
|
+
assert msg && msg.object == msg1, "Failed to retrieve message"
|
95
|
+
@topic.put msg2, :expires=>1
|
96
|
+
restart.call if restart
|
97
|
+
sleep 2
|
98
|
+
msg = @topic.get
|
99
|
+
assert msg.nil?, "Incorrectly retrieved expired message"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
metadata
CHANGED
@@ -1,63 +1,76 @@
|
|
1
|
-
!ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.8.
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.3
|
3
3
|
specification_version: 1
|
4
4
|
name: reliable-msg
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.0
|
7
|
-
date: 2005-11-
|
6
|
+
version: 1.1.0
|
7
|
+
date: 2005-11-26
|
8
8
|
summary: Reliable messaging and persistent queues for building asynchronous applications in Ruby
|
9
9
|
require_paths:
|
10
|
-
- lib
|
10
|
+
- lib
|
11
11
|
email: assaf@labnotes.org
|
12
|
-
homepage: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/
|
13
|
-
rubyforge_project:
|
14
|
-
description: This package provides reliable messaging and persistent queues for building
|
12
|
+
homepage: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/Ruby/ReliableMessaging
|
13
|
+
rubyforge_project: reliable-msg
|
14
|
+
description: "This package provides reliable messaging and persistent queues for building
|
15
|
+
asynchronous applications in Ruby. It supports transaction processing, message
|
16
|
+
selectors, priorities, delivery semantics, remote queue managers, disk-based and
|
17
|
+
MySQL message stores and more."
|
15
18
|
autorequire: reliable-msg.rb
|
16
19
|
default_executable: queues
|
17
20
|
bindir: bin
|
18
21
|
has_rdoc: true
|
19
22
|
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
23
|
requirements:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
+
-
|
25
|
+
- ">"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.0.0
|
24
28
|
version:
|
25
29
|
platform: ruby
|
26
|
-
signing_key:
|
27
|
-
cert_chain:
|
28
30
|
authors:
|
29
|
-
- Assaf Arkin
|
31
|
+
- Assaf Arkin
|
30
32
|
files:
|
31
|
-
- bin/queues
|
32
|
-
- test/test-queue.rb
|
33
|
-
- test/test-
|
34
|
-
-
|
35
|
-
- lib/reliable-msg
|
36
|
-
- lib/
|
37
|
-
- lib/reliable-msg/cli.rb
|
38
|
-
- lib/reliable-msg/
|
39
|
-
- lib/reliable-msg/
|
40
|
-
- lib/reliable-msg/
|
41
|
-
- lib/reliable-msg/queue.rb
|
42
|
-
- lib/reliable-msg/
|
43
|
-
-
|
44
|
-
-
|
45
|
-
-
|
33
|
+
- bin/queues
|
34
|
+
- test/test-queue.rb
|
35
|
+
- test/test-rails.rb
|
36
|
+
- test/test-topic.rb
|
37
|
+
- lib/reliable-msg
|
38
|
+
- lib/reliable-msg.rb
|
39
|
+
- lib/reliable-msg/cli.rb
|
40
|
+
- lib/reliable-msg/client.rb
|
41
|
+
- lib/reliable-msg/message-store.rb
|
42
|
+
- lib/reliable-msg/mysql.sql
|
43
|
+
- lib/reliable-msg/queue-manager.rb
|
44
|
+
- lib/reliable-msg/queue.rb
|
45
|
+
- lib/reliable-msg/rails.rb
|
46
|
+
- lib/reliable-msg/selector.rb
|
47
|
+
- lib/reliable-msg/topic.rb
|
48
|
+
- README
|
49
|
+
- MIT-LICENSE
|
50
|
+
- Rakefile
|
51
|
+
- changelog.txt
|
46
52
|
test_files: []
|
47
|
-
|
48
53
|
rdoc_options:
|
49
|
-
- --main
|
50
|
-
- README
|
51
|
-
- --title
|
52
|
-
- Reliable Messaging for Ruby
|
53
|
-
- --line-numbers
|
54
|
+
- "--main"
|
55
|
+
- README
|
56
|
+
- "--title"
|
57
|
+
- Reliable Messaging for Ruby
|
58
|
+
- "--line-numbers"
|
54
59
|
extra_rdoc_files:
|
55
|
-
- README
|
60
|
+
- README
|
56
61
|
executables:
|
57
|
-
- queues
|
62
|
+
- queues
|
58
63
|
extensions: []
|
59
|
-
|
60
64
|
requirements:
|
61
|
-
- MySQL for database store, otherwise uses the file system
|
62
|
-
dependencies:
|
63
|
-
|
65
|
+
- "MySQL for database store, otherwise uses the file system"
|
66
|
+
dependencies:
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: uuid
|
69
|
+
version_requirement:
|
70
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
71
|
+
requirements:
|
72
|
+
-
|
73
|
+
- ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.0.0
|
76
|
+
version:
|
data/lib/uuid.rb
DELETED
@@ -1,384 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# = uuid.rb - UUID generator
|
3
|
-
#
|
4
|
-
# Author:: Assaf Arkin assaf@labnotes.org
|
5
|
-
# Documentation:: http://trac.labnotes.org/cgi-bin/trac.cgi/wiki/RubyReliableMessaging
|
6
|
-
# Copyright:: Copyright (c) 2005 Assaf Arkin
|
7
|
-
# License:: MIT and/or Creative Commons Attribution-ShareAlike
|
8
|
-
#
|
9
|
-
#--
|
10
|
-
# Changes:
|
11
|
-
# 11/07/05 -- Incorporated into reliable-msg package
|
12
|
-
#++
|
13
|
-
|
14
|
-
|
15
|
-
require 'thread'
|
16
|
-
require 'yaml'
|
17
|
-
require 'singleton'
|
18
|
-
require 'logger'
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
# == Generating UUIDs
|
23
|
-
#
|
24
|
-
# Call UUID.new to generate and return a new UUID. The method returns a string in one of three
|
25
|
-
# formats. The default format is 36 characters long, and contains the 32 hexadecimal octets and
|
26
|
-
# hyphens separating the various value parts. The <tt>:compact</tt> format omits the hyphens,
|
27
|
-
# while the <tt>:urn</tt> format adds the <tt>:urn:uuid</tt> prefix.
|
28
|
-
#
|
29
|
-
# For example:
|
30
|
-
# 10.times do
|
31
|
-
# p UUID.new
|
32
|
-
# end
|
33
|
-
#
|
34
|
-
# ---
|
35
|
-
# == UUIDs in Brief
|
36
|
-
#
|
37
|
-
# UUID (universally unique identifier) are guaranteed to be unique across time and space.
|
38
|
-
#
|
39
|
-
# A UUID is 128 bit long, and consists of a 60-bit time value, a 16-bit sequence number and
|
40
|
-
# a 48-bit node identifier.
|
41
|
-
#
|
42
|
-
# The time value is taken from the system clock, and is monotonically incrementing. However,
|
43
|
-
# since it is possible to set the system clock backward, a sequence number is added. The
|
44
|
-
# sequence number is incremented each time the UUID generator is started. The combination
|
45
|
-
# guarantees that identifiers created on the same machine are unique with a high degree of
|
46
|
-
# probability.
|
47
|
-
#
|
48
|
-
# Note that due to the structure of the UUID and the use of sequence number, there is no
|
49
|
-
# guarantee that UUID values themselves are monotonically incrementing. The UUID value
|
50
|
-
# cannot itself be used to sort based on order of creation.
|
51
|
-
#
|
52
|
-
# To guarantee that UUIDs are unique across all machines in the network, use the IEEE 802
|
53
|
-
# MAC address of the machine's network interface card as the node identifier. Network interface
|
54
|
-
# cards have unique MAC address that are 47-bit long (the last bit acts as a broadcast flag).
|
55
|
-
# Use +ipconfig+ (Windows), or +ifconfig+ (Unix) to find the MAC address (aka physical address)
|
56
|
-
# of a network card. It takes the form of six pairs of hexadecimal digits, separated by hypen or
|
57
|
-
# colon, e.g. '<tt>08-0E-46-21-4B-35</tt>'
|
58
|
-
#
|
59
|
-
# For more information see {RFC 4122}[http://www.ietf.org/rfc/rfc4122.txt].
|
60
|
-
#
|
61
|
-
# == Configuring the UUID generator
|
62
|
-
#
|
63
|
-
# The UUID generator requires a state file which maintains the MAC address and next sequence
|
64
|
-
# number to use. By default, the UUID generator will use the file <tt>uuid.state</tt> contained
|
65
|
-
# in the current directory, or in the installation directory.
|
66
|
-
#
|
67
|
-
# Use UUID.config to specify a different location for the UUID state file. If the UUID state file
|
68
|
-
# does not exist, you can create one manually, or use UUID.config with the options <tt>:sequence</tt>
|
69
|
-
# and <tt>:mac_addr</tt>.
|
70
|
-
#
|
71
|
-
# A UUID state file looks like:
|
72
|
-
# ---
|
73
|
-
# last_clock: "0x28227f76122d80"
|
74
|
-
# mac_addr: 08-0E-46-21-4B-35
|
75
|
-
# sequence: "0x1639"
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#--
|
79
|
-
# === Time-based UUID
|
80
|
-
#
|
81
|
-
# The UUID specification prescribes the following format for representing UUIDs. Four octets encode
|
82
|
-
# the low field of the time stamp, two octects encode the middle field of the timestamp, and two
|
83
|
-
# octets encode the high field of the timestamp with the version number. Two octets encode the
|
84
|
-
# clock sequence number and six octets encode the unique node identifier.
|
85
|
-
#
|
86
|
-
# The timestamp is a 60 bit value holding UTC time as a count of 100 nanosecond intervals since
|
87
|
-
# October 15, 1582. UUIDs generated in this manner are guaranteed not to roll over until 3400 AD.
|
88
|
-
#
|
89
|
-
# The clock sequence is used to help avoid duplicates that could arise when the clock is set backward
|
90
|
-
# in time or if the node ID changes. Although the system clock is guaranteed to be monotonic, the
|
91
|
-
# system clock is not guaranteed to be monotonic across system failures. The UUID cannot be sure
|
92
|
-
# that no UUIDs were generated with timestamps larger than the current timestamp.
|
93
|
-
#
|
94
|
-
# If the clock sequence can be determined at initialization, it is incremented by one. The clock sequence
|
95
|
-
# MUST be originally (i.e. once in the lifetime of a system) initialized to a random number to minimize the
|
96
|
-
# correlation across systems. The initial value must not be correlated to the node identifier.
|
97
|
-
#
|
98
|
-
# The node identifier must be unique for each UUID generator. This is accomplished using the IEEE 802
|
99
|
-
# network card address. For systems with multiple IEEE 802 addresses, any available address can be used.
|
100
|
-
# For systems with no IEEE address, a 47 bit random value is used and the multicast bit is set so it will
|
101
|
-
# never conflict with addresses obtained from network cards.
|
102
|
-
#
|
103
|
-
# === UUID state file
|
104
|
-
#
|
105
|
-
# The UUID state is contained in the UUID state file. The file name can be specified when configuring
|
106
|
-
# the UUID generator with UUID.config. The default is to use the file +uuid.state+ in the current directory,
|
107
|
-
# or the installation directory.
|
108
|
-
#
|
109
|
-
# The UUID state file is read once when the UUID generator is first used (or configured). The sequence
|
110
|
-
# number contained in the UUID is read and used, and the state file is updated to the next sequence
|
111
|
-
# number. The MAC address is also read from the state file. The current clock time (in 100ns resolution)
|
112
|
-
# is stored in the state file whenever the sequence number is updated, but is never read.
|
113
|
-
#
|
114
|
-
# If the UUID generator detects that the system clock has been moved backwards, it will obtain a new
|
115
|
-
# sequence in the same manner. So it is possible that the UUID state file will be updated while the
|
116
|
-
# application is running.
|
117
|
-
#++
|
118
|
-
module UUID
|
119
|
-
|
120
|
-
PACKAGE = "uuid" #:nodoc:
|
121
|
-
|
122
|
-
# Default state file.
|
123
|
-
STATE_FILE = "uuid.state"
|
124
|
-
|
125
|
-
# Clock multiplier. Converts Time (resolution: seconds) to UUID clock (resolution: 10ns)
|
126
|
-
CLOCK_MULTIPLIER = 10000000 #:nodoc:
|
127
|
-
|
128
|
-
# Clock gap is the number of ticks (resolution: 10ns) between two Ruby Time ticks.
|
129
|
-
CLOCK_GAPS = 100000 #:nodoc:
|
130
|
-
|
131
|
-
# Version number stamped into the UUID to identify it as time-based.
|
132
|
-
VERSION_CLOCK = 0x0100 #:nodoc:
|
133
|
-
|
134
|
-
# Formats supported by the UUID generator.
|
135
|
-
FORMATS = {:compact=>"%08x%04x%04x%04x%012x", :default=>"%08x-%04x-%04x-%04x-%012x", :urn=>"urn:uuid:%08x-%04x-%04x-%04x-%012x"} #:nodoc:
|
136
|
-
|
137
|
-
# Length (in characters) of UUIDs generated for each of the formats.
|
138
|
-
FORMATS_LENGTHS = {:compact=>32, :default=>36, :urn=>45} #:nodoc:
|
139
|
-
|
140
|
-
ERROR_INVALID_SEQUENCE = "Invalid sequence number: found '%s', expected 4 hexdecimal digits" #:nodoc:
|
141
|
-
|
142
|
-
ERROR_NOT_A_SEQUENCE = "Not a sequence number: expected integer between 0 and 0xFFFF" #:nodoc:
|
143
|
-
|
144
|
-
ERROR_INVALID_MAC_ADDR = "Invalid MAC address: found '%s', expected a number in the format XX-XX-XX-XX-XX-XX" #:nodoc:
|
145
|
-
|
146
|
-
INFO_INITIALIZED = "Initialized UUID generator with sequence number 0x%04x and MAC address %s" #:nodoc:
|
147
|
-
|
148
|
-
ERROR_INITIALIZED_RANDOM_1 = "Initialized UUID generator with random sequence number/MAC address." #:nodoc:
|
149
|
-
|
150
|
-
ERROR_INITIALIZED_RANDOM_2 = "UUIDs are not guaranteed to be unique. Please create a uuid.state file as soon as possible." #:nodoc:
|
151
|
-
|
152
|
-
IFCONFIG_PATTERN = /[^:\-](?:[0-9A-Za-z][0-9A-Za-z][:\-]){5}[0-9A-Za-z][0-9A-Za-z][^:\-]/ #:nodoc:
|
153
|
-
|
154
|
-
@@mutex = Mutex.new
|
155
|
-
@@last_clock = @@logger = @@state_file = nil
|
156
|
-
|
157
|
-
# Generates and returns a new UUID string.
|
158
|
-
#
|
159
|
-
# The argument +format+ specifies which formatting template to use:
|
160
|
-
# * <tt>:default</tt> -- Produces 36 characters, including hyphens separating the UUID value parts
|
161
|
-
# * <tt>:compact</tt> -- Produces a 32 digits (hexadecimal) value with no hyphens
|
162
|
-
# * <tt>:urn</tt> -- Aadds the prefix <tt>urn:uuid:</tt> to the <tt>:default</tt> format
|
163
|
-
#
|
164
|
-
# For example:
|
165
|
-
# print UUID.new :default
|
166
|
-
# or just
|
167
|
-
# print UUID.new
|
168
|
-
#
|
169
|
-
# :call-seq:
|
170
|
-
# UUID.new([format]) -> string
|
171
|
-
#
|
172
|
-
def new format = nil
|
173
|
-
# Determine which format we're using for the UUID string.
|
174
|
-
template = FORMATS[format || :default] or
|
175
|
-
raise RuntimeError, "I don't know the format '#{format}'"
|
176
|
-
|
177
|
-
# The clock must be monotonically increasing. The clock resolution is at best 100 ns
|
178
|
-
# (UUID spec), but practically may be lower (on my setup, around 1ms). If this method
|
179
|
-
# is called too fast, we don't have a monotonically increasing clock, so the solution is
|
180
|
-
# to just wait.
|
181
|
-
# It is possible for the clock to be adjusted backwards, in which case we would end up
|
182
|
-
# blocking for a long time. When backward clock is detected, we prevent duplicates by
|
183
|
-
# asking for a new sequence number and continue with the new clock.
|
184
|
-
clock = @@mutex.synchronize do
|
185
|
-
# Initialize UUID generator if not already initialized. Uninitizlied UUID generator has no
|
186
|
-
# last known clock.
|
187
|
-
next_sequence unless @@last_clock
|
188
|
-
clock = (Time.new.to_f * CLOCK_MULTIPLIER).to_i & 0xFFFFFFFFFFFFFFF0
|
189
|
-
if clock > @@last_clock
|
190
|
-
@@drift = 0
|
191
|
-
@@last_clock = clock
|
192
|
-
elsif clock = @@last_clock
|
193
|
-
drift = @@drift += 1
|
194
|
-
if drift < 10000
|
195
|
-
@@last_clock += 1
|
196
|
-
else
|
197
|
-
Thread.pass
|
198
|
-
nil
|
199
|
-
end
|
200
|
-
else
|
201
|
-
next_sequence
|
202
|
-
@@last_clock = clock
|
203
|
-
end
|
204
|
-
end while not clock
|
205
|
-
sprintf template, clock & 0xFFFFFFFF, (clock >> 32)& 0xFFFF, ((clock >> 48) & 0xFFFF | VERSION_CLOCK),
|
206
|
-
@@sequence & 0xFFFF, @@mac_hex & 0xFFFFFFFFFFFF
|
207
|
-
end
|
208
|
-
|
209
|
-
alias uuid new
|
210
|
-
module_function :uuid, :new
|
211
|
-
|
212
|
-
# Configures the UUID generator. Use this method to specify the UUID state file, logger, etc.
|
213
|
-
#
|
214
|
-
# The method accepts the following options:
|
215
|
-
# * <tt>:state_file</tt> -- Specifies the location of the state file. If missing, the default
|
216
|
-
# is <tt>uuid.state</tt>
|
217
|
-
# * <tt>:logger<tt> -- The UUID generator will use this logger to report the state information (optional).
|
218
|
-
# * <tt>:sequence</tt> -- Specifies the sequence number (0 to 0xFFFF) to use. Required to
|
219
|
-
# create a new state file, ignored if the state file already exists.
|
220
|
-
# * <tt>:mac_addr</tt> -- Specifies the MAC address (xx-xx-xx-xx-xx) to use. Required to
|
221
|
-
# create a new state file, ignored if the state file already exists.
|
222
|
-
#
|
223
|
-
# For example, to create a new state file:
|
224
|
-
# UUID.config :state_file=>'my-uuid.state', :sequence=>rand(0x10000), :mac_addr=>'0C-0E-35-41-60-65'
|
225
|
-
# To use an existing state file and log to +STDOUT+:
|
226
|
-
# UUID.config :state_file=>'my-uuid.state', :logger=>Logger.new(STDOUT)
|
227
|
-
#
|
228
|
-
# :call-seq:
|
229
|
-
# UUID.config(config)
|
230
|
-
#
|
231
|
-
def self.config options
|
232
|
-
options ||= {}
|
233
|
-
@@mutex.synchronize do
|
234
|
-
@@logger = options[:logger]
|
235
|
-
next_sequence options
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
# Create a uuid.state file by finding the IEEE 802 NIC MAC address for this machine.
|
240
|
-
# Works for UNIX (ifconfig) and Windows (ipconfig). Creates the uuid.state file in the
|
241
|
-
# installation directory (typically the GEM's lib).
|
242
|
-
def self.setup
|
243
|
-
file = File.expand_path(File.dirname(__FILE__))
|
244
|
-
file = File.basename(file) == 'lib' ? file = File.join(file, '..', STATE_FILE) : file = File.join(file, STATE_FILE)
|
245
|
-
file = File.expand_path(file)
|
246
|
-
if File.exist? file
|
247
|
-
puts "#{PACKAGE}: Found an existing UUID state file: #{file}"
|
248
|
-
else
|
249
|
-
puts "#{PACKAGE}: No UUID state file found, attempting to create one for you:"
|
250
|
-
# Run ifconfig for UNIX, or ipconfig for Windows.
|
251
|
-
config = ""
|
252
|
-
begin
|
253
|
-
Kernel.open "|ifconfig" do |input|
|
254
|
-
input.each_line { |line| config << line }
|
255
|
-
end
|
256
|
-
rescue
|
257
|
-
end
|
258
|
-
begin
|
259
|
-
Kernel.open "|ipconfig /all" do |input|
|
260
|
-
input.each_line { |line| config << line }
|
261
|
-
end
|
262
|
-
rescue
|
263
|
-
end
|
264
|
-
|
265
|
-
addresses = config.scan(IFCONFIG_PATTERN).collect { |addr| addr[1..-2] }
|
266
|
-
if addresses.empty?
|
267
|
-
puts "Could not find any IEEE 802 NIC MAC addresses for this machine."
|
268
|
-
puts "You need to create the uuid.state file manually."
|
269
|
-
else
|
270
|
-
puts "Found the following IEEE 802 NIC MAC addresses on your computer:"
|
271
|
-
addresses.each { |addr| puts " #{addr}" }
|
272
|
-
puts "Selecting the first address #{addresses[0]} for use in your UUID state file."
|
273
|
-
File.open file, "w" do |output|
|
274
|
-
output.puts "mac_addr: #{addresses[0]}"
|
275
|
-
output.puts format("sequence: \"0x%04x\"", rand(0x10000))
|
276
|
-
end
|
277
|
-
puts "Created a new UUID state file: #{file}"
|
278
|
-
end
|
279
|
-
end
|
280
|
-
file
|
281
|
-
end
|
282
|
-
|
283
|
-
private
|
284
|
-
def self.state plus_one = false
|
285
|
-
return nil unless @@sequence && @@mac_addr
|
286
|
-
{"sequence"=>sprintf("0x%04x", (plus_one ? @@sequence + 1 : @@sequence) & 0xFFFF), "last_clock"=>sprintf("0x%x", @@last_clock || (Time.new.to_f * CLOCK_MULTIPLIER).to_i), "mac_addr" => @@mac_addr}
|
287
|
-
end
|
288
|
-
|
289
|
-
def self.next_sequence config = nil
|
290
|
-
# If called to advance the sequence number (config is nil), we have a state file that we're able to use.
|
291
|
-
# If called from configuration, use the specified or default state file.
|
292
|
-
state_file = (config && config[:state_file]) || @@state_file
|
293
|
-
unless state_file
|
294
|
-
state_file = if File.exist?(STATE_FILE)
|
295
|
-
STATE_FILE
|
296
|
-
else
|
297
|
-
file = File.expand_path(File.dirname(__FILE__))
|
298
|
-
file = File.basename(file) == 'lib' ? file = File.join(file, '..', STATE_FILE) : file = File.join(file, STATE_FILE)
|
299
|
-
setup unless File.exist?(file)
|
300
|
-
end
|
301
|
-
end
|
302
|
-
begin
|
303
|
-
File.open state_file, "r+" do |file|
|
304
|
-
# Lock the file for exclusive access, just to make sure it's not being read while we're
|
305
|
-
# updating its contents.
|
306
|
-
file.flock(File::LOCK_EX)
|
307
|
-
state = YAML::load file
|
308
|
-
# Get the sequence number. Must be a valid 16-bit hexadecimal value.
|
309
|
-
sequence = state['sequence']
|
310
|
-
if sequence
|
311
|
-
raise RuntimeError, format(ERROR_INVALID_SEQUENCE, sequence) unless sequence.is_a?(String) and sequence =~ /[0-9a-fA-F]{4}/
|
312
|
-
sequence = sequence.hex & 0xFFFF
|
313
|
-
else
|
314
|
-
sequence = rand(0x10000)
|
315
|
-
end
|
316
|
-
# Get the MAC address. Must be 6 pairs of hexadecimal octets. Convert MAC address into
|
317
|
-
# a 48-bit value with the higher bit being zero.
|
318
|
-
mac_addr = state['mac_addr']
|
319
|
-
raise RuntimeError, format(ERROR_INVALID_MAC_ADDR, mac_addr) unless mac_addr.is_a?(String) and mac_addr =~ /([0-9a-fA-F]{2}[:\-]){5}[0-9a-fA-F]{2}/
|
320
|
-
mac_hex = mac_addr.scan(/[0-9a-fA-F]{2}/).join.hex & 0x7FFFFFFFFFFF
|
321
|
-
|
322
|
-
# If everything is OK, proceed to the next step. Grab the sequence number and store
|
323
|
-
# the new state. Start at beginning of file, and truncate file when done.
|
324
|
-
@@mac_addr, @@mac_hex, @@sequence, @@state_file = mac_addr, mac_hex, sequence, state_file
|
325
|
-
file.pos = 0
|
326
|
-
YAML::dump state(true), file
|
327
|
-
file.truncate file.pos
|
328
|
-
end
|
329
|
-
# Initialized.
|
330
|
-
if @@logger
|
331
|
-
@@logger.info format(INFO_INITIALIZED, @@sequence, @@mac_addr)
|
332
|
-
else
|
333
|
-
warn "#{PACKAGE}: " + format(INFO_INITIALIZED, @@sequence, @@mac_addr)
|
334
|
-
end
|
335
|
-
@@last_clock, @@drift = (Time.new.to_f * CLOCK_MULTIPLIER).to_i, 0
|
336
|
-
rescue Errno::ENOENT=>error
|
337
|
-
if !config
|
338
|
-
# Generate random values.
|
339
|
-
@@mac_hex, @@sequence, @@state_file = rand(0x800000000000) | 0xF00000000000, rand(0x10000), nil
|
340
|
-
# Initialized.
|
341
|
-
if @@logger
|
342
|
-
@@logger.error ERROR_INITIALIZED_RANDOM_1
|
343
|
-
@@logger.error ERROR_INITIALIZED_RANDOM_2
|
344
|
-
else
|
345
|
-
warn "#{PACKAGE}: " + ERROR_INITIALIZED_RANDOM_1
|
346
|
-
warn "#{PACKAGE}: " + ERROR_INITIALIZED_RANDOM_2
|
347
|
-
end
|
348
|
-
@@last_clock, @@drift = (Time.new.to_f * CLOCK_MULTIPLIER).to_i, 0
|
349
|
-
else
|
350
|
-
# No state file. If we were called for configuration with valid sequence number and MAC address,
|
351
|
-
# attempt to create state file. See code above for how we interpret these values.
|
352
|
-
sequence = config[:sequence]
|
353
|
-
raise RuntimeError, format(ERROR_NOT_A_SEQUENCE, sequence) unless sequence.is_a?(Integer)
|
354
|
-
sequence &= 0xFFFF
|
355
|
-
mac_addr = config[:mac_addr]
|
356
|
-
raise RuntimeError, format(ERROR_INVALID_MAC_ADDR, mac_addr) unless mac_addr.is_a?(String) and mac_addr =~ /([0-9a-fA-F]{2}[:\-]){5}[0-9a-fA-F]{2}/
|
357
|
-
mac_hex = mac_addr.scan(/[0-9a-fA-F]{2}/).join.hex & 0x7FFFFFFFFFFF
|
358
|
-
File.open state_file, "w" do |file|
|
359
|
-
file.flock(File::LOCK_EX)
|
360
|
-
@@mac_addr, @@mac_hex, @@sequence, @@state_file = mac_addr, mac_hex, sequence, state_file
|
361
|
-
file.pos = 0
|
362
|
-
YAML::dump state(true), file
|
363
|
-
file.truncate file.pos
|
364
|
-
end
|
365
|
-
# Initialized.
|
366
|
-
if @@logger
|
367
|
-
@@logger.info format(INFO_INITIALIZED, @@sequence, @@mac_addr)
|
368
|
-
else
|
369
|
-
warn "#{PACKAGE}: " + format(INFO_INITIALIZED, @@sequence, @@mac_addr)
|
370
|
-
end
|
371
|
-
@@last_clock, @@drift = (Time.new.to_f * CLOCK_MULTIPLIER).to_i, 0
|
372
|
-
end
|
373
|
-
rescue Exception=>error
|
374
|
-
@@last_clock = nil
|
375
|
-
raise error
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
end
|
380
|
-
|
381
|
-
|
382
|
-
if __FILE__ == $0
|
383
|
-
UUID.setup
|
384
|
-
end
|