wamp-worker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +204 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/bin/wamp-worker +46 -0
  13. data/lib/wamp/worker.rb +132 -0
  14. data/lib/wamp/worker/config.rb +184 -0
  15. data/lib/wamp/worker/handler.rb +196 -0
  16. data/lib/wamp/worker/proxy/backgrounder.rb +38 -0
  17. data/lib/wamp/worker/proxy/base.rb +101 -0
  18. data/lib/wamp/worker/proxy/dispatcher.rb +115 -0
  19. data/lib/wamp/worker/proxy/requestor.rb +91 -0
  20. data/lib/wamp/worker/queue.rb +135 -0
  21. data/lib/wamp/worker/rails.rb +28 -0
  22. data/lib/wamp/worker/runner.rb +240 -0
  23. data/lib/wamp/worker/ticker.rb +30 -0
  24. data/lib/wamp/worker/version.rb +5 -0
  25. data/spec/spec_helper.rb +29 -0
  26. data/spec/support/client_stub.rb +47 -0
  27. data/spec/support/handler_stub.rb +105 -0
  28. data/spec/support/redis_stub.rb +89 -0
  29. data/spec/support/session_stub.rb +101 -0
  30. data/spec/wamp/worker/config_spec.rb +90 -0
  31. data/spec/wamp/worker/handler_spec.rb +162 -0
  32. data/spec/wamp/worker/proxy_spec.rb +153 -0
  33. data/spec/wamp/worker/queue_spec.rb +49 -0
  34. data/spec/wamp/worker/runner_spec.rb +108 -0
  35. data/spec/wamp/worker_spec.rb +8 -0
  36. data/test/app_test.rb +124 -0
  37. data/test/hello.py +50 -0
  38. data/test/sidekiq.yml +5 -0
  39. data/test/wamp_test/.generators +8 -0
  40. data/test/wamp_test/.ruby-version +1 -0
  41. data/test/wamp_test/Gemfile +65 -0
  42. data/test/wamp_test/Gemfile.lock +246 -0
  43. data/test/wamp_test/README.md +24 -0
  44. data/test/wamp_test/Rakefile +6 -0
  45. data/test/wamp_test/app/assets/config/manifest.js +3 -0
  46. data/test/wamp_test/app/assets/images/.keep +0 -0
  47. data/test/wamp_test/app/assets/javascripts/application.js +16 -0
  48. data/test/wamp_test/app/assets/javascripts/cable.js +13 -0
  49. data/test/wamp_test/app/assets/javascripts/channels/.keep +0 -0
  50. data/test/wamp_test/app/assets/stylesheets/application.css +15 -0
  51. data/test/wamp_test/app/channels/application_cable/channel.rb +4 -0
  52. data/test/wamp_test/app/channels/application_cable/connection.rb +4 -0
  53. data/test/wamp_test/app/controllers/add_controller.rb +11 -0
  54. data/test/wamp_test/app/controllers/application_controller.rb +3 -0
  55. data/test/wamp_test/app/controllers/concerns/.keep +0 -0
  56. data/test/wamp_test/app/controllers/ping_controller.rb +7 -0
  57. data/test/wamp_test/app/handlers/add_handler.rb +9 -0
  58. data/test/wamp_test/app/handlers/back_add_handler.rb +26 -0
  59. data/test/wamp_test/app/handlers/back_ping_handler.rb +10 -0
  60. data/test/wamp_test/app/handlers/ping_handler.rb +10 -0
  61. data/test/wamp_test/app/helpers/application_helper.rb +2 -0
  62. data/test/wamp_test/app/jobs/application_job.rb +2 -0
  63. data/test/wamp_test/app/mailers/application_mailer.rb +4 -0
  64. data/test/wamp_test/app/models/application_record.rb +3 -0
  65. data/test/wamp_test/app/models/concerns/.keep +0 -0
  66. data/test/wamp_test/app/views/layouts/application.html.erb +15 -0
  67. data/test/wamp_test/app/views/layouts/mailer.html.erb +13 -0
  68. data/test/wamp_test/app/views/layouts/mailer.text.erb +1 -0
  69. data/test/wamp_test/bin/bundle +3 -0
  70. data/test/wamp_test/bin/rails +9 -0
  71. data/test/wamp_test/bin/rake +9 -0
  72. data/test/wamp_test/bin/setup +36 -0
  73. data/test/wamp_test/bin/spring +17 -0
  74. data/test/wamp_test/bin/update +31 -0
  75. data/test/wamp_test/bin/yarn +11 -0
  76. data/test/wamp_test/config.ru +5 -0
  77. data/test/wamp_test/config/application.rb +19 -0
  78. data/test/wamp_test/config/boot.rb +4 -0
  79. data/test/wamp_test/config/cable.yml +10 -0
  80. data/test/wamp_test/config/credentials.yml.enc +1 -0
  81. data/test/wamp_test/config/database.yml +25 -0
  82. data/test/wamp_test/config/environment.rb +5 -0
  83. data/test/wamp_test/config/environments/development.rb +61 -0
  84. data/test/wamp_test/config/environments/production.rb +94 -0
  85. data/test/wamp_test/config/environments/test.rb +46 -0
  86. data/test/wamp_test/config/initializers/application_controller_renderer.rb +8 -0
  87. data/test/wamp_test/config/initializers/assets.rb +14 -0
  88. data/test/wamp_test/config/initializers/backtrace_silencers.rb +7 -0
  89. data/test/wamp_test/config/initializers/content_security_policy.rb +25 -0
  90. data/test/wamp_test/config/initializers/cookies_serializer.rb +5 -0
  91. data/test/wamp_test/config/initializers/filter_parameter_logging.rb +4 -0
  92. data/test/wamp_test/config/initializers/inflections.rb +16 -0
  93. data/test/wamp_test/config/initializers/mime_types.rb +4 -0
  94. data/test/wamp_test/config/initializers/wamp-worker.rb +8 -0
  95. data/test/wamp_test/config/initializers/wrap_parameters.rb +14 -0
  96. data/test/wamp_test/config/locales/en.yml +33 -0
  97. data/test/wamp_test/config/master.key +1 -0
  98. data/test/wamp_test/config/puma.rb +34 -0
  99. data/test/wamp_test/config/routes.rb +4 -0
  100. data/test/wamp_test/config/sidekiq.yml +6 -0
  101. data/test/wamp_test/config/spring.rb +6 -0
  102. data/test/wamp_test/config/storage.yml +34 -0
  103. data/test/wamp_test/db/development.sqlite3 +0 -0
  104. data/test/wamp_test/db/seeds.rb +7 -0
  105. data/test/wamp_test/lib/assets/.keep +0 -0
  106. data/test/wamp_test/lib/tasks/.keep +0 -0
  107. data/test/wamp_test/package.json +5 -0
  108. data/test/wamp_test/public/404.html +67 -0
  109. data/test/wamp_test/public/422.html +67 -0
  110. data/test/wamp_test/public/500.html +66 -0
  111. data/test/wamp_test/public/apple-touch-icon-precomposed.png +0 -0
  112. data/test/wamp_test/public/apple-touch-icon.png +0 -0
  113. data/test/wamp_test/public/favicon.ico +0 -0
  114. data/test/wamp_test/public/robots.txt +1 -0
  115. data/test/wamp_test/storage/.keep +0 -0
  116. data/test/wamp_test/test/application_system_test_case.rb +5 -0
  117. data/test/wamp_test/test/controllers/.keep +0 -0
  118. data/test/wamp_test/test/fixtures/.keep +0 -0
  119. data/test/wamp_test/test/fixtures/files/.keep +0 -0
  120. data/test/wamp_test/test/helpers/.keep +0 -0
  121. data/test/wamp_test/test/integration/.keep +0 -0
  122. data/test/wamp_test/test/mailers/.keep +0 -0
  123. data/test/wamp_test/test/models/.keep +0 -0
  124. data/test/wamp_test/test/system/.keep +0 -0
  125. data/test/wamp_test/test/test_helper.rb +10 -0
  126. data/test/wamp_test/vendor/.keep +0 -0
  127. data/test/web/index.html +101 -0
  128. data/wamp-worker.gemspec +32 -0
  129. metadata +395 -0
@@ -0,0 +1,184 @@
1
+ require "redis"
2
+
3
+ module Wamp
4
+ module Worker
5
+ DEFAULT = :default
6
+
7
+ #region Storage Objects
8
+ class Handle
9
+ attr_reader :klass, :method, :options
10
+
11
+ def initialize(klass, method, options)
12
+ @klass = klass
13
+ @method = method
14
+ @options = options
15
+
16
+ unless klass.ancestors.include? BaseHandler
17
+ raise(ArgumentError, "'klass' must be a Wamp::Worker::Handler type")
18
+ end
19
+ end
20
+ end
21
+
22
+ class Registration < Handle
23
+ attr_reader :procedure
24
+
25
+ def initialize(procedure, klass, method, options)
26
+ super klass, method, options
27
+ @procedure = procedure
28
+ end
29
+ end
30
+
31
+ class Subscription < Handle
32
+ attr_reader :topic
33
+
34
+ def initialize(topic, klass, method, options)
35
+ super klass, method, options
36
+ @topic = topic
37
+ end
38
+ end
39
+ #endregion
40
+
41
+ # This class is a config proxy that lets you specify the name globally
42
+ #
43
+ class ConfigProxy
44
+ attr_reader :name, :config
45
+
46
+ def initialize(config, name=nil)
47
+ @name = name || DEFAULT
48
+ @config = config
49
+ end
50
+
51
+ # Sets the timeout value
52
+ #
53
+ def timeout(seconds)
54
+ self[:timeout] = seconds
55
+ end
56
+
57
+ # Sets the Redis connection
58
+ #
59
+ def redis(connection)
60
+ self[:redis] = connection
61
+ end
62
+
63
+ # Connection options
64
+ #
65
+ def connection(**options)
66
+ self[:connection] = options
67
+ end
68
+
69
+ # Subscribe the handler to a topic
70
+ #
71
+ # @param topic [String] - The topic to subscribe to
72
+ # @param klass [Wamp::Worker::Handler] - The class to use
73
+ # @param method [Symbol] - The name of the method to execute
74
+ # @param options [Hash] - Options for the subscription
75
+ def subscribe(topic, klass, method, **options)
76
+ subscriptions = self[:subscriptions] || []
77
+ subscriptions << Subscription.new(topic, klass, method, options)
78
+ self[:subscriptions] = subscriptions
79
+ end
80
+
81
+ # Register the handler for a procedure
82
+ #
83
+ # @param procedure [String] - The procedure to register for
84
+ # @param klass [Wamp::Worker::Handler] - The class to use
85
+ # @param method [Symbol] - The name of the method to execute
86
+ # @param options [Hash] - Options for the subscription
87
+ def register(procedure, klass, method, **options)
88
+ registrations = self[:registrations] || []
89
+ registrations << Registration.new(procedure, klass, method, options)
90
+ self[:registrations] = registrations
91
+ end
92
+
93
+ # Allows the user to configure without typing "config."
94
+ #
95
+ def configure(&callback)
96
+ self.instance_eval(&callback)
97
+ end
98
+
99
+ # Sets the attribute using the name
100
+ #
101
+ # @param attribute [Symbol] - The attribute
102
+ # @param value - The value for the attribute
103
+ def []=(attribute, value)
104
+ self.config[self.name][attribute] = value
105
+ end
106
+
107
+ # Gets the attribute using the name
108
+ #
109
+ # @param attribute [Symbol] - The attribute
110
+ def [](attribute)
111
+ self.config[self.name][attribute]
112
+ end
113
+ end
114
+
115
+ # This class is used to store the configuration of the worker
116
+ #
117
+ class Config
118
+ attr_reader :settings
119
+
120
+ def initialize
121
+ @settings = {}
122
+ end
123
+
124
+ # Returns the connection options
125
+ #
126
+ # @param name [Symbol] - The name of the connection
127
+ def connection(name=nil)
128
+ name ||= DEFAULT
129
+ self[name][:connection] || {}
130
+ end
131
+
132
+ # Returns the timeout value
133
+ #
134
+ # @param name [Symbol] - The name of the connection
135
+ def timeout(name=nil)
136
+ name ||= DEFAULT
137
+ self[name][:timeout] || 60
138
+ end
139
+
140
+ # Returns the redis value
141
+ #
142
+ # @param name [Symbol] - The name of the connection
143
+ def redis(name=nil)
144
+ name ||= DEFAULT
145
+ redis = self[name][:redis]
146
+
147
+ # If it is not a redis object, create one using it as the options
148
+ if redis == nil
149
+ redis = ::Redis.new
150
+ elsif not redis.is_a? ::Redis
151
+ redis = ::Redis.new(redis)
152
+ end
153
+
154
+ redis
155
+ end
156
+
157
+ # Returns the subscriptions
158
+ #
159
+ # @param name [Symbol] - The name of the connection
160
+ def subscriptions(name=nil)
161
+ name ||= DEFAULT
162
+ self[name][:subscriptions] || []
163
+ end
164
+
165
+ # Returns the registrations
166
+ #
167
+ # @param name [Symbol] - The name of the connection
168
+ def registrations(name=nil)
169
+ name ||= DEFAULT
170
+ self[name][:registrations] || []
171
+ end
172
+
173
+ # Returns the settings for a particular connection
174
+ #
175
+ # @param name [Symbol] - The name of the connection
176
+ def [](name)
177
+ settings = self.settings[name] || {}
178
+ self.settings[name] = settings
179
+ settings
180
+ end
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,196 @@
1
+ require_relative "proxy/backgrounder"
2
+ require 'wamp/client/response'
3
+ require 'json'
4
+
5
+ module Wamp
6
+ module Worker
7
+
8
+ module BaseHandler
9
+ def self.included(base)
10
+ attr_reader :proxy, :command, :args, :kwargs, :details, :background
11
+
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ # Instantiates the object
18
+ #
19
+ def create(proxy, command, args, kwargs, details)
20
+ handler = self.new
21
+ handler.configure(proxy, command, args, kwargs, details)
22
+ handler
23
+ end
24
+
25
+ # Subscribe the handler to a topic
26
+ #
27
+ # @param topic [String] - The topic to subscribe to
28
+ # @param method [Symbol] - The name of the method to execute
29
+ # @param options [Hash] - Options for the subscription
30
+ def subscribe(topic, method, name: nil, **options)
31
+ klass = self
32
+ Wamp::Worker::configure name do
33
+ subscribe topic, klass, method, **options
34
+ end
35
+ end
36
+
37
+ # Register the handler for a procedure
38
+ #
39
+ # @param procedure [String] - The procedure to register for
40
+ # @param method [Symbol] - The name of the method to execute
41
+ # @param options [Hash] - Options for the subscription
42
+ def register(procedure, method, name: nil, **options)
43
+ klass = self
44
+ Wamp::Worker::configure name do
45
+ register procedure, klass, method, **options
46
+ end
47
+ end
48
+ end
49
+
50
+ # Configures the handler
51
+ #
52
+ def configure(proxy, command, args, kwargs, details, background=false)
53
+ @proxy = proxy
54
+ @command = command
55
+ @args = args || []
56
+ @kwargs = kwargs || {}
57
+ @details = details || {}
58
+ @background = background
59
+ end
60
+
61
+ # This method will send progress of the call to the caller
62
+ #
63
+ # @param result - The value you would like to send to the caller for progress
64
+ def progress(result)
65
+
66
+ # Only allow progress if it is a procedure and the client set "receive_progress"
67
+ if command.to_sym == :procedure and self.details[:receive_progress]
68
+
69
+ # Get the request ID
70
+ request = self.details[:request]
71
+
72
+ # Send the data back to the
73
+ self.session.yield request, result, { progress: true }, self.background
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ module Handler
81
+
82
+ def self.included(base)
83
+ base.class_eval do
84
+ include BaseHandler
85
+ end
86
+ end
87
+
88
+ # Returns the session for the call
89
+ #
90
+ # @return [Wamp::Client::Session, Wamp::Worker::Proxy::Requestor]
91
+ def session
92
+ self.proxy.session
93
+ end
94
+
95
+ # Method that invokes the handler
96
+ #
97
+ def invoke(method)
98
+ self.send(method)
99
+ end
100
+
101
+ end
102
+
103
+ module BackgroundHandler
104
+
105
+ def self.included(base)
106
+ base.class_eval do
107
+ include BaseHandler
108
+
109
+ # Use Sidekiq
110
+ require 'sidekiq'
111
+ include ::Sidekiq::Worker
112
+ end
113
+ end
114
+
115
+ # Returns the session for the call
116
+ #
117
+ # @return [Wamp::Client::Session, Wamp::Worker::Proxy::Requestor]
118
+ def session
119
+ self.proxy
120
+ end
121
+
122
+ # Override the invoke method to push the process to the background
123
+ #
124
+ def invoke(method)
125
+
126
+ # Also need to remove the session since it is not serializable.
127
+ # Will add a new one in the background handler
128
+ self.details.delete(:session)
129
+
130
+ # Send the task to Sidekiq
131
+ #
132
+ # Note: We are explicitly serializing the args, kwargs, details
133
+ # so that we can deserialize and have them appear as symbols in
134
+ # the handler.
135
+ self.class.perform_async(
136
+ method,
137
+ self.proxy.name,
138
+ self.proxy.background_res_queue,
139
+ self.command,
140
+ self.args.to_json,
141
+ self.kwargs.to_json,
142
+ details.to_json)
143
+
144
+ # If it is a procedure, return a defer
145
+ if self.command.to_sym == :procedure
146
+ Wamp::Client::Response::CallDefer.new
147
+ else
148
+ nil
149
+ end
150
+
151
+ end
152
+
153
+ # Method that is run when the process is invoked on the worker
154
+ #
155
+ # @param method [Symbol] - The name of the method to execute
156
+ # @param command [Symbol] - The command that is being backgrounded
157
+ # @param args [Array] - The arguments for the handler
158
+ # @param kwargs [Hash] - The keyword arguments for the handler
159
+ # @param details [Hash] - Other details about the call
160
+ def perform(method, proxy_name, proxy_handle, command, args, kwargs, details)
161
+
162
+ # Create a proxy to act like the session. Use a backgrounder so we also
163
+ # get the "yield" method
164
+ proxy = Proxy::Backgrounder.new(proxy_name, proxy_handle)
165
+
166
+ # Deserialize the arguments as symbols
167
+ args = JSON.parse(args, :symbolize_names => true)
168
+ kwargs = JSON.parse(kwargs, :symbolize_names => true)
169
+ details = JSON.parse(details, :symbolize_names => true)
170
+
171
+ # Get the request ID
172
+ request = details[:request]
173
+
174
+ # Add the proxy to the details as a "session"
175
+ details[:session] = self.session
176
+
177
+ # Configure the handler
178
+ self.configure(proxy, command, args, kwargs, details, true)
179
+
180
+ # Call the user code and make sure to catch exceptions
181
+ result = Wamp::Client::Response.invoke_handler do
182
+ self.send(method)
183
+ end
184
+
185
+ # Only return the response if it is a procedure
186
+ if command.to_sym == :procedure
187
+
188
+ # Send the data back to the
189
+ self.session.yield request, result, {}, true
190
+
191
+ end
192
+ end
193
+
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "requestor"
2
+
3
+ module Wamp
4
+ module Worker
5
+ module Proxy
6
+
7
+ class Backgrounder < Requestor
8
+ attr_reader :handle
9
+
10
+ # Constructor
11
+ #
12
+ def initialize(name, handle, uuid: nil)
13
+ super name, uuid: uuid
14
+ @handle = handle
15
+ end
16
+
17
+ # Returns the response to the dispatcher
18
+ #
19
+ # @param request [Int] - The ID of the request
20
+ # @param result [CallResult,CallError] - The result or error for us to serialize
21
+ # @param options [Hash] - Options for the yield
22
+ # @param check_defer [Bool] - 'true' is this is linked to a defer call
23
+ def yield(request, result, options={}, check_defer=false)
24
+
25
+ # Create the response object
26
+ result = Wamp::Client::Response::CallResult.ensure(result, allow_error: true)
27
+
28
+ # Create the params
29
+ params = { request: request, result: result.to_hash, options: options, check_defer: check_defer }
30
+
31
+ # Push to the worker who requested the result
32
+ self.queue.push self.handle, :yield, params
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,101 @@
1
+ require "wamp/client/response"
2
+ require_relative "../ticker"
3
+ require_relative "../queue"
4
+
5
+ module Wamp
6
+ module Worker
7
+ module Proxy
8
+
9
+ class Base
10
+ attr_reader :queue, :ticker, :name, :uuid
11
+
12
+ # Constructor
13
+ #
14
+ # @param name [Symbol] - The name of the connection
15
+ # @param uuid [String] - The uuid for the proxy. This can be passed in as
16
+ # well to allow it to be initialized for hte entire worker
17
+ def initialize(name, uuid: nil)
18
+ @name = name
19
+ @queue = Wamp::Worker::Queue.new(name)
20
+ @ticker = Wamp::Worker::Ticker.new(name)
21
+ @uuid = uuid || ENV['DYNO'] || SecureRandom.hex(12)
22
+ end
23
+
24
+ #region Command/Response
25
+ # ----------------
26
+ # This workflow is used by a "Requestor" to make a "call"
27
+ # or "publish" request to the "Dispatcher". This would for example be in
28
+ # your rails app where a service or controller needs to make a WAMP call
29
+ #
30
+ # The flow is as follows
31
+ #
32
+ # - Requestor performs a "push" operation with the following parameters
33
+ # - queue_name - The "command queue"
34
+ # - command - The command ("call" or "publish")
35
+ # - params - The parameters for the command (args/kwargs/etc.)
36
+ # - handle - A unique "response queue" name that the Dispatcher will
37
+ # provide the resposne on
38
+ # - Requestor then blocks (with timeout) awaiting the response
39
+ # - Dispatcher performs a "pop" operation and executes the command
40
+ # - Dispatcher "pushes" the response to the "handle" queue
41
+ # - Requestor "pops" the response and deletes the temporary "handle" queue
42
+
43
+ # Returns the commands queue key for the worker
44
+ #
45
+ # @return [String] - The key for the commands list
46
+ def command_req_queue
47
+ "wamp:#{self.name}:command"
48
+ end
49
+
50
+ # Returns a new handle
51
+ #
52
+ # @return [String] - The key for the new handle
53
+ def unique_command_resp_queue
54
+ "wamp:#{self.name}:response:#{SecureRandom.hex(12)}"
55
+ end
56
+
57
+ #endregion
58
+
59
+ #region Dispatcher/Backgrounder
60
+ # ----------------
61
+ # This workflow is used by a "Dispatcher" to execute a "topic" or "procedure"
62
+ # on a background thread. This is used by a "BackgroundHandler" to push a
63
+ # handler to Sidekiq and get the response from the background job. This frees
64
+ # up the Event Machine to process other requests
65
+ #
66
+ # The flow is as follows
67
+ #
68
+ # - Dispatcher pushes the task to the background sidekiq worker providing
69
+ # the "handle" it will respond on
70
+ # - Backgrounder performs the operation
71
+ # - Backgrounder performs a "push" with response back to the Dispatcher
72
+ # - Dispatcher performs a "pop" and sends the response (if it is a call)
73
+
74
+ # Returns the response queue name for the backgrounder
75
+ #
76
+ # @return [String] - The key for the worker
77
+ def background_res_queue
78
+ "wamp:#{self.name}:background:#{self.uuid}"
79
+ end
80
+
81
+ #endregion
82
+
83
+ #region Requestor/Dispatcher Ticker
84
+ # ----------------
85
+ # This workflow is used to sense when a worker is no longer running
86
+ #
87
+ # The flow is as follows
88
+ #
89
+ # - Dispatcher periodically increments the ticker
90
+ # - Requestor does the following when performing a pop
91
+ # - stores the value of the ticker
92
+ # - blocks waiting for a "pop"
93
+ # - if the pop comes back "nil", it means we timed out
94
+ # - if the value of the ticker is that same as before, then th worker is not running
95
+
96
+ #endregion
97
+
98
+ end
99
+ end
100
+ end
101
+ end