garcun 0.0.2

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 (139) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +17 -0
  3. data/.gitignore +197 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +201 -0
  7. data/README.md +521 -0
  8. data/Rakefile +47 -0
  9. data/garcun.gemspec +83 -0
  10. data/lib/garcon.rb +290 -0
  11. data/lib/garcon/chef/chef_helpers.rb +343 -0
  12. data/lib/garcon/chef/coerce/coercer.rb +134 -0
  13. data/lib/garcon/chef/coerce/coercions/boolean_definitions.rb +34 -0
  14. data/lib/garcon/chef/coerce/coercions/date_definitions.rb +32 -0
  15. data/lib/garcon/chef/coerce/coercions/date_time_definitions.rb +32 -0
  16. data/lib/garcon/chef/coerce/coercions/fixnum_definitions.rb +34 -0
  17. data/lib/garcon/chef/coerce/coercions/float_definitions.rb +32 -0
  18. data/lib/garcon/chef/coerce/coercions/hash_definitions.rb +29 -0
  19. data/lib/garcon/chef/coerce/coercions/integer_definitions.rb +31 -0
  20. data/lib/garcon/chef/coerce/coercions/string_definitions.rb +45 -0
  21. data/lib/garcon/chef/coerce/coercions/time_definitions.rb +32 -0
  22. data/lib/garcon/chef/handler/devreporter.rb +127 -0
  23. data/lib/garcon/chef/log.rb +64 -0
  24. data/lib/garcon/chef/node.rb +100 -0
  25. data/lib/garcon/chef/provider/civilize.rb +209 -0
  26. data/lib/garcon/chef/provider/development.rb +159 -0
  27. data/lib/garcon/chef/provider/download.rb +420 -0
  28. data/lib/garcon/chef/provider/house_keeping.rb +265 -0
  29. data/lib/garcon/chef/provider/node_cache.rb +31 -0
  30. data/lib/garcon/chef/provider/partial.rb +183 -0
  31. data/lib/garcon/chef/provider/recovery.rb +80 -0
  32. data/lib/garcon/chef/provider/zip_file.rb +271 -0
  33. data/lib/garcon/chef/resource/attribute.rb +52 -0
  34. data/lib/garcon/chef/resource/base_dsl.rb +174 -0
  35. data/lib/garcon/chef/resource/blender.rb +140 -0
  36. data/lib/garcon/chef/resource/lazy_eval.rb +66 -0
  37. data/lib/garcon/chef/resource/resource_name.rb +109 -0
  38. data/lib/garcon/chef/secret_bag.rb +204 -0
  39. data/lib/garcon/chef/validations.rb +76 -0
  40. data/lib/garcon/chef_inclusions.rb +151 -0
  41. data/lib/garcon/configuration.rb +138 -0
  42. data/lib/garcon/core_ext.rb +39 -0
  43. data/lib/garcon/core_ext/array.rb +27 -0
  44. data/lib/garcon/core_ext/binding.rb +64 -0
  45. data/lib/garcon/core_ext/boolean.rb +66 -0
  46. data/lib/garcon/core_ext/duration.rb +271 -0
  47. data/lib/garcon/core_ext/enumerable.rb +34 -0
  48. data/lib/garcon/core_ext/file.rb +127 -0
  49. data/lib/garcon/core_ext/filetest.rb +62 -0
  50. data/lib/garcon/core_ext/hash.rb +279 -0
  51. data/lib/garcon/core_ext/kernel.rb +159 -0
  52. data/lib/garcon/core_ext/lazy.rb +222 -0
  53. data/lib/garcon/core_ext/method_access.rb +243 -0
  54. data/lib/garcon/core_ext/module.rb +92 -0
  55. data/lib/garcon/core_ext/nil.rb +53 -0
  56. data/lib/garcon/core_ext/numeric.rb +44 -0
  57. data/lib/garcon/core_ext/object.rb +342 -0
  58. data/lib/garcon/core_ext/pathname.rb +152 -0
  59. data/lib/garcon/core_ext/process.rb +41 -0
  60. data/lib/garcon/core_ext/random.rb +497 -0
  61. data/lib/garcon/core_ext/string.rb +312 -0
  62. data/lib/garcon/core_ext/struct.rb +49 -0
  63. data/lib/garcon/core_ext/symbol.rb +170 -0
  64. data/lib/garcon/core_ext/time.rb +234 -0
  65. data/lib/garcon/exceptions.rb +101 -0
  66. data/lib/garcon/inflections.rb +237 -0
  67. data/lib/garcon/inflections/defaults.rb +79 -0
  68. data/lib/garcon/inflections/inflections.rb +182 -0
  69. data/lib/garcon/inflections/rules_collection.rb +37 -0
  70. data/lib/garcon/secret.rb +271 -0
  71. data/lib/garcon/stash/format.rb +114 -0
  72. data/lib/garcon/stash/journal.rb +226 -0
  73. data/lib/garcon/stash/queue.rb +83 -0
  74. data/lib/garcon/stash/serializer.rb +86 -0
  75. data/lib/garcon/stash/store.rb +435 -0
  76. data/lib/garcon/task.rb +31 -0
  77. data/lib/garcon/task/atomic.rb +151 -0
  78. data/lib/garcon/task/atomic_boolean.rb +127 -0
  79. data/lib/garcon/task/condition.rb +99 -0
  80. data/lib/garcon/task/copy_on_notify_observer_set.rb +154 -0
  81. data/lib/garcon/task/copy_on_write_observer_set.rb +153 -0
  82. data/lib/garcon/task/count_down_latch.rb +92 -0
  83. data/lib/garcon/task/delay.rb +196 -0
  84. data/lib/garcon/task/dereferenceable.rb +144 -0
  85. data/lib/garcon/task/event.rb +119 -0
  86. data/lib/garcon/task/executor.rb +275 -0
  87. data/lib/garcon/task/executor_options.rb +59 -0
  88. data/lib/garcon/task/future.rb +107 -0
  89. data/lib/garcon/task/immediate_executor.rb +84 -0
  90. data/lib/garcon/task/ivar.rb +171 -0
  91. data/lib/garcon/task/lazy_reference.rb +74 -0
  92. data/lib/garcon/task/monotonic_time.rb +69 -0
  93. data/lib/garcon/task/obligation.rb +256 -0
  94. data/lib/garcon/task/observable.rb +101 -0
  95. data/lib/garcon/task/priority_queue.rb +234 -0
  96. data/lib/garcon/task/processor_count.rb +128 -0
  97. data/lib/garcon/task/read_write_lock.rb +304 -0
  98. data/lib/garcon/task/safe_task_executor.rb +58 -0
  99. data/lib/garcon/task/single_thread_executor.rb +97 -0
  100. data/lib/garcon/task/thread_pool/cached.rb +71 -0
  101. data/lib/garcon/task/thread_pool/executor.rb +294 -0
  102. data/lib/garcon/task/thread_pool/fixed.rb +61 -0
  103. data/lib/garcon/task/thread_pool/worker.rb +90 -0
  104. data/lib/garcon/task/timer.rb +44 -0
  105. data/lib/garcon/task/timer_set.rb +194 -0
  106. data/lib/garcon/task/timer_task.rb +377 -0
  107. data/lib/garcon/task/waitable_list.rb +58 -0
  108. data/lib/garcon/utility/ansi.rb +199 -0
  109. data/lib/garcon/utility/at_random.rb +77 -0
  110. data/lib/garcon/utility/crypto.rb +292 -0
  111. data/lib/garcon/utility/equalizer.rb +146 -0
  112. data/lib/garcon/utility/faker/extensions/array.rb +22 -0
  113. data/lib/garcon/utility/faker/extensions/symbol.rb +9 -0
  114. data/lib/garcon/utility/faker/faker.rb +164 -0
  115. data/lib/garcon/utility/faker/faker/company.rb +17 -0
  116. data/lib/garcon/utility/faker/faker/hacker.rb +30 -0
  117. data/lib/garcon/utility/faker/faker/version.rb +3 -0
  118. data/lib/garcon/utility/faker/locales/en-US.yml +83 -0
  119. data/lib/garcon/utility/faker/locales/en.yml +21 -0
  120. data/lib/garcon/utility/file_helper.rb +170 -0
  121. data/lib/garcon/utility/hookers.rb +178 -0
  122. data/lib/garcon/utility/interpolation.rb +90 -0
  123. data/lib/garcon/utility/memstash.rb +364 -0
  124. data/lib/garcon/utility/misc.rb +54 -0
  125. data/lib/garcon/utility/msg_from_god.rb +62 -0
  126. data/lib/garcon/utility/retry.rb +238 -0
  127. data/lib/garcon/utility/timeout.rb +58 -0
  128. data/lib/garcon/utility/uber/builder.rb +91 -0
  129. data/lib/garcon/utility/uber/callable.rb +7 -0
  130. data/lib/garcon/utility/uber/delegates.rb +13 -0
  131. data/lib/garcon/utility/uber/inheritable_attr.rb +37 -0
  132. data/lib/garcon/utility/uber/options.rb +101 -0
  133. data/lib/garcon/utility/uber/uber_version.rb +3 -0
  134. data/lib/garcon/utility/uber/version.rb +33 -0
  135. data/lib/garcon/utility/url_helper.rb +100 -0
  136. data/lib/garcon/utils.rb +29 -0
  137. data/lib/garcon/version.rb +62 -0
  138. data/lib/garcun.rb +24 -0
  139. metadata +680 -0
@@ -0,0 +1,61 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require_relative 'executor'
21
+
22
+ module Garcon
23
+
24
+ class FixedThreadPool < ThreadPoolExecutor
25
+
26
+ # Create a new thread pool.
27
+ #
28
+ # @param [Integer] num_threads
29
+ # The number of threads to allocate.
30
+ #
31
+ # @param [Hash] opts
32
+ # The options defining pool behavior.
33
+ #
34
+ # @option opts [Symbol] :fallback (`:abort`)
35
+ # The fallback policy
36
+ #
37
+ # @raise [ArgumentError] if `num_threads` is less than or equal to zero
38
+ #
39
+ # @raise [ArgumentError] if `fallback` is not a known policy
40
+ #
41
+ # @api public
42
+ def initialize(num_threads, opts = {})
43
+ fallback = opts.fetch(:fallback, :abort)
44
+
45
+ if num_threads < 1
46
+ raise ArgumentError, 'number of threads must be greater than zero'
47
+ elsif !FALLBACK_POLICY.include?(fallback)
48
+ raise ArgumentError, "#{fallback} is not a valid fallback policy"
49
+ end
50
+
51
+ opts = opts.merge(
52
+ min_threads: num_threads,
53
+ max_threads: num_threads,
54
+ fallback: fallback,
55
+ max_queue: DEFAULT_MAX_QUEUE_SIZE,
56
+ idletime: DEFAULT_THREAD_IDLETIMEOUT)
57
+
58
+ super(opts)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'thread'
21
+ require_relative '../monotonic_time'
22
+
23
+ module Garcon
24
+
25
+ # @!visibility private
26
+ class ThreadPoolWorker
27
+
28
+ # @!visibility private
29
+ def initialize(queue, parent)
30
+ @queue = queue
31
+ @parent = parent
32
+ @mutex = Mutex.new
33
+ @last_activity = Garcon.monotonic_time
34
+ @thread = nil
35
+ end
36
+
37
+ # @!visibility private
38
+ def dead?
39
+ return @mutex.synchronize do
40
+ @thread.nil? ? false : ! @thread.alive?
41
+ end
42
+ end
43
+
44
+ # @!visibility private
45
+ def last_activity
46
+ @mutex.synchronize { @last_activity }
47
+ end
48
+
49
+ def status
50
+ @mutex.synchronize do
51
+ return 'not running' if @thread.nil?
52
+ @thread.status
53
+ end
54
+ end
55
+
56
+ # @!visibility private
57
+ def kill
58
+ @mutex.synchronize do
59
+ Thread.kill(@thread) unless @thread.nil?
60
+ @thread = nil
61
+ end
62
+ end
63
+
64
+ # @!visibility private
65
+ def run(thread = Thread.current)
66
+ @mutex.synchronize do
67
+ raise StandardError, 'already running' unless @thread.nil?
68
+ @thread = thread
69
+ end
70
+
71
+ loop do
72
+ task = @queue.pop
73
+ if task == :stop
74
+ @thread = nil
75
+ @parent.on_worker_exit(self)
76
+ break
77
+ end
78
+
79
+ begin
80
+ task.last.call(*task.first)
81
+ rescue => e
82
+ Chef::Log.debug "Caught exception => #{e}"
83
+ ensure
84
+ @last_activity = Garcon.monotonic_time
85
+ @parent.on_end_task
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'thread'
21
+
22
+ module Garcon
23
+
24
+ # Perform the given operation asynchronously after the given number of
25
+ # seconds.
26
+ #
27
+ # @param [Fixnum] seconds
28
+ # The interval in seconds to wait before executing the task
29
+ #
30
+ # @yield the task to execute
31
+ #
32
+ # @return [Boolean] true
33
+ #
34
+ def timer(seconds, *args, &block)
35
+ raise ArgumentError, 'no block given' unless block_given?
36
+ if seconds < 0
37
+ raise ArgumentError, 'interval must be greater than or equal to zero'
38
+ end
39
+
40
+ Garcon.global_timer_set.post(seconds, *args, &block)
41
+ true
42
+ end
43
+ module_function :timer
44
+ end
@@ -0,0 +1,194 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'thread'
21
+ require_relative 'event'
22
+ require_relative 'priority_queue'
23
+ require_relative 'executor'
24
+ require_relative 'single_thread_executor'
25
+ require_relative 'monotonic_time'
26
+ require_relative 'executor_options'
27
+
28
+ module Garcon
29
+
30
+ # Executes a collection of tasks, each after a given delay. A master task
31
+ # monitors the set and schedules each task for execution at the appropriate
32
+ # time. Tasks are run on the global task pool or on the supplied executor.
33
+ #
34
+ class TimerSet
35
+ include RubyExecutor
36
+ include ExecutorOptions
37
+
38
+ # Create a new set of timed tasks.
39
+ #
40
+ # @!macro [attach] executor_options
41
+ #
42
+ # @param [Hash] opts
43
+ # The options used to specify the executor on which to perform actions.
44
+ #
45
+ # @option opts [Executor] :executor
46
+ # When set use the given `Executor` instance. Three special values are
47
+ # also supported: `:task` returns the global task pool, `:operation`
48
+ # returns the global operation pool, and `:immediate` returns a new
49
+ # `ImmediateExecutor` object.
50
+ #
51
+ def initialize(opts = {})
52
+ @queue = PriorityQueue.new(order: :min)
53
+ @task_executor = get_executor_from(opts) || Garcon.global_io_executor
54
+ @timer_executor = SingleThreadExecutor.new
55
+ @condition = Condition.new
56
+ init_executor
57
+ enable_at_exit_handler!(opts)
58
+ end
59
+
60
+ # Post a task to be execute run after a given delay (in seconds). If the
61
+ # delay is less than 1/100th of a second the task will be immediately post
62
+ # to the executor.
63
+ #
64
+ # @param [Float] delay
65
+ # The number of seconds to wait for before executing the task.
66
+ #
67
+ # @yield the task to be performed.
68
+ #
69
+ # @raise [ArgumentError] f the intended execution time is not in the future.
70
+ #
71
+ # @raise [ArgumentError] if no block is given.
72
+ #
73
+ # @return [Boolean]
74
+ # True if the message is post, false after shutdown.
75
+ #
76
+ def post(delay, *args, &task)
77
+ raise ArgumentError, 'no block given' unless block_given?
78
+ delay = TimerSet.calculate_delay!(delay)
79
+
80
+ mutex.synchronize do
81
+ return false unless running?
82
+
83
+ if (delay) <= 0.01
84
+ @task_executor.post(*args, &task)
85
+ else
86
+ @queue.push(Task.new(Garcon.monotonic_time + delay, args, task))
87
+ @timer_executor.post(&method(:process_tasks))
88
+ end
89
+ end
90
+
91
+ @condition.signal
92
+ true
93
+ end
94
+
95
+ # @!visibility private
96
+ def <<(task)
97
+ post(0.0, &task)
98
+ self
99
+ end
100
+
101
+ # @!macro executor_method_shutdown
102
+ def kill
103
+ shutdown
104
+ end
105
+
106
+ # Schedule a task to be executed after a given delay (in seconds).
107
+ #
108
+ # @param [Float] delay
109
+ # The number of seconds to wait for before executing the task.
110
+ #
111
+ # @raise [ArgumentError] if the intended execution time is not in the future
112
+ #
113
+ # @raise [ArgumentError] if no block is given.
114
+ #
115
+ # @return [Float]
116
+ # The number of seconds to delay.
117
+ #
118
+ def self.calculate_delay!(delay)
119
+ if delay.is_a?(Time)
120
+ if delay <= now
121
+ raise ArgumentError, 'schedule time must be in the future'
122
+ end
123
+ delay.to_f - now.to_f
124
+ else
125
+ if delay.to_f < 0.0
126
+ raise ArgumentError, 'seconds must be greater than zero'
127
+ end
128
+ delay.to_f
129
+ end
130
+ end
131
+
132
+ private # P R O P R I E T À P R I V A T A Vietato L'accesso
133
+
134
+ # A struct for encapsulating a task and its intended execution time.
135
+ # It facilitates proper prioritization by overriding the comparison
136
+ # (spaceship) operator as a comparison of the intended execution
137
+ # times.
138
+ #
139
+ # @!visibility private
140
+ Task = Struct.new(:time, :args, :op) do
141
+ include Comparable
142
+
143
+ def <=>(other)
144
+ self.time <=> other.time
145
+ end
146
+ end
147
+
148
+ private_constant :Task
149
+
150
+ # @!visibility private
151
+ def shutdown_execution
152
+ @queue.clear
153
+ @timer_executor.kill
154
+ stopped_event.set
155
+ end
156
+
157
+ # Run a loop and execute tasks in the scheduled order and at the approximate
158
+ # scheduled time. If no tasks remain the thread will exit gracefully so that
159
+ # garbage collection can occur. If there are no ready tasks it will sleep
160
+ # for up to 60 seconds waiting for the next scheduled task.
161
+ #
162
+ # @!visibility private
163
+ def process_tasks
164
+ loop do
165
+ task = mutex.synchronize { @queue.peek }
166
+ break unless task
167
+
168
+ now = Garcon.monotonic_time
169
+ diff = task.time - now
170
+
171
+ if diff <= 0
172
+ # We need to remove the task from the queue before passing it to the
173
+ # executor, to avoid race conditions where we pass the peek'ed task
174
+ # to the executor and then pop a different one that's been added in
175
+ # the meantime.
176
+ #
177
+ # Note that there's no race condition between the peek and this pop -
178
+ # this pop could retrieve a different task from the peek, but that
179
+ # task would be due to fire now anyway (because @queue is a priority
180
+ # queue, and this thread is the only reader, so whatever timer is at
181
+ # the head of the queue now must have the same pop time, or a closer
182
+ # one, as when we peeked).
183
+ #
184
+ task = mutex.synchronize { @queue.pop }
185
+ @task_executor.post(*task.args, &task.op)
186
+ else
187
+ mutex.synchronize do
188
+ @condition.wait(mutex, [diff, 60].min)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,377 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Author: Stefano Harding <riddopic@gmail.com>
4
+ # License: Apache License, Version 2.0
5
+ # Copyright: (C) 2014-2015 Stefano Harding
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require_relative 'dereferenceable'
21
+ require_relative 'observable'
22
+ require_relative 'atomic_boolean'
23
+ require_relative 'executor'
24
+ require_relative 'safe_task_executor'
25
+
26
+ module Garcon
27
+
28
+ # A very common currency pattern is to run a thread that performs a task at
29
+ # regular intervals. The thread that performs the task sleeps for the given
30
+ # interval then wakes up and performs the task. Lather, rinse, repeat... This
31
+ # pattern causes two problems. First, it is difficult to test the business
32
+ # logic of the task because the task itself is tightly coupled with the
33
+ # concurrency logic. Second, an exception raised while performing the task can
34
+ # cause the entire thread to abend. In a long-running application where the
35
+ # task thread is intended to run for days/weeks/years a crashed task thread
36
+ # can pose a significant problem. `TimerTask` alleviates both problems.
37
+ #
38
+ # When a `TimerTask` is launched it starts a thread for monitoring the
39
+ # execution interval. The `TimerTask` thread does not perform the task,
40
+ # however. Instead, the TimerTask launches the task on a separate thread.
41
+ # Should the task experience an unrecoverable crash only the task thread will
42
+ # crash. This makes the `TimerTask` very fault tolerant Additionally, the
43
+ # `TimerTask` thread can respond to the success or failure of the task,
44
+ # performing logging or ancillary operations. `TimerTask` can also be
45
+ # configured with a timeout value allowing it to kill a task that runs too
46
+ # long.
47
+ #
48
+ # One other advantage of `TimerTask` is that it forces the business logic to
49
+ # be completely decoupled from the concurrency logic. The business logic can
50
+ # be tested separately then passed to the `TimerTask` for scheduling and
51
+ # running.
52
+ #
53
+ # In some cases it may be necessary for a `TimerTask` to affect its own
54
+ # execution cycle. To facilitate this, a reference to the TimerTask instance
55
+ # is passed as an argument to the provided block every time the task is
56
+ # executed.
57
+ #
58
+ # The `TimerTask` class includes the `Dereferenceable` mixin module so the
59
+ # result of the last execution is always available via the `#value` method.
60
+ # Derefencing options can be passed to the `TimerTask` during construction or
61
+ # at any later time using the `#set_deref_options` method.
62
+ #
63
+ # `TimerTask` supports notification through the Ruby standard library
64
+ # {http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
65
+ # Observable} module. On execution the `TimerTask` will notify the observers
66
+ # with three arguments: time of execution, the result of the block (or nil on
67
+ # failure), and any raised exceptions (or nil on success). If the timeout
68
+ # interval is exceeded the observer will receive a `Garcon::TimeoutError`
69
+ # object as the third argument.
70
+ #
71
+ # @example Basic usage
72
+ # tt = Garcon::TimerTask.new { puts 'Run! Go! Execute! GO! GO! GO!' }
73
+ # tt.execute
74
+ #
75
+ # tt.execution_interval # => 60 (default)
76
+ # tt.timeout_interval # => 30 (default)
77
+ #
78
+ # # wait 60 seconds...
79
+ # # => 'Run! Go! Execute! GO! GO! GO!'
80
+ #
81
+ # tt.shutdown # => true
82
+ #
83
+ # @example Configuring `:execution_interval` and `:timeout_interval`
84
+ # tt = Garcon::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
85
+ # puts 'Execute! Execute! GO! GO! GO!'
86
+ # end
87
+ #
88
+ # tt.execution_interval # => 5
89
+ # tt.timeout_interval # => 5
90
+ #
91
+ # @example Immediate execution with `:run_now`
92
+ # tt = Garcon::TimerTask.new(run_now: true) { puts 'GO! GO! GO!' }
93
+ # tt.execute
94
+ #
95
+ # # => 'GO! GO! GO!'
96
+ #
97
+ # @example Last `#value` and `Dereferenceable` mixin
98
+ # tt = Garcon::TimerTask.new(dup_on_deref: true, execution_interval: 5) do
99
+ # Time.now
100
+ # end
101
+ #
102
+ # tt.execute
103
+ # Time.now # => 2015-03-21 08:56:50 -0700
104
+ # sleep(10)
105
+ # tt.value # => 2015-03-21 08:56:55 -0700
106
+ #
107
+ # @example Controlling execution from within the block
108
+ # timer_task = Garcon::TimerTask.new(execution_interval: 1) do |task|
109
+ # task.execution_interval.times { print 'Execute! ' }
110
+ # print "\n"
111
+ # task.execution_interval += 1
112
+ # if task.execution_interval > 5
113
+ # puts 'Executed...'
114
+ # task.shutdown
115
+ # end
116
+ # end
117
+ #
118
+ # timer_task.execute # blocking call - this task will stop itself
119
+ # # => Execute!
120
+ # # => Execute! Execute!
121
+ # # => Execute! Execute! Execute!
122
+ # # => Execute! Execute! Execute! Execute!
123
+ # # => Execute! Execute! Execute! Execute! Execute!
124
+ # # => Executed...
125
+ #
126
+ # @example Observation
127
+ # class TaskObserver
128
+ # def update(time, result, ex)
129
+ # if result
130
+ # print "(#{time}) Execution successfully returned #{result}\n"
131
+ # elsif ex.is_a?(Garcon::TimeoutError)
132
+ # print "(#{time}) Execution timed out\n"
133
+ # else
134
+ # print "(#{time}) Execution failed with error #{ex}\n"
135
+ # end
136
+ # end
137
+ # end
138
+ #
139
+ # tt = Garcon::TimerTask.new(execution_interval: 1, timeout_interval: 1) {
140
+ # 42
141
+ # }
142
+ # tt.add_observer(TaskObserver.new)
143
+ # tt.execute
144
+ #
145
+ # # => (2015-03-21 09:06:07 -0700) Execution successfully returned 42
146
+ # # => (2015-03-21 09:06:08 -0700) Execution successfully returned 42
147
+ # # => (2015-03-21 09:06:09 -0700) Execution successfully returned 42
148
+ # tt.shutdown
149
+ #
150
+ # tt = Garcon::TimerTask.new(execution_interval: 1, timeout_interval: 1) {
151
+ # sleep
152
+ # }
153
+ # tt.add_observer(TaskObserver.new)
154
+ # tt.execute
155
+ #
156
+ # # => (2015-03-21 09:07:10 -0700) Execution timed out
157
+ # # => (2015-03-21 09:07:12 -0700) Execution timed out
158
+ # # => (2015-03-21 09:07:14 -0700) Execution timed out
159
+ # tt.shutdown
160
+ #
161
+ # tt = Garcon::TimerTask.new(execution_interval: 1) { raise StandardError }
162
+ # tt.add_observer(TaskObserver.new)
163
+ # tt.execute
164
+ #
165
+ # # => (2015-03-21 09:12:11 -0700) Execution failed with error StandardError
166
+ # # => (2015-03-21 09:12:12 -0700) Execution failed with error StandardError
167
+ # # => (2015-03-21 09:12:13 -0700) Execution failed with error StandardError
168
+ # tt.shutdown
169
+ #
170
+ class TimerTask
171
+ include Dereferenceable
172
+ include RubyExecutor
173
+ include Observable
174
+
175
+ # Default :execution_interval in seconds.
176
+ EXECUTION_INTERVAL = 60
177
+
178
+ # Default :timeout_interval in seconds.
179
+ TIMEOUT_INTERVAL = 30
180
+
181
+ # Create a new TimerTask with the given task and configuration.
182
+ #
183
+ # @!macro [attach] timer_task_initialize
184
+ # @note
185
+ # Calls Garcon::Dereferenceable# set_deref_options passing opts. All
186
+ # options supported by Garcon::Dereferenceable can be set during object
187
+ # initialization.
188
+ #
189
+ # @param [Hash] opts
190
+ # The options defining task execution.
191
+ # @option opts [Integer] :execution_interval
192
+ # The number of seconds between task executions (defaults to:
193
+ # EXECUTION_INTERVAL)
194
+ # @option opts [Integer] :timeout_interval
195
+ # The number of seconds a task can run before it is considered to have
196
+ # failed (default: TIMEOUT_INTERVAL)
197
+ # @option opts [Boolean] :run_now
198
+ # Whether to run the task immediately upon instantiation or to wait
199
+ # until the first execution_interval has passed (default: false)
200
+ #
201
+ # @raise ArgumentError
202
+ # when no block is given.
203
+ #
204
+ # @yield to the block after :execution_interval seconds have passed since
205
+ # the last yield
206
+ # @yieldparam task a reference to the TimerTask instance so that the
207
+ # block can control its own lifecycle. Necessary since self will
208
+ # refer to the execution context of the block rather than the running
209
+ # TimerTask.
210
+ #
211
+ # @return [TimerTask]
212
+ # the new TimerTask.
213
+ #
214
+ # @see Garcon::Dereferenceable# set_deref_options
215
+ def initialize(opts = {}, &task)
216
+ raise ArgumentError.new('no block given') unless block_given?
217
+
218
+ init_executor
219
+ set_deref_options(opts)
220
+
221
+ self.execution_interval =
222
+ opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
223
+
224
+ self.timeout_interval =
225
+ opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
226
+
227
+ @run_now = opts[:now] || opts[:run_now]
228
+ @executor = Garcon::SafeTaskExecutor.new(task)
229
+ @running = Garcon::AtomicBoolean.new(false)
230
+
231
+ self.observers = CopyOnNotifyObserverSet.new
232
+ end
233
+
234
+ # Is the executor running?
235
+ #
236
+ # @return [Boolean]
237
+ # True when running, false when shutting down or shutdown
238
+ def running?
239
+ @running.true?
240
+ end
241
+
242
+ # Execute a previously created TimerTask.
243
+ #
244
+ # @example Instance and execute in separate steps
245
+ # tt = Garcon::TimerTask.new(execution_interval: 10) { puts 'Sup!' }
246
+ # tt.running? # => false
247
+ # tt.execute
248
+ # tt.running? # => true
249
+ #
250
+ # @example Instance and execute in one line
251
+ # tt = Garcon::TimerTask.new(execution_interval: 10) { puts 'hi' }.execute
252
+ # tt.running? # => true
253
+ #
254
+ # @return [TimerTask]
255
+ # A reference to self.
256
+ def execute
257
+ mutex.synchronize do
258
+ if @running.false?
259
+ @running.make_true
260
+ schedule_next_task(@run_now ? 0 : @execution_interval)
261
+ end
262
+ end
263
+ self
264
+ end
265
+
266
+ # Create and execute a new TimerTask.
267
+ #
268
+ # @!macro timer_task_initialize
269
+ #
270
+ # @example
271
+ # task = Garcon::TimerTask.execute(execution_interval: 10) do
272
+ # puts 'Sappening d00d?'
273
+ # end
274
+ # task.running? # => true
275
+ def self.execute(opts = {}, &task)
276
+ TimerTask.new(opts, &task).execute
277
+ end
278
+
279
+ # @!attribute [rw] execution_interval
280
+ # @return [Fixnum]
281
+ # Number of seconds after the task completes before it is performed again.
282
+ def execution_interval
283
+ mutex.lock
284
+ @execution_interval
285
+ ensure
286
+ mutex.unlock
287
+ end
288
+
289
+ # @!attribute [rw] execution_interval
290
+ # @return [Fixnum]
291
+ # Number of seconds after the task completes before it is performed again.
292
+ def execution_interval=(value)
293
+ if (value = value.to_f) <= 0.0
294
+ raise ArgumentError.new 'must be greater than zero'
295
+ else
296
+ begin
297
+ mutex.lock
298
+ @execution_interval = value
299
+ ensure
300
+ mutex.unlock
301
+ end
302
+ end
303
+ end
304
+
305
+ # @!attribute [rw] timeout_interval
306
+ # @return [Fixnum]
307
+ # Number of seconds the task can run before it is considered failed.
308
+ def timeout_interval
309
+ mutex.lock
310
+ @timeout_interval
311
+ ensure
312
+ mutex.unlock
313
+ end
314
+
315
+ # @!attribute [rw] timeout_interval
316
+ # @return [Fixnum]
317
+ # Number of seconds the task can run before it is considered failed.
318
+ def timeout_interval=(value)
319
+ if (value = value.to_f) <= 0.0
320
+ raise ArgumentError.new('must be greater than zero')
321
+ else
322
+ begin
323
+ mutex.lock
324
+ @timeout_interval = value
325
+ ensure
326
+ mutex.unlock
327
+ end
328
+ end
329
+ end
330
+
331
+ private :post, :<<
332
+
333
+ protected # A T T E N Z I O N E A R E A P R O T E T T A
334
+
335
+ # @!visibility private
336
+ def shutdown_execution
337
+ @running.make_false
338
+ super
339
+ end
340
+
341
+ # @!visibility private
342
+ def kill_execution
343
+ @running.make_false
344
+ super
345
+ end
346
+
347
+ # @!visibility private
348
+ def schedule_next_task(interval = execution_interval)
349
+ Garcon::timer(interval, Garcon::Event.new, &method(:execute_task))
350
+ end
351
+
352
+ # @!visibility private
353
+ def execute_task(completion)
354
+ return unless @running.true?
355
+ Garcon::timer(execution_interval, completion, &method(:timeout_task))
356
+ _success, value, reason = @executor.execute(self)
357
+ if completion.try?
358
+ self.value = value
359
+ schedule_next_task
360
+ time = Time.now
361
+ observers.notify_observers do
362
+ [time, self.value, reason]
363
+ end
364
+ end
365
+ end
366
+
367
+ # @!visibility private
368
+ def timeout_task(completion)
369
+ return unless @running.true?
370
+ if completion.try?
371
+ self.value = value
372
+ schedule_next_task
373
+ observers.notify_observers(Time.now, nil, Garcon::TimeoutError.new)
374
+ end
375
+ end
376
+ end
377
+ end