lepus 0.0.1.beta2

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/specs.yml +44 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +35 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +10 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +120 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +213 -0
  12. data/Rakefile +4 -0
  13. data/bin/console +9 -0
  14. data/bin/setup +7 -0
  15. data/docker-compose.yml +8 -0
  16. data/exec/lepus +9 -0
  17. data/gemfiles/rails52.gemfile +5 -0
  18. data/gemfiles/rails52.gemfile.lock +242 -0
  19. data/gemfiles/rails61.gemfile +5 -0
  20. data/gemfiles/rails61.gemfile.lock +260 -0
  21. data/lepus.gemspec +53 -0
  22. data/lib/lepus/app_executor.rb +19 -0
  23. data/lib/lepus/cli.rb +27 -0
  24. data/lib/lepus/configuration.rb +90 -0
  25. data/lib/lepus/consumer.rb +177 -0
  26. data/lib/lepus/consumer_config.rb +149 -0
  27. data/lib/lepus/consumer_wrapper.rb +46 -0
  28. data/lib/lepus/lifecycle_hooks.rb +49 -0
  29. data/lib/lepus/message.rb +37 -0
  30. data/lib/lepus/middleware.rb +18 -0
  31. data/lib/lepus/middlewares/honeybadger.rb +23 -0
  32. data/lib/lepus/middlewares/json.rb +35 -0
  33. data/lib/lepus/middlewares/max_retry.rb +57 -0
  34. data/lib/lepus/primitive/string.rb +55 -0
  35. data/lib/lepus/process.rb +136 -0
  36. data/lib/lepus/process_registry.rb +37 -0
  37. data/lib/lepus/processes/base.rb +50 -0
  38. data/lib/lepus/processes/callbacks.rb +72 -0
  39. data/lib/lepus/processes/consumer.rb +113 -0
  40. data/lib/lepus/processes/interruptible.rb +38 -0
  41. data/lib/lepus/processes/procline.rb +11 -0
  42. data/lib/lepus/processes/registrable.rb +67 -0
  43. data/lib/lepus/processes/runnable.rb +102 -0
  44. data/lib/lepus/processes/supervised.rb +44 -0
  45. data/lib/lepus/processes.rb +6 -0
  46. data/lib/lepus/producer.rb +42 -0
  47. data/lib/lepus/rails/log_subscriber.rb +120 -0
  48. data/lib/lepus/rails/railtie.rb +31 -0
  49. data/lib/lepus/rails.rb +7 -0
  50. data/lib/lepus/supervisor/config.rb +45 -0
  51. data/lib/lepus/supervisor/maintenance.rb +35 -0
  52. data/lib/lepus/supervisor/pidfile.rb +61 -0
  53. data/lib/lepus/supervisor/pidfiled.rb +29 -0
  54. data/lib/lepus/supervisor/signals.rb +71 -0
  55. data/lib/lepus/supervisor.rb +204 -0
  56. data/lib/lepus/timer.rb +29 -0
  57. data/lib/lepus/version.rb +5 -0
  58. data/lib/lepus.rb +95 -0
  59. data/lib/puma/plugin/lepus.rb +74 -0
  60. metadata +290 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Producer
5
+ DEFAULT_EXCHANGE_OPTIONS = {
6
+ type: :topic,
7
+ durable: true,
8
+ auto_delete: false
9
+ }.freeze
10
+
11
+ DEFAULT_PUBLISH_OPTIONS = {
12
+ expiration: 7 * (60 * 60 * 24)
13
+ }.freeze
14
+
15
+ def initialize(exchange_name, **options)
16
+ @exchange_name = exchange_name
17
+ @exchange_options = DEFAULT_EXCHANGE_OPTIONS.merge(options)
18
+ end
19
+
20
+ def publish(message, **options)
21
+ payload = if message.is_a?(String)
22
+ options[:content_type] ||= "text/plain"
23
+ message
24
+ else
25
+ options[:content_type] ||= "application/json"
26
+ MultiJson.dump(message)
27
+ end
28
+
29
+ bunny.with_channel do |channel|
30
+ exchange = channel.exchange(@exchange_name, @exchange_options)
31
+ exchange.publish(
32
+ payload,
33
+ DEFAULT_PUBLISH_OPTIONS.merge(options)
34
+ )
35
+ end
36
+ end
37
+
38
+ def bunny
39
+ Thread.current[:lepus_bunny] ||= Lepus.config.create_connection(suffix: "producer")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lepus::LogSubscriber < ActiveSupport::LogSubscriber
4
+ def start_process(event)
5
+ process = event.payload[:process]
6
+
7
+ attributes = {
8
+ pid: process.pid,
9
+ hostname: process.hostname,
10
+ process_id: process.process_id,
11
+ name: process.name
12
+ }.merge(process.metadata)
13
+
14
+ info formatted_event(event, action: "Started #{process.kind}", **attributes)
15
+ end
16
+
17
+ def shutdown_process(event)
18
+ process = event.payload[:process]
19
+
20
+ attributes = {
21
+ pid: process.pid,
22
+ hostname: process.hostname,
23
+ process_id: process.process_id,
24
+ name: process.name
25
+ }.merge(process.metadata)
26
+
27
+ info formatted_event(event, action: "Shutdown #{process.kind}", **attributes)
28
+ end
29
+
30
+ def register_process(event)
31
+ process_kind = event.payload[:kind]
32
+ attributes = event.payload.slice(:pid, :hostname, :process_id, :name)
33
+
34
+ if (error = event.payload[:error])
35
+ warn formatted_event(event, action: "Error registering #{process_kind}", **attributes.merge(error: formatted_error(error)))
36
+ else
37
+ debug formatted_event(event, action: "Register #{process_kind}", **attributes)
38
+ end
39
+ end
40
+
41
+ def deregister_process(event)
42
+ process = event.payload[:process]
43
+
44
+ attributes = {
45
+ process_id: process.id,
46
+ pid: process.pid,
47
+ hostname: process.hostname,
48
+ name: process.name,
49
+ last_heartbeat_at: process.last_heartbeat_at&.strftime("%Y-%m-%d %H:%M:%S"),
50
+ claimed_size: event.payload[:claimed_size],
51
+ pruned: event.payload[:pruned]
52
+ }
53
+
54
+ if (error = event.payload[:error])
55
+ warn formatted_event(event, action: "Error deregistering #{process.kind}", **attributes.merge(error: formatted_error(error)))
56
+ else
57
+ debug formatted_event(event, action: "Deregister #{process.kind}", **attributes)
58
+ end
59
+ end
60
+
61
+ def prune_processes(event)
62
+ debug formatted_event(event, action: "Prune dead processes", **event.payload.slice(:size))
63
+ end
64
+
65
+ def thread_error(event)
66
+ error formatted_event(event, action: "Error in thread", error: formatted_error(event.payload[:error]))
67
+ end
68
+
69
+ def graceful_termination(event)
70
+ attributes = event.payload.slice(:process_id, :supervisor_pid, :supervised_processes)
71
+
72
+ if event.payload[:shutdown_timeout_exceeded]
73
+ warn formatted_event(event, action: "Supervisor wasn't terminated gracefully - shutdown timeout exceeded", **attributes)
74
+ else
75
+ info formatted_event(event, action: "Supervisor terminated gracefully", **attributes)
76
+ end
77
+ end
78
+
79
+ def immediate_termination(event)
80
+ info formatted_event(event, action: "Supervisor terminated immediately", **event.payload.slice(:process_id, :supervisor_pid, :supervised_processes))
81
+ end
82
+
83
+ def unhandled_signal_error(event)
84
+ error formatted_event(event, action: "Received unhandled signal", **event.payload.slice(:signal))
85
+ end
86
+
87
+ def replace_fork(event)
88
+ status = event.payload[:status]
89
+ attributes = event.payload.slice(:pid).merge \
90
+ status: status.exitstatus || "no exit status set",
91
+ pid_from_status: status.pid,
92
+ signaled: status.signaled?,
93
+ stopsig: status.stopsig,
94
+ termsig: status.termsig
95
+
96
+ if (replaced_fork = event.payload[:fork])
97
+ info formatted_event(event, action: "Replaced terminated #{replaced_fork.kind}", **attributes.merge(hostname: replaced_fork.hostname, name: replaced_fork.name))
98
+ else
99
+ warn formatted_event(event, action: "Tried to replace forked process but it had already died", **attributes)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def formatted_event(event, action:, **attributes)
106
+ "Lepus-#{Lepus::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
107
+ end
108
+
109
+ def formatted_attributes(**attributes)
110
+ attributes.map { |attr, value| "#{attr}: #{value.inspect}" }.join(", ")
111
+ end
112
+
113
+ def formatted_error(error)
114
+ [error.class, error.message].compact.join(" ")
115
+ end
116
+
117
+ def logger
118
+ Lepus.logger
119
+ end
120
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Railtie < ::Rails::Railtie
5
+ config.lepus = ActiveSupport::OrderedOptions.new
6
+ config.lepus.app_executor = nil
7
+ config.lepus.on_thread_error = nil
8
+
9
+ initializer "lepus.app_executor", before: :run_prepare_callbacks do |app|
10
+ config.lepus.app_executor ||= app.executor
11
+ if ::Rails.respond_to?(:error) && config.lepus.on_thread_error.nil?
12
+ config.lepus.on_thread_error = ->(exception) { ::Rails.error.report(exception, handled: false) }
13
+ elsif config.lepus.on_thread_error.nil?
14
+ config.lepus.on_thread_error = ->(exception) { Lepus.logger.error(exception) }
15
+ end
16
+
17
+ Lepus.config.app_executor = config.lepus.app_executor
18
+ Lepus.config.on_thread_error = config.lepus.on_thread_error
19
+ end
20
+
21
+ initializer "lepus.logger" do
22
+ ActiveSupport.on_load(:lepus) do
23
+ self.logger = ::Rails.logger if logger == Lepus::DEFAULT_LOGGER
24
+ end
25
+
26
+ Lepus::LogSubscriber.attach_to :lepus
27
+ end
28
+ end
29
+
30
+ ActiveSupport.run_load_hooks(:lepus, self)
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+ require "active_support/log_subscriber"
5
+
6
+ require_relative "rails/log_subscriber"
7
+ require_relative "rails/railtie"
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "logger"
5
+
6
+ module Lepus
7
+ class Supervisor < Processes::Base
8
+ class Config
9
+ class ProcessStruct < Struct.new(:process_class, :attributes)
10
+ def instantiate
11
+ process_class.new(**attributes)
12
+ end
13
+ end
14
+
15
+ attr_accessor :pidfile, :require_file
16
+
17
+ def initialize(require_file: nil, pidfile: "tmp/pids/lepus.pid", **kwargs)
18
+ @pidfile = pidfile
19
+ @require_file = require_file
20
+ self.consumers = kwargs[:consumers] if kwargs.key?(:consumers)
21
+ end
22
+
23
+ def configured_processes
24
+ consumer_processes
25
+ end
26
+
27
+ def consumers=(vals)
28
+ @consumer_processes = nil
29
+ @consumers = Array(vals).map(&:to_s)
30
+ end
31
+
32
+ def consumers
33
+ @consumers ||= Lepus::Consumer.descendants.reject(&:abstract_class?).map(&:name).compact
34
+ end
35
+
36
+ protected
37
+
38
+ def consumer_processes
39
+ @consumer_processes ||= consumers.map do |class_name|
40
+ ProcessStruct.new(Lepus::Processes::Consumer, {class_name: class_name})
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ module Lepus
2
+ class Supervisor < Processes::Base
3
+ module Maintenance
4
+ def self.included(base)
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ private
10
+
11
+ def launch_maintenance_task
12
+ @maintenance_task = ::Concurrent::TimerTask.new(run_now: true, execution_interval: Lepus.config.process_alive_threshold) do
13
+ prune_dead_processes
14
+ end
15
+
16
+ @maintenance_task.add_observer do |_, _, error|
17
+ handle_thread_error(error) if error
18
+ end
19
+
20
+ @maintenance_task.execute
21
+ end
22
+
23
+ def stop_maintenance_task
24
+ @maintenance_task&.shutdown
25
+ end
26
+
27
+ def prune_dead_processes
28
+ wrap_in_app_executor do
29
+ Lepus::Process.prune(excluding: process)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Supervisor < Processes::Base
5
+ class Pidfile
6
+ def initialize(path)
7
+ @path = path
8
+ @pid = ::Process.pid
9
+ end
10
+
11
+ def setup
12
+ check_status
13
+ write_file
14
+ set_at_exit_hook
15
+ end
16
+
17
+ def delete
18
+ delete_file
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :path, :pid
24
+
25
+ def check_status
26
+ if ::File.exist?(path)
27
+ existing_pid = ::File.read(path).strip.to_i
28
+ existing_pid > 0 && ::Process.kill(0, existing_pid)
29
+
30
+ already_running!
31
+ else
32
+ FileUtils.mkdir_p File.dirname(path)
33
+ end
34
+ rescue Errno::ESRCH
35
+ # Process is dead, ignore, just delete the file
36
+ delete
37
+ rescue Errno::EPERM
38
+ already_running!
39
+ end
40
+
41
+ def write_file
42
+ ::File.open(path, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |file| file.write(pid.to_s) }
43
+ rescue Errno::EEXIST
44
+ check_status
45
+ retry
46
+ end
47
+
48
+ def set_at_exit_hook
49
+ at_exit { delete if ::Process.pid == pid }
50
+ end
51
+
52
+ def delete_file
53
+ ::File.delete(path) if ::File.exist?(path)
54
+ end
55
+
56
+ def already_running!
57
+ abort "A supervisor is already running. Check #{path}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Supervisor < Processes::Base
5
+ module Pidfiled
6
+ def self.included(base)
7
+ base.send :include, InstanceMethods
8
+ base.class_eval do
9
+ before_boot :setup_pidfile
10
+ after_shutdown :delete_pidfile
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+ private
16
+
17
+ def setup_pidfile
18
+ if (path = configuration.pidfile)
19
+ @pidfile = Pidfile.new(path).tap(&:setup)
20
+ end
21
+ end
22
+
23
+ def delete_pidfile
24
+ @pidfile&.delete
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Supervisor < Processes::Base
5
+ module Signals
6
+ def self.included(base)
7
+ base.send :include, InstanceMethods
8
+ base.class_eval do
9
+ before_boot :register_signal_handlers
10
+ after_shutdown :restore_default_signal_handlers
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+ private
16
+
17
+ SIGNALS = %i[QUIT INT TERM]
18
+
19
+ def register_signal_handlers
20
+ SIGNALS.each do |signal|
21
+ trap(signal) do
22
+ signal_queue << signal
23
+ interrupt
24
+ end
25
+ end
26
+ end
27
+
28
+ def restore_default_signal_handlers
29
+ SIGNALS.each do |signal|
30
+ trap(signal, :DEFAULT)
31
+ end
32
+ end
33
+
34
+ def process_signal_queue
35
+ while (signal = signal_queue.shift)
36
+ handle_signal(signal)
37
+ end
38
+ end
39
+
40
+ def handle_signal(signal)
41
+ case signal
42
+ when :TERM, :INT
43
+ stop
44
+ terminate_gracefully
45
+ when :QUIT
46
+ stop
47
+ terminate_immediately
48
+ else
49
+ Lepus.instrument :unhandled_signal_error, signal: signal
50
+ end
51
+ end
52
+
53
+ def signal_processes(pids, signal)
54
+ pids.each do |pid|
55
+ signal_process pid, signal
56
+ end
57
+ end
58
+
59
+ def signal_process(pid, signal)
60
+ ::Process.kill signal, pid
61
+ rescue Errno::ESRCH
62
+ # Ignore, process died before
63
+ end
64
+
65
+ def signal_queue
66
+ @signal_queue ||= []
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Supervisor < Processes::Base
5
+ include LifecycleHooks
6
+ include Maintenance
7
+ include Signals
8
+ include Pidfiled
9
+
10
+ class << self
11
+ def start(**options)
12
+ # Lepus.config.supervisor = true
13
+ config = Config.new(**options)
14
+ new(config).tap(&:start)
15
+ end
16
+ end
17
+
18
+ def initialize(configuration)
19
+ @configuration = configuration
20
+ @forks = {}
21
+ @configured_processes = {}
22
+ ProcessRegistry.instance # Ensure the registry is initialized
23
+ end
24
+
25
+ def start
26
+ boot
27
+
28
+ run_start_hooks
29
+
30
+ start_processes
31
+ launch_maintenance_task
32
+
33
+ supervise
34
+ end
35
+
36
+ def stop
37
+ super
38
+ run_stop_hooks
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :configuration, :forks, :configured_processes
44
+
45
+ def boot
46
+ Lepus.instrument(:start_process, process: self) do
47
+ if configuration.require_file
48
+ Kernel.require configuration.require_file
49
+ else
50
+ begin
51
+ require "rails"
52
+ require_relative "rails"
53
+ require File.expand_path("config/environment", Dir.pwd)
54
+ rescue LoadError
55
+ # Rails not found
56
+ end
57
+ end
58
+
59
+ setup_consumers
60
+ check_bunny_connection
61
+
62
+ run_process_callbacks(:boot) do
63
+ sync_std_streams
64
+ end
65
+ end
66
+ end
67
+
68
+ def setup_consumers
69
+ Lepus.eager_load_consumers!
70
+
71
+ if configuration.consumers.empty?
72
+ abort "No consumers found. Exiting..."
73
+ end
74
+ end
75
+
76
+ def check_bunny_connection
77
+ temp_bunny = Lepus.config.create_connection(suffix: "(boot-check)")
78
+ temp_bunny.close
79
+ end
80
+
81
+ def start_processes
82
+ configuration.configured_processes.each do |configured_process|
83
+ start_process(configured_process)
84
+ end
85
+ end
86
+
87
+ def supervise
88
+ loop do
89
+ break if stopped?
90
+
91
+ set_procline
92
+ process_signal_queue
93
+
94
+ unless stopped?
95
+ reap_and_replace_terminated_forks
96
+ interruptible_sleep(1)
97
+ end
98
+ end
99
+ ensure
100
+ shutdown
101
+ end
102
+
103
+ def start_process(configured_process)
104
+ process_instance = configured_process.instantiate.tap do |instance|
105
+ instance.supervised_by process
106
+ instance.mode = :fork
107
+ end
108
+
109
+ process_instance.before_fork
110
+ pid = fork do
111
+ process_instance.after_fork
112
+ process_instance.start
113
+ end
114
+
115
+ configured_processes[pid] = configured_process
116
+ forks[pid] = process_instance
117
+ end
118
+
119
+ def set_procline
120
+ procline "supervising #{supervised_processes.join(", ")}"
121
+ end
122
+
123
+ def terminate_gracefully
124
+ Lepus.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do |payload|
125
+ term_forks
126
+
127
+ shutdown_timeout = 5
128
+ puts "\nWaiting up to #{shutdown_timeout} seconds for processes to terminate gracefully..."
129
+ Timer.wait_until(shutdown_timeout, -> { all_forks_terminated? }) do
130
+ reap_terminated_forks
131
+ end
132
+
133
+ unless all_forks_terminated?
134
+ payload[:shutdown_timeout_exceeded] = true
135
+ terminate_immediately
136
+ end
137
+ end
138
+ end
139
+
140
+ def terminate_immediately
141
+ Lepus.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do
142
+ quit_forks
143
+ end
144
+ end
145
+
146
+ def shutdown
147
+ Lepus.instrument(:shutdown_process, process: self) do
148
+ run_process_callbacks(:shutdown) do
149
+ stop_maintenance_task
150
+ end
151
+ end
152
+ end
153
+
154
+ def sync_std_streams
155
+ $stdout.sync = $stderr.sync = true
156
+ end
157
+
158
+ def supervised_processes
159
+ forks.keys
160
+ end
161
+
162
+ def term_forks
163
+ signal_processes(forks.keys, :TERM)
164
+ end
165
+
166
+ def quit_forks
167
+ signal_processes(forks.keys, :QUIT)
168
+ end
169
+
170
+ def reap_and_replace_terminated_forks
171
+ loop do
172
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
173
+ break unless pid
174
+
175
+ replace_fork(pid, status)
176
+ end
177
+ end
178
+
179
+ def reap_terminated_forks
180
+ loop do
181
+ pid, _ = ::Process.waitpid2(-1, ::Process::WNOHANG)
182
+ break unless pid
183
+
184
+ configured_processes.delete(pid)
185
+ end
186
+ rescue SystemCallError
187
+ # All children already reaped
188
+ end
189
+
190
+ def replace_fork(pid, status)
191
+ Lepus.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
192
+ if (terminated_fork = forks.delete(pid))
193
+ payload[:fork] = terminated_fork
194
+
195
+ start_process(configured_processes.delete(pid))
196
+ end
197
+ end
198
+ end
199
+
200
+ def all_forks_terminated?
201
+ forks.empty?
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Timer
5
+ extend self
6
+
7
+ def wait_until(timeout, condition, &block)
8
+ if timeout > 0
9
+ deadline = monotonic_time_now + timeout
10
+
11
+ while monotonic_time_now < deadline && !condition.call
12
+ sleep 0.1
13
+ yield
14
+ end
15
+ else
16
+ until condition.call
17
+ sleep 0.5
18
+ yield
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def monotonic_time_now
26
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ VERSION = "0.0.1.beta2"
5
+ end