onstomp 1.0.0pre1 → 1.0.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 (75) hide show
  1. data/README.md +1 -1
  2. data/Rakefile +8 -0
  3. data/examples/openuri.rb +36 -0
  4. data/lib/onstomp.rb +4 -0
  5. data/lib/onstomp/client.rb +6 -5
  6. data/lib/onstomp/components.rb +0 -1
  7. data/lib/onstomp/components/frame_headers.rb +35 -38
  8. data/lib/onstomp/components/threaded_processor.rb +13 -0
  9. data/lib/onstomp/connections/base.rb +15 -8
  10. data/lib/onstomp/connections/stomp_1.rb +0 -6
  11. data/lib/onstomp/connections/stomp_1_0.rb +8 -0
  12. data/lib/onstomp/connections/stomp_1_1.rb +8 -0
  13. data/lib/onstomp/failover.rb +16 -0
  14. data/lib/onstomp/failover/buffers.rb +8 -0
  15. data/lib/onstomp/failover/buffers/written.rb +91 -0
  16. data/lib/onstomp/failover/client.rb +127 -0
  17. data/lib/onstomp/failover/failover_configurable.rb +63 -0
  18. data/lib/onstomp/failover/failover_events.rb +96 -0
  19. data/lib/onstomp/failover/new_with_failover.rb +20 -0
  20. data/lib/onstomp/failover/pools.rb +8 -0
  21. data/lib/onstomp/failover/pools/base.rb +39 -0
  22. data/lib/onstomp/failover/pools/round_robin.rb +17 -0
  23. data/lib/onstomp/failover/uri.rb +34 -0
  24. data/lib/onstomp/interfaces/client_configurable.rb +2 -6
  25. data/lib/onstomp/interfaces/client_events.rb +4 -0
  26. data/lib/onstomp/interfaces/connection_events.rb +3 -3
  27. data/lib/onstomp/interfaces/event_manager.rb +8 -0
  28. data/lib/onstomp/interfaces/uri_configurable.rb +7 -7
  29. data/lib/onstomp/open-uri.rb +37 -0
  30. data/lib/onstomp/open-uri/client_extensions.rb +88 -0
  31. data/lib/onstomp/open-uri/message_queue.rb +38 -0
  32. data/lib/onstomp/version.rb +1 -1
  33. data/spec/onstomp/client_spec.rb +1 -4
  34. data/spec/onstomp/components/frame_headers_spec.rb +2 -5
  35. data/spec/onstomp/connections/stomp_1_0_spec.rb +22 -0
  36. data/spec/onstomp/connections/stomp_1_1_spec.rb +22 -0
  37. data/spec/onstomp/connections/stomp_1_spec.rb +2 -19
  38. data/spec/onstomp/connections_spec.rb +4 -0
  39. data/spec/onstomp/failover/buffers/written_spec.rb +8 -0
  40. data/spec/onstomp/failover/client_spec.rb +38 -0
  41. data/spec/onstomp/failover/failover_events_spec.rb +75 -0
  42. data/spec/onstomp/failover/new_with_failover_spec.rb +16 -0
  43. data/spec/onstomp/failover/pools/base_spec.rb +54 -0
  44. data/spec/onstomp/failover/pools/round_robin_spec.rb +27 -0
  45. data/spec/onstomp/failover/uri_spec.rb +21 -0
  46. data/spec/onstomp/full_stacks/failover_spec.rb +55 -0
  47. data/spec/onstomp/full_stacks/onstomp_spec.rb +15 -0
  48. data/spec/onstomp/full_stacks/open-uri_spec.rb +40 -0
  49. data/spec/onstomp/full_stacks/ssl/README +6 -0
  50. data/spec/onstomp/full_stacks/ssl/broker_cert.csr +17 -0
  51. data/spec/onstomp/full_stacks/ssl/broker_cert.pem +72 -0
  52. data/spec/onstomp/full_stacks/ssl/broker_key.pem +27 -0
  53. data/spec/onstomp/full_stacks/ssl/client_cert.csr +17 -0
  54. data/spec/onstomp/full_stacks/ssl/client_cert.pem +72 -0
  55. data/spec/onstomp/full_stacks/ssl/client_key.pem +27 -0
  56. data/spec/onstomp/full_stacks/ssl/demoCA/cacert.pem +17 -0
  57. data/spec/onstomp/full_stacks/ssl/demoCA/index.txt +2 -0
  58. data/spec/onstomp/full_stacks/ssl/demoCA/index.txt.attr +1 -0
  59. data/spec/onstomp/full_stacks/ssl/demoCA/index.txt.attr.old +1 -0
  60. data/spec/onstomp/full_stacks/ssl/demoCA/index.txt.old +1 -0
  61. data/spec/onstomp/full_stacks/ssl/demoCA/newcerts/01.pem +72 -0
  62. data/spec/onstomp/full_stacks/ssl/demoCA/newcerts/02.pem +72 -0
  63. data/spec/onstomp/full_stacks/ssl/demoCA/private/cakey.pem +17 -0
  64. data/spec/onstomp/full_stacks/ssl/demoCA/serial +1 -0
  65. data/spec/onstomp/full_stacks/ssl/demoCA/serial.old +1 -0
  66. data/spec/onstomp/full_stacks/test_broker.rb +251 -0
  67. data/spec/onstomp/interfaces/connection_events_spec.rb +3 -1
  68. data/spec/onstomp/open-uri/client_extensions_spec.rb +113 -0
  69. data/spec/onstomp/open-uri/message_queue_spec.rb +29 -0
  70. data/spec/onstomp/open-uri_spec.rb +43 -0
  71. data/spec/spec_helper.rb +2 -0
  72. data/yard_extensions.rb +5 -1
  73. metadata +82 -8
  74. data/lib/onstomp/components/nil_processor.rb +0 -20
  75. data/spec/onstomp/components/nil_processor_spec.rb +0 -32
@@ -0,0 +1,127 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A failover client that wraps multiple {OnStomp::Client clients} and maintains
4
+ # a connection to one of these clients. Frames are sent to the currently
5
+ # connected client. If the connection is lost, a failover client will
6
+ # automatically reconnect to another client in the pool, re-transmit any
7
+ # necessary frames and resume operation.
8
+ class OnStomp::Failover::Client
9
+ include OnStomp::Failover::FailoverConfigurable
10
+ include OnStomp::Failover::FailoverEvents
11
+ include OnStomp::Interfaces::FrameMethods
12
+
13
+ # The class to use when instantiating a new {#client_pool}.
14
+ # Defaults to {OnStomp::Failover::Pools::RoundRobin}
15
+ # @return [Class]
16
+ attr_configurable_pool :pool
17
+ # The class to use when instantiating a new frame buffer.
18
+ # Defaults to {OnStomp::Failover::Buffers::Written}
19
+ # @return [Class]
20
+ attr_configurable_buffer :buffer
21
+ # The delay in seconds to wait between connection retries.
22
+ # Defaults to +10+.
23
+ # @return [Fixnum]
24
+ attr_configurable_int :retry_delay, :default => 10
25
+ # The maximum number of times to retry connecting during a reconnect
26
+ # loop. A non-positive number will force the failover client to try to
27
+ # reconnect indefinitely. Defaults to +0+
28
+ # @return [Fixnum]
29
+ attr_configurable_int :retry_attempts, :default => 0
30
+ # Whether or not to randomize the {#client_pool} before connecting through
31
+ # any of its {OnStomp::Client clients}. Defaults to +false+
32
+ # @return [true,false]
33
+ attr_configurable_bool :randomize, :default => false
34
+
35
+ attr_reader :uri, :client_pool, :active_client, :frame_buffer, :connection
36
+
37
+ def initialize(uris, options={})
38
+ if uris.is_a?(Array)
39
+ uris = "failover:(#{uris.map { |u| u.to_s }.join(',')})"
40
+ end
41
+ @client_mutex = Mutex.new
42
+ @uri = URI.parse(uris)
43
+ configure_configurable options
44
+ create_client_pool
45
+ @active_client = nil
46
+ @connection = nil
47
+ @frame_buffer = buffer.new self
48
+ @disconnecting = false
49
+ @client_ready = false
50
+ end
51
+
52
+ # Returns true if there is an {#active_client} and it is
53
+ # {OnStomp::Client#connected? connected}.
54
+ # @return [true,false,nil]
55
+ def connected?
56
+ active_client && active_client.connected?
57
+ end
58
+
59
+ # Transmits a frame to the {#active_client} if one exists.
60
+ # @return [OnStomp::Components::Frame,nil]
61
+ def transmit frame, cbs={}
62
+ active_client && active_client.transmit(frame, cbs)
63
+ end
64
+
65
+ # Connects to one of the clients in the {#client_pool}
66
+ # @return [self]
67
+ def connect
68
+ @disconnecting = false
69
+ unless reconnect
70
+ raise OnStomp::Failover::MaximumRetriesExceededError
71
+ end
72
+ self
73
+ end
74
+
75
+ # Ensures that a connection is properly established, then invokes
76
+ # {OnStomp::Client#disconnect disconnect} on the {#active_client}
77
+ def disconnect *args, &block
78
+ return unless active_client
79
+ @disconnecting = true
80
+ Thread.pass until @client_ready
81
+ active_client.disconnect *args, &block
82
+ end
83
+
84
+ private
85
+ def reconnect
86
+ @client_mutex.synchronize do
87
+ @client_ready = false
88
+ attempt = 1
89
+ until connected? || retry_exceeded?(attempt)
90
+ sleep_for_retry attempt
91
+ begin
92
+ trigger_failover_retry :before, attempt
93
+ @active_client = client_pool.next_client
94
+ # +reconnect+ could be called again within the marked range.
95
+ active_client.connect # <--- From here
96
+ @connection = active_client.connection
97
+ rescue Exception
98
+ trigger_failover_event :connect_failure, :on, active_client, $!.message
99
+ end
100
+ trigger_failover_retry :after, attempt
101
+ attempt += 1
102
+ end
103
+ connected?.tap do |b|
104
+ b && trigger_failover_event(:connected, :on, active_client)
105
+ @client_ready = b
106
+ end # <--- Until here
107
+ end
108
+ end
109
+
110
+ def retry_exceeded? attempt
111
+ retry_attempts > 0 && attempt > retry_attempts
112
+ end
113
+
114
+ def sleep_for_retry attempt
115
+ sleep(retry_delay) if retry_delay > 0 && attempt > 1
116
+ end
117
+
118
+ def create_client_pool
119
+ @client_pool = pool.new(uri.failover_uris)
120
+ on_connection_closed do |client, *_|
121
+ unless @disconnecting
122
+ trigger_failover_event(:lost, :on, active_client)
123
+ reconnect
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,63 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Module for configurable attributes specific to
4
+ # {OnStomp::Failover::Client failover} clients.
5
+ module OnStomp::Failover::FailoverConfigurable
6
+ # Includes {OnStomp::Interfaces::ClientConfigurable} into +base+ and
7
+ # extends {OnStomp::Failover::FailoverConfigurable::ClassMethods}
8
+ # @param [Module] base
9
+ def self.included(base)
10
+ base.__send__ :include, OnStomp::Interfaces::ClientConfigurable
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ # Provides attribute methods for {OnStomp::Failover::Client failover}
15
+ # clients.
16
+ module ClassMethods
17
+ # Creates readable and writeable attributes that are automatically
18
+ # converted into integers.
19
+ def attr_configurable_int *args, &block
20
+ trans = attr_configurable_wrap lambda { |v| v.to_i }, block
21
+ attr_configurable_single(*args, &trans)
22
+ end
23
+
24
+ # Creates readable and writeable attributes that are automatically
25
+ # converted into boolean values. Assigning the attributes any of
26
+ # +true+, +'true'+, +'1'+ or +1+ will set the attribute to +true+, all
27
+ # other values with be treated as +false+. This method will also alias
28
+ # the reader methods with +attr_name?+
29
+ def attr_configurable_bool *args, &block
30
+ trans = attr_configurable_wrap lambda { |v|
31
+ [true, 'true', '1', 1].include?(v) }, block
32
+ attr_configurable_single(*args, &trans)
33
+ args.each do |a|
34
+ unless a.is_a?(Hash)
35
+ alias_method :"#{a}?", a
36
+ end
37
+ end
38
+ end
39
+
40
+ # Creates a readable and writeable attribute with the given name that
41
+ # defaults to the {OnStomp::Failover::Pools::RoundRobin}. Corresponds
42
+ # the the class to use when creating new
43
+ # {OnStomp::Failover::Client#client_pool client pools}.
44
+ # @param [Symbol] nm name of attribute
45
+ def attr_configurable_pool nm
46
+ attr_configurable_class(nm,
47
+ :default => OnStomp::Failover::Pools::RoundRobin) do |p|
48
+ p || OnStomp::Failover::Pools::RoundRobin
49
+ end
50
+ end
51
+
52
+ # Creates a readable and writeable attribute with the given name that
53
+ # defaults to the {OnStomp::Failover::Buffers::Written}. Corresponds
54
+ # the the class to use for frame buffering and de-buffering.
55
+ # @param [Symbol] nm name of attribute
56
+ def attr_configurable_buffer nm
57
+ attr_configurable_class(nm,
58
+ :default => OnStomp::Failover::Buffers::Written) do |b|
59
+ b || OnStomp::Failover::Buffers::Written
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,96 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Events mixin for {OnStomp::Failover::Client failover} clients.
4
+ module OnStomp::Failover::FailoverEvents
5
+ include OnStomp::Interfaces::EventManager
6
+
7
+ # We do this one using +class << self+ instead of the +self.included+ hook
8
+ # because we need 'create_client_event_method+ immediately.
9
+ class << self
10
+ # Creates a forwarded binding for client events.
11
+ def create_client_event_method name
12
+ module_eval "def #{name}(&block); bind_client_event(:#{name}, block); end"
13
+ end
14
+ end
15
+
16
+ # Create forwarded bindings for all {OnStomp::Client} events.
17
+ OnStomp::Interfaces::ClientEvents.event_methods.each do |ev|
18
+ create_client_event_method ev
19
+ end
20
+
21
+ # Binds a callback to {OnStomp::Client#on_connction_established}. This has
22
+ # to be handled directly because :on_connection_established isn't a true
23
+ # event.
24
+ def on_connection_established &block
25
+ bind_client_event(:on_connection_established, block)
26
+ end
27
+ # Binds a callback to {OnStomp::Client#on_connection_died}. This has
28
+ # to be handled directly because :on_connection_died isn't a true
29
+ # event.
30
+ def on_connection_died &block
31
+ bind_client_event(:on_connection_died, block)
32
+ end
33
+ # Binds a callback to {OnStomp::Client#on_connection_terminated}. This has
34
+ # to be handled directly because :on_connection_terminated isn't a true
35
+ # event.
36
+ def on_connection_terminated &block
37
+ bind_client_event(:on_connection_terminated, block)
38
+ end
39
+ # Binds a callback to {OnStomp::Client#on_connection_closed}. This has
40
+ # to be handled directly because :on_connection_closed isn't a true
41
+ # event.
42
+ def on_connection_closed &block
43
+ bind_client_event(:on_connection_closed, block)
44
+ end
45
+
46
+ # Sets up a forwarded event binding, applying it to all clients in
47
+ # {OnStomp::Failover::Client#client_pool}.
48
+ def bind_client_event(name, block)
49
+ client_pool.each do |client|
50
+ client.__send__ name do |*args|
51
+ if client == active_client
52
+ block.call *args
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Binds a callback to be invoked when a failover client is attempting to
59
+ # connect through a new {OnStomp::Client client} in its
60
+ # {OnStomp::Failover::Client#pool}.
61
+ # @yield [failover, attempt, client] callback invoked when event is triggered
62
+ # @yieldparam [OnStomp::Failover::Client] failover
63
+ # @yieldparam [Fixnum] attempt
64
+ # @yieldparam [OnStomp::Client] client
65
+ create_event_methods :failover_retry, :before, :after
66
+ # Binds a callback to be invoked when a failover client fails to establish
67
+ # a connection through a {OnStomp::Client client} while reconnecting.
68
+ # @yield [failover, client, error_message] callback invoked when event is triggered
69
+ # @yieldparam [OnStomp::Failover::Client] failover
70
+ # @yieldparam [OnStomp::Client] client
71
+ # @yieldparam [String] error_message
72
+ create_event_methods :failover_connect_failure, :on
73
+ #create_event_methods :failover_retries_exceeded, :on
74
+ # Binds a callback to be invoked when an established connection through a
75
+ # client is lost.
76
+ # @yield [failover, client] callback invoked when event is triggered
77
+ # @yieldparam [OnStomp::Failover::Client] failover
78
+ # @yieldparam [OnStomp::Client] client
79
+ create_event_methods :failover_lost, :on
80
+ # Binds a callback to be invoked when a connection through a
81
+ # client is established.
82
+ # @yield [failover, client] callback invoked when event is triggered
83
+ # @yieldparam [OnStomp::Failover::Client] failover
84
+ # @yieldparam [OnStomp::Client] client
85
+ create_event_methods :failover_connected, :on
86
+
87
+ # Triggers a failover retry event
88
+ def trigger_failover_retry pref, attempt
89
+ trigger_failover_event :retry, pref, attempt, self.active_client
90
+ end
91
+
92
+ # Triggers a general failover event
93
+ def trigger_failover_event ev, pref, *args
94
+ trigger_event :"#{pref}_failover_#{ev}", self, *args
95
+ end
96
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class OnStomp::Client
4
+ class << self
5
+ # Creates an alias chain for {OnStomp::Client.new} so that if
6
+ # a failover: URI or an array of URIs are passed to the constructor,
7
+ # a {OnStomp::Failover::Client failover} client is built instead.
8
+ # @return [OnStomp::Client,OnStomp::Failover::Client]
9
+ def new_with_failover(uri, options={})
10
+ if uri.is_a?(Array) || uri.to_s =~ /^failover:/i
11
+ OnStomp::Failover::Client.new(uri, options)
12
+ else
13
+ new_without_failover(uri, options)
14
+ end
15
+ end
16
+
17
+ alias :new_without_failover :new
18
+ alias :new :new_with_failover
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Namespace for client pool managers.
4
+ module OnStomp::Failover::Pools
5
+ end
6
+
7
+ require 'onstomp/failover/pools/base'
8
+ require 'onstomp/failover/pools/round_robin'
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # An abstract pool of clients. This class manages the shared behaviors
4
+ # of client pools, but has no means of picking successive clients.
5
+ # Subclasses must define +next_client+ or pool will not function.
6
+ class OnStomp::Failover::Pools::Base
7
+ attr_reader :clients
8
+
9
+ # Creates a new client pool by mapping an array of URIs into an array of
10
+ # {OnStomp::Client clients}.
11
+ def initialize uris
12
+ @clients = uris.map do |u|
13
+ OnStomp::Client.new u
14
+ end
15
+ end
16
+
17
+ # Raises an error, because it is up to subclasses to define this behavior.
18
+ # @raise [StandardError]
19
+ def next_client
20
+ raise 'implemented in subclasses'
21
+ end
22
+
23
+ # Shuffles the client pool.
24
+ def shuffle!
25
+ clients.shuffle!
26
+ end
27
+
28
+ # Yields each client in the pool to the supplied block. Raises an error
29
+ # if no block is provided.
30
+ # @raise [ArgumentError] if no block is given
31
+ # @yield [client] block to call for each client in the pool
32
+ # @yieldparam [OnStomp::Client] client
33
+ # @return [self]
34
+ def each &block
35
+ raise ArgumentError, 'no block provided' unless block_given?
36
+ clients.each &block
37
+ self
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A round-robin client pool. Clients are processed sequentially, and once
4
+ # all clients have been processed, the pool cycles back to the beginning.
5
+ class OnStomp::Failover::Pools::RoundRobin < OnStomp::Failover::Pools::Base
6
+ def initialize uris
7
+ super
8
+ @index = -1
9
+ end
10
+
11
+ # Returns the next sequential client in the pool
12
+ # @return [OnStomp::Client]
13
+ def next_client
14
+ @index = (@index + 1) % clients.size
15
+ clients[@index]
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Namespace for failover related URI classes.
4
+ module OnStomp::Failover::URI
5
+ # A URI class for representing URIs with a 'failover' scheme.
6
+ class FAILOVER < OnStomp::Components::URI::STOMP
7
+ # Matches the internal URIs and query contained in
8
+ # the +opaque+ part of a failover: URI
9
+ FAILOVER_OPAQUE_REG = /^\(([^\)]+)\)(?:\?(.*))?/
10
+
11
+ attr_reader :failover_uris
12
+ def initialize(*args)
13
+ super
14
+ _split_opaque_
15
+ end
16
+
17
+ private
18
+ def _split_opaque_
19
+ if opaque =~ FAILOVER_OPAQUE_REG
20
+ furis, fquery = $1, $2
21
+ @failover_uris = furis.split(',').map { |u| ::URI.parse(u.strip) }
22
+ self.set_opaque nil
23
+ self.set_path ''
24
+ self.set_query fquery
25
+ else
26
+ raise OnStomp::Failover::InvalidFailoverURIError, self.to_s
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ module ::URI
33
+ @@schemes['FAILOVER'] = OnStomp::Failover::URI::FAILOVER
34
+ end
@@ -40,16 +40,12 @@ module OnStomp::Interfaces::ClientConfigurable
40
40
  end
41
41
 
42
42
  # Creates a readable and writeable attribute with the given name that
43
- # defaults to the {OnStomp::Components::ThreadedProcessor} and if set
44
- # to nil will instead use {OnStomp::Components::NilProcessor}. Corresponds
43
+ # defaults to the {OnStomp::Components::ThreadedProcessor}. Corresponds
45
44
  # the the class to use when create new processor instances when a client
46
45
  # is connected.
47
46
  # @param [Symbol] nm name of attribute
48
47
  def attr_configurable_processor nm
49
- attr_configurable_class(nm,
50
- :default => OnStomp::Components::ThreadedProcessor) do |pr|
51
- pr || OnStomp::Components::NilProcessor
52
- end
48
+ attr_configurable_class(nm, :default => OnStomp::Components::ThreadedProcessor)
53
49
  end
54
50
  end
55
51
  end
@@ -12,6 +12,10 @@ module OnStomp::Interfaces::ClientEvents
12
12
 
13
13
  # @group Client Frame Event Bindings
14
14
 
15
+ # Can't get +before+ because the CONNECT frame isn't transmitted by
16
+ # the client.
17
+ create_event_methods :connect, :on
18
+
15
19
  # Binds a callback to be invoked when an ACK frame is transmitted
16
20
  # @yield [frame, client] callback invoked when event is triggered
17
21
  # @yieldparam [OnStomp::Components::Frame] frame
@@ -42,8 +42,8 @@ module OnStomp::Interfaces::ConnectionEvents
42
42
 
43
43
  # Triggers a connection specific event.
44
44
  # @param [Symbol] event name
45
- def trigger_connection_event event
46
- trigger_event :"on_#{event}", self.client, self
45
+ def trigger_connection_event event, msg=''
46
+ trigger_event :"on_#{event}", self.client, self, msg
47
47
  end
48
48
 
49
49
  # Takes a hash of event bindings a {OnStomp::Client client} has stored
@@ -57,6 +57,6 @@ module OnStomp::Interfaces::ConnectionEvents
57
57
  ev_hash.each do |ev, cbs|
58
58
  cbs.each { |cb| bind_event(ev, cb) }
59
59
  end
60
- trigger_connection_event :established
60
+ trigger_connection_event :established, "STOMP #{self.version} connection negotiated"
61
61
  end
62
62
  end