rsmp 0.8.3 → 0.9.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yaml +14 -0
  3. data/.ruby-version +1 -1
  4. data/Gemfile.lock +46 -67
  5. data/README.md +2 -2
  6. data/bin/console +1 -1
  7. data/config/tlc.yaml +8 -6
  8. data/documentation/classes_and_modules.md +4 -4
  9. data/documentation/collecting_message.md +2 -2
  10. data/documentation/tasks.md +149 -0
  11. data/lib/rsmp/archive.rb +3 -3
  12. data/lib/rsmp/cli.rb +27 -4
  13. data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
  14. data/lib/rsmp/collect/collector.rb +13 -6
  15. data/lib/rsmp/collect/command_response_collector.rb +1 -1
  16. data/lib/rsmp/collect/state_collector.rb +1 -1
  17. data/lib/rsmp/collect/status_collector.rb +2 -1
  18. data/lib/rsmp/components.rb +3 -3
  19. data/lib/rsmp/convert/export/json_schema.rb +4 -4
  20. data/lib/rsmp/convert/import/yaml.rb +1 -1
  21. data/lib/rsmp/error.rb +0 -3
  22. data/lib/rsmp/inspect.rb +1 -1
  23. data/lib/rsmp/logger.rb +5 -5
  24. data/lib/rsmp/logging.rb +1 -1
  25. data/lib/rsmp/message.rb +1 -1
  26. data/lib/rsmp/node.rb +10 -45
  27. data/lib/rsmp/proxy.rb +184 -134
  28. data/lib/rsmp/rsmp.rb +1 -1
  29. data/lib/rsmp/site.rb +24 -61
  30. data/lib/rsmp/site_proxy.rb +33 -37
  31. data/lib/rsmp/supervisor.rb +25 -21
  32. data/lib/rsmp/supervisor_proxy.rb +55 -29
  33. data/lib/rsmp/task.rb +84 -0
  34. data/lib/rsmp/tlc/signal_group.rb +17 -7
  35. data/lib/rsmp/tlc/signal_plan.rb +2 -2
  36. data/lib/rsmp/tlc/traffic_controller.rb +125 -39
  37. data/lib/rsmp/tlc/traffic_controller_site.rb +51 -35
  38. data/lib/rsmp/version.rb +1 -1
  39. data/lib/rsmp.rb +1 -1
  40. data/rsmp.gemspec +7 -7
  41. metadata +20 -20
  42. data/lib/rsmp/site_proxy_wait.rb +0 -0
  43. data/lib/rsmp/wait.rb +0 -16
  44. data/test.rb +0 -27
@@ -23,7 +23,7 @@ module RSMP
23
23
 
24
24
  def self.build_value item
25
25
  out = {}
26
-
26
+
27
27
  if item['description']
28
28
  out["description"] = item['description']
29
29
  end
@@ -95,7 +95,7 @@ module RSMP
95
95
  }
96
96
  end
97
97
  json = {
98
- "properties" => {
98
+ "properties" => {
99
99
  "aCId" => { "enum" => items.keys.sort },
100
100
  "rvs" => { "items" => { "allOf" => list } }
101
101
  }
@@ -175,7 +175,7 @@ module RSMP
175
175
  }
176
176
  ]
177
177
  }
178
- out["sxl.json"] = output_json json
178
+ out["sxl.json"] = output_json json
179
179
  end
180
180
 
181
181
  def self.generate sxl
@@ -192,7 +192,7 @@ module RSMP
192
192
  out.each_pair do |relative_path,str|
193
193
  path = File.join(folder, relative_path)
194
194
  FileUtils.mkdir_p File.dirname(path) # create folders if needed
195
- file = File.open(path, 'w+') # w+ means truncate or create new file
195
+ file = File.open(path, 'w+') # w+ means truncate or create new file
196
196
  file.puts str
197
197
  end
198
198
  end
@@ -24,7 +24,7 @@ module RSMP
24
24
  commands: {}
25
25
  }
26
26
 
27
- yaml['objects'].each_pair do |type,object|
27
+ yaml['objects'].each_pair do |type,object|
28
28
  object["alarms"].each { |id,item| sxl[:alarms][id] = item }
29
29
  object["statuses"].each { |id,item| sxl[:statuses][id] = item }
30
30
  object["commands"].each { |id,item| sxl[:commands][id] = item }
data/lib/rsmp/error.rb CHANGED
@@ -64,7 +64,4 @@ module RSMP
64
64
 
65
65
  class RepeatedStatusError < Error
66
66
  end
67
-
68
- class TimestampError < Error
69
- end
70
67
  end
data/lib/rsmp/inspect.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Costume inspect, to reduce noise
2
- #
2
+ #
3
3
  # Instance variables of classes starting with Async or RSMP are shown
4
4
  # with only their class name and object id, which reduces output,
5
5
  # especially for deep object structures.
data/lib/rsmp/logger.rb CHANGED
@@ -2,7 +2,7 @@ module RSMP
2
2
  class Logger
3
3
 
4
4
  attr_accessor :settings
5
-
5
+
6
6
  def initialize settings={}
7
7
  defaults = {
8
8
  'active'=>true,
@@ -115,7 +115,7 @@ module RSMP
115
115
  end
116
116
  end
117
117
  end
118
- return false if ack && @settings["acknowledgements"] == false &&
118
+ return false if ack && @settings["acknowledgements"] == false &&
119
119
  [:not_acknowledged,:warning,:error].include?(item[:level]) == false
120
120
  end
121
121
  true
@@ -159,7 +159,7 @@ module RSMP
159
159
 
160
160
  def log item, force:false
161
161
  if output?(item, force)
162
- output item[:level], build_output(item)
162
+ output item[:level], build_output(item)
163
163
  end
164
164
  end
165
165
 
@@ -169,7 +169,7 @@ module RSMP
169
169
  else
170
170
  ' '*length
171
171
  end
172
- end
172
+ end
173
173
 
174
174
  def dump archive, force:false, num:nil
175
175
  num ||= archive.items.size
@@ -183,7 +183,7 @@ module RSMP
183
183
  def build_part parts, item, key, &block
184
184
  skey = key.to_s
185
185
  return unless @settings[skey]
186
-
186
+
187
187
  part = item[key]
188
188
  part = yield part if block
189
189
  part = part.to_s
data/lib/rsmp/logging.rb CHANGED
@@ -8,7 +8,7 @@ module RSMP
8
8
 
9
9
  def initialize_logging options
10
10
  @archive = options[:archive] || RSMP::Archive.new
11
- @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
11
+ @logger = options[:logger] || RSMP::Logger.new(options[:log_settings])
12
12
  end
13
13
 
14
14
  def author
data/lib/rsmp/message.rb CHANGED
@@ -188,7 +188,7 @@ module RSMP
188
188
  class Unknown < Message
189
189
  end
190
190
 
191
- class AggregatedStatus < Message
191
+ class AggregatedStatus < Message
192
192
  def initialize attributes = {}
193
193
  super({
194
194
  "type" => "AggregatedStatus",
data/lib/rsmp/node.rb CHANGED
@@ -3,14 +3,14 @@
3
3
  module RSMP
4
4
  class Node
5
5
  include Logging
6
- include Wait
7
6
  include Inspect
7
+ include Task
8
8
 
9
9
  attr_reader :archive, :logger, :task, :deferred, :error_queue, :clock, :collector
10
10
 
11
11
  def initialize options
12
12
  initialize_logging options
13
- @task = options[:task]
13
+ initialize_task
14
14
  @deferred = []
15
15
  @clock = Clock.new
16
16
  @error_queue = Async::Queue.new
@@ -18,6 +18,13 @@ module RSMP
18
18
  @collect = options[:collect]
19
19
  end
20
20
 
21
+ # stop proxies, then call super
22
+ def stop_subtasks
23
+ @proxies.each { |proxy| proxy.stop }
24
+ @proxies.clear
25
+ super
26
+ end
27
+
21
28
  def ignore_errors classes, &block
22
29
  was, @ignore_errors = @ignore_errors, [classes].flatten
23
30
  yield
@@ -50,55 +57,13 @@ module RSMP
50
57
 
51
58
  def clear_deferred
52
59
  @deferred.clear
53
- end
54
-
55
- def do_start task
56
- task.annotate self.class.to_s
57
- @task = task
58
- start_action
59
- idle
60
- end
61
-
62
- def start
63
- starting
64
- if @task
65
- do_start @task
66
- else
67
- Async do |task|
68
- do_start task
69
- end
70
- end
71
- rescue Errno::EADDRINUSE => e
72
- log "Cannot start: #{e.to_s}", level: :error
73
- rescue SystemExit, SignalException, Interrupt
74
- @logger.unmute_all
75
- exiting
76
- end
77
-
78
- def idle
79
- loop do
80
- @task.sleep 60
81
- end
82
- end
83
-
84
- def stop
85
- @task.stop if @task
86
- end
87
-
88
- def restart
89
- stop
90
- start
91
- end
92
-
93
- def exiting
94
- log "Exiting", level: :info
95
60
  end
96
61
 
97
62
  def check_required_settings settings, required
98
63
  raise ArgumentError.new "Settings is empty" unless settings
99
64
  required.each do |setting|
100
65
  raise ArgumentError.new "Missing setting: #{setting}" unless settings.include? setting.to_s
101
- end
66
+ end
102
67
  end
103
68
 
104
69
  def author
data/lib/rsmp/proxy.rb CHANGED
@@ -1,32 +1,104 @@
1
- # Logging class for a connection to a remote site or supervisor.
1
+ # A connection to a remote site or supervisor.
2
+ # Uses the Task module to handle asyncronous work, but adds
3
+ # the concept of a connection that can be connected or disconnected.
2
4
 
3
5
  require 'rubygems'
4
6
 
5
- module RSMP
7
+ module RSMP
6
8
  class Proxy
7
9
  WRAPPING_DELIMITER = "\f"
8
10
 
9
11
  include Logging
10
- include Wait
11
12
  include Notifier
12
13
  include Inspect
14
+ include Task
13
15
 
14
- attr_reader :state, :archive, :connection_info, :sxl, :task, :collector, :ip, :port
16
+ attr_reader :state, :archive, :connection_info, :sxl, :collector, :ip, :port
15
17
 
16
18
  def initialize options
17
19
  initialize_logging options
18
20
  initialize_distributor
21
+ initialize_task
19
22
  setup options
20
23
  clear
24
+ @state = :disconnected
21
25
  end
22
26
 
27
+ def disconnect
28
+ end
29
+
30
+ # wait for the reader task to complete,
31
+ # which is not expected to happen before the connection is closed
32
+ def wait_for_reader
33
+ @reader.wait if @reader
34
+ end
35
+
36
+ # close connection, but keep our main task running so we can reconnect
37
+ def close
38
+ log "Closing connection", level: :warning
39
+ close_stream
40
+ close_socket
41
+ set_state :disconnected
42
+ notify_error DisconnectError.new("Connection was closed")
43
+ stop_timer
44
+ end
45
+
46
+ def stop_subtasks
47
+ stop_timer
48
+ stop_reader
49
+ clear
50
+ super
51
+ end
52
+
53
+ def stop_timer
54
+ return unless @timer
55
+ @timer.stop
56
+ @timer = nil
57
+ end
58
+
59
+ def stop_reader
60
+ return unless @reader
61
+ @reader.stop
62
+ @reader = nil
63
+ end
64
+
65
+ def close_stream
66
+ return unless @stream
67
+ @stream.close
68
+ @stream = nil
69
+ end
70
+
71
+ def close_socket
72
+ return unless @socket
73
+ @socket.close
74
+ @socket = nil
75
+ end
76
+
77
+ def stop_task
78
+ close
79
+ super
80
+ end
81
+
82
+ # change our state
83
+ def set_state state
84
+ return if state == @state
85
+ @state = state
86
+ state_changed
87
+ end
88
+
89
+ # the state changed
90
+ # override to to things like notifications
91
+ def state_changed
92
+ @state_condition.signal @state
93
+ end
94
+
95
+ # revive after a reconnect
23
96
  def revive options
24
97
  setup options
25
98
  end
26
99
 
27
100
  def setup options
28
101
  @settings = options[:settings]
29
- @task = options[:task]
30
102
  @socket = options[:socket]
31
103
  @stream = options[:stream]
32
104
  @protocol = options[:protocol]
@@ -35,7 +107,6 @@ module RSMP
35
107
  @connection_info = options[:info]
36
108
  @sxl = nil
37
109
  @site_settings = nil # can't pick until we know the site id
38
- @state = :stopped
39
110
  if options[:collect]
40
111
  @collector = RSMP::Collector.new self, options[:collect]
41
112
  @collector.start
@@ -52,35 +123,16 @@ module RSMP
52
123
  node.clock
53
124
  end
54
125
 
55
- def run
56
- start
57
- @reader.wait if @reader
58
- ensure
59
- stop unless [:stopped, :stopping].include? @state
60
- end
61
-
62
126
  def ready?
63
127
  @state == :ready
64
128
  end
65
129
 
66
130
  def connected?
67
- @state == :starting || @state == :ready
131
+ @state == :connected || @state == :ready
68
132
  end
69
133
 
70
-
71
- def start
72
- set_state :starting
73
- end
74
-
75
- def stop
76
- return if @state == :stopped
77
- set_state :stopping
78
- stop_tasks
79
- notify_error DisconnectError.new("Connection was closed")
80
- ensure
81
- close_socket
82
- clear
83
- set_state :stopped
134
+ def disconnected?
135
+ @state == :disconnected
84
136
  end
85
137
 
86
138
  def clear
@@ -97,56 +149,57 @@ module RSMP
97
149
  @acknowledgement_condition = Async::Notification.new
98
150
  end
99
151
 
100
- def close_socket
101
- if @stream
102
- @stream.close
103
- @stream = nil
104
- end
105
-
106
- if @socket
107
- @socket.close
108
- @socket = nil
152
+ # run an async task that reads from @socket
153
+ def start_reader
154
+ @reader = @task.async do |task|
155
+ task.annotate "reader"
156
+ run_reader
109
157
  end
110
158
  end
111
159
 
112
- def start_reader
113
- @reader = @task.async do |task|
114
- task.annotate "reader"
115
- @stream ||= Async::IO::Stream.new(@socket)
116
- @protocol ||= Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
117
- while json = @protocol.read_line
118
- beginning = Time.now
119
- message = process_packet json
120
- duration = Time.now - beginning
121
- ms = (duration*1000).round(4)
122
- if duration > 0
123
- per_second = (1.0 / duration).round
124
- else
125
- per_second = Float::INFINITY
126
- end
127
- if message
128
- type = message.type
129
- m_id = Logger.shorten_message_id(message.m_id)
130
- else
131
- type = 'Unknown'
132
- m_id = nil
133
- end
134
- str = [type,m_id,"processed in #{ms}ms, #{per_second}req/s"].compact.join(' ')
135
- log str, level: :statistics
136
- end
137
- rescue Async::Wrapper::Cancelled
138
- # ignore
139
- rescue EOFError
140
- log "Connection closed", level: :warning
141
- rescue IOError => e
142
- log "IOError: #{e}", level: :warning
143
- rescue Errno::ECONNRESET
144
- log "Connection reset by peer", level: :warning
145
- rescue Errno::EPIPE
146
- log "Broken pipe", level: :warning
147
- rescue StandardError => e
148
- notify_error e, level: :internal
160
+ def run_reader
161
+ @stream ||= Async::IO::Stream.new(@socket)
162
+ @protocol ||= Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
163
+ loop do
164
+ read_line
165
+ end
166
+ rescue Restart
167
+ log "Closing connection", level: :warning
168
+ raise
169
+ rescue Async::Wrapper::Cancelled
170
+ # ignore exceptions raised when a wait is aborted because a task is stopped
171
+ rescue EOFError, Async::Stop
172
+ log "Connection closed", level: :warning
173
+ rescue IOError => e
174
+ log "IOError: #{e}", level: :warning
175
+ rescue Errno::ECONNRESET
176
+ log "Connection reset by peer", level: :warning
177
+ rescue Errno::EPIPE
178
+ log "Broken pipe", level: :warning
179
+ rescue StandardError => e
180
+ notify_error e, level: :internal
181
+ end
182
+
183
+ def read_line
184
+ json = @protocol.read_line
185
+ beginning = Time.now
186
+ message = process_packet json
187
+ duration = Time.now - beginning
188
+ ms = (duration*1000).round(4)
189
+ if duration > 0
190
+ per_second = (1.0 / duration).round
191
+ else
192
+ per_second = Float::INFINITY
193
+ end
194
+ if message
195
+ type = message.type
196
+ m_id = Logger.shorten_message_id(message.m_id)
197
+ else
198
+ type = 'Unknown'
199
+ m_id = nil
149
200
  end
201
+ str = [type,m_id,"processed in #{ms}ms, #{per_second}req/s"].compact.join(' ')
202
+ log str, level: :statistics
150
203
  end
151
204
 
152
205
  def notify_error e, options={}
@@ -160,36 +213,40 @@ module RSMP
160
213
  end
161
214
 
162
215
  def start_timer
216
+ return if @timer
163
217
  name = "timer"
164
218
  interval = @site_settings['intervals']['timer'] || 1
165
219
  log "Starting #{name} with interval #{interval} seconds", level: :debug
166
220
  @latest_watchdog_received = Clock.now
167
-
168
221
  @timer = @task.async do |task|
169
222
  task.annotate "timer"
170
- next_time = Time.now.to_f
171
- loop do
172
- begin
173
- now = Clock.now
174
- timer(now)
175
- rescue RSMP::Schemer::Error => e
176
- puts "Timer: Schema error: #{e}"
177
- rescue EOFError => e
178
- log "Timer: Connection closed: #{e}", level: :warning
179
- rescue IOError => e
180
- log "Timer: IOError", level: :warning
181
- rescue Errno::ECONNRESET
182
- log "Timer: Connection reset by peer", level: :warning
183
- rescue Errno::EPIPE => e
184
- log "Timer: Broken pipe", level: :warning
185
- rescue StandardError => e
186
- notify_error e, level: :internal
187
- end
188
- ensure
189
- next_time += interval
190
- duration = next_time - Time.now.to_f
191
- task.sleep duration
223
+ run_timer task, interval
224
+ end
225
+ end
226
+
227
+ def run_timer task, interval
228
+ next_time = Time.now.to_f
229
+ loop do
230
+ begin
231
+ now = Clock.now
232
+ timer(now)
233
+ rescue RSMP::Schemer::Error => e
234
+ log "Timer: Schema error: #{e}", level: :warning
235
+ rescue EOFError => e
236
+ log "Timer: Connection closed: #{e}", level: :warning
237
+ rescue IOError => e
238
+ log "Timer: IOError", level: :warning
239
+ rescue Errno::ECONNRESET
240
+ log "Timer: Connection reset by peer", level: :warning
241
+ rescue Errno::EPIPE => e
242
+ log "Timer: Broken pipe", level: :warning
243
+ rescue StandardError => e
244
+ notify_error e, level: :internal
192
245
  end
246
+ ensure
247
+ next_time += interval
248
+ duration = next_time - Time.now.to_f
249
+ task.sleep duration
193
250
  end
194
251
  end
195
252
 
@@ -200,7 +257,7 @@ module RSMP
200
257
  end
201
258
 
202
259
  def watchdog_send_timer now
203
- return unless @watchdog_started
260
+ return unless @watchdog_started
204
261
  return if @site_settings['intervals']['watchdog'] == :never
205
262
  if @latest_watchdog_send_at == nil
206
263
  send_watchdog now
@@ -226,9 +283,13 @@ module RSMP
226
283
  @awaiting_acknowledgement.clone.each_pair do |m_id, message|
227
284
  latest = message.timestamp + timeout
228
285
  if now > latest
229
- log "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds", level: :error
230
- stop
231
- notify_error MissingAcknowledgment.new('No ack')
286
+ str = "No acknowledgements for #{message.type} #{message.m_id_short} within #{timeout} seconds"
287
+ log str, level: :error
288
+ begin
289
+ close
290
+ ensure
291
+ notify_error MissingAcknowledgment.new(str)
292
+ end
232
293
  end
233
294
  end
234
295
  end
@@ -238,16 +299,16 @@ module RSMP
238
299
  latest = @latest_watchdog_received + timeout
239
300
  left = latest - now
240
301
  if left < 0
241
- log "No Watchdog within #{timeout} seconds", level: :error
242
- stop
302
+ str = "No Watchdog within #{timeout} seconds"
303
+ log str, level: :error
304
+ begin
305
+ close # this will stop the current task (ourself)
306
+ ensure
307
+ notify_error MissingWatchdog.new(str) # but ensure block will still be reached
308
+ end
243
309
  end
244
310
  end
245
311
 
246
- def stop_tasks
247
- @timer.stop if @timer
248
- @reader.stop if @reader
249
- end
250
-
251
312
  def log str, options={}
252
313
  super str, options.merge(ip: @ip, port: @port, site_id: @site_id)
253
314
  end
@@ -355,7 +416,7 @@ module RSMP
355
416
  str = "Rejected #{message.type},"
356
417
  notify_error e.exception(str), message: message
357
418
  dont_acknowledge message, str, reason
358
- stop
419
+ close
359
420
  message
360
421
  ensure
361
422
  node.clear_deferred
@@ -436,19 +497,14 @@ module RSMP
436
497
  send_message message, "for #{original.type} #{original.m_id_short}"
437
498
  end
438
499
 
439
- def set_state state
440
- @state = state
441
- @state_condition.signal @state
442
- end
443
-
444
- def wait_for_state state, timeout
500
+ def wait_for_state state, timeout:
445
501
  states = [state].flatten
446
502
  return if states.include?(@state)
447
- wait_for(@state_condition,timeout) do
503
+ wait_for_condition(@state_condition,timeout: timeout) do
448
504
  states.include?(@state)
449
505
  end
450
506
  @state
451
- rescue Async::TimeoutError
507
+ rescue RSMP::TimeoutError
452
508
  raise RSMP::TimeoutError.new "Did not reach state #{state} within #{timeout}s"
453
509
  end
454
510
 
@@ -557,10 +613,10 @@ module RSMP
557
613
  end
558
614
  end
559
615
 
560
- def connection_complete
616
+ def handshake_complete
561
617
  set_state :ready
562
618
  end
563
-
619
+
564
620
  def version_acknowledged
565
621
  end
566
622
 
@@ -572,23 +628,17 @@ module RSMP
572
628
  node.site_id
573
629
  end
574
630
 
575
- def wait_for_acknowledgement parent_task, options={}, m_id
576
- collector = Collector.new self, options.merge(task: parent_task, type: ['MessageAck','MessageNotAck'])
577
- collector.collect do |message|
578
- if message.is_a?(MessageNotAck)
579
- if message.attribute('oMId') == m_id
580
- m_id_short = RSMP::Message.shorten_m_id m_id, 8
581
- raise RSMP::MessageRejected.new "Aggregated status request #{m_id_short} was rejected with '#{message.attribute('rea')}'"
582
- end
583
- elsif message.is_a?(MessageAck)
584
- collector.complete if message.attribute('oMId') == m_id
631
+ def send_and_optionally_collect message, options, &block
632
+ collect_options = options[:collect] || options[:collect!]
633
+ if collect_options
634
+ task = @task.async do |task|
635
+ task.annotate 'send_and_optionally_collect'
636
+ collector = yield collect_options # call block to create collector
637
+ collector.collect
638
+ collector.ok! if options[:collect!] # raise any errors if the bang version was specified
639
+ collector
585
640
  end
586
- end
587
- end
588
641
 
589
- def send_and_optionally_collect message, options, &block
590
- if options[:collect]
591
- task = @task.async { |task| yield task }
592
642
  send_message message, validate: options[:validate]
593
643
  { sent: message, collector: task.wait }
594
644
  else
data/lib/rsmp/rsmp.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Get the current time in UTC, with optional adjustment
2
- # Convertion to string uses the RSMP format 2015-06-08T12:01:39.654Z
2
+ # Convertion to string uses the RSMP format 2015-06-08T12:01:39.654Z
3
3
  # Note that using to_s on a my_clock.to_s will not produce an RSMP formatted timestamp,
4
4
  # you need to use Clock.to_s my_clock
5
5