knife-cloudformation 0.1.22 → 0.2.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +56 -2
  4. data/knife-cloudformation.gemspec +4 -7
  5. data/lib/chef/knife/cloudformation_create.rb +105 -245
  6. data/lib/chef/knife/cloudformation_describe.rb +50 -26
  7. data/lib/chef/knife/cloudformation_destroy.rb +17 -18
  8. data/lib/chef/knife/cloudformation_events.rb +48 -14
  9. data/lib/chef/knife/cloudformation_export.rb +117 -34
  10. data/lib/chef/knife/cloudformation_import.rb +124 -18
  11. data/lib/chef/knife/cloudformation_inspect.rb +159 -71
  12. data/lib/chef/knife/cloudformation_list.rb +20 -24
  13. data/lib/chef/knife/cloudformation_promote.rb +40 -0
  14. data/lib/chef/knife/cloudformation_update.rb +132 -15
  15. data/lib/chef/knife/cloudformation_validate.rb +35 -0
  16. data/lib/knife-cloudformation.rb +28 -0
  17. data/lib/knife-cloudformation/cache.rb +213 -35
  18. data/lib/knife-cloudformation/knife.rb +9 -0
  19. data/lib/knife-cloudformation/knife/base.rb +179 -0
  20. data/lib/knife-cloudformation/knife/stack.rb +94 -0
  21. data/lib/knife-cloudformation/knife/template.rb +174 -0
  22. data/lib/knife-cloudformation/monkey_patch.rb +8 -0
  23. data/lib/knife-cloudformation/monkey_patch/stack.rb +195 -0
  24. data/lib/knife-cloudformation/provider.rb +225 -0
  25. data/lib/knife-cloudformation/utils.rb +18 -98
  26. data/lib/knife-cloudformation/utils/animal_strings.rb +28 -0
  27. data/lib/knife-cloudformation/utils/debug.rb +31 -0
  28. data/lib/knife-cloudformation/utils/json.rb +64 -0
  29. data/lib/knife-cloudformation/utils/object_storage.rb +28 -0
  30. data/lib/knife-cloudformation/utils/output.rb +79 -0
  31. data/lib/knife-cloudformation/utils/path_selector.rb +99 -0
  32. data/lib/knife-cloudformation/utils/ssher.rb +29 -0
  33. data/lib/knife-cloudformation/utils/stack_exporter.rb +271 -0
  34. data/lib/knife-cloudformation/utils/stack_parameter_scrubber.rb +35 -0
  35. data/lib/knife-cloudformation/utils/stack_parameter_validator.rb +124 -0
  36. data/lib/knife-cloudformation/version.rb +2 -4
  37. metadata +47 -94
  38. data/Gemfile +0 -3
  39. data/Gemfile.lock +0 -90
  40. data/knife-cloudformation-0.1.20.gem +0 -0
  41. data/lib/knife-cloudformation/aws_commons.rb +0 -267
  42. data/lib/knife-cloudformation/aws_commons/stack.rb +0 -435
  43. data/lib/knife-cloudformation/aws_commons/stack_parameter_validator.rb +0 -79
  44. data/lib/knife-cloudformation/cloudformation_base.rb +0 -168
  45. data/lib/knife-cloudformation/export.rb +0 -174
@@ -0,0 +1,40 @@
1
+ require 'knife-cloudformation'
2
+
3
+ class Chef
4
+ class Knife
5
+ class CloudformationPromote < Knife
6
+
7
+ include KnifeCloudformation::Knife::Base
8
+
9
+ banner 'knife cloudformation promote NEW_STACK_NAME DESTINATION'
10
+
11
+ option(:accounts,
12
+ :long => '--accounts-file PATH',
13
+ :short => '-A PATH',
14
+ :description => 'JSON account file',
15
+ :proc => lambda{|v|
16
+ Chef::Config[:knife][:cloudformation][:promote_accounts] = JSON.load(File.read(v))
17
+ }
18
+ )
19
+
20
+ option(:storage_bucket,
21
+ :long => '--exports-bucket NAME',
22
+ :description => 'Bucket name containing the exports',
23
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:promote_exports_bucket] = v }
24
+ )
25
+
26
+ option(:storage_prefix,
27
+ :long => '--exports-prefix PREFIX',
28
+ :description => 'Prefix of stack key',
29
+ :proc => lambda{|v| Chef::Config[:knife][:cloudformation][:promote_exports_prefix] = v }
30
+ )
31
+
32
+
33
+ def _run
34
+ stack_name, destination = name_args
35
+
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -1,30 +1,147 @@
1
- require 'knife-cloudformation/cloudformation_base'
2
- require File.join(File.dirname(__FILE__), 'cloudformation_create')
1
+ require 'knife-cloudformation'
3
2
 
4
3
  class Chef
5
4
  class Knife
6
5
  class CloudformationUpdate < Knife
7
- banner 'knife cloudformation update NAME'
8
6
 
9
- include KnifeCloudformation::KnifeBase
10
- include CloudformationCreate::Options
7
+ include KnifeCloudformation::Knife::Base
8
+ include KnifeCloudformation::Knife::Template
9
+ include KnifeCloudformation::Knife::Stack
10
+
11
+ option(:file_path_prompt,
12
+ :long => '--[no-]file-path-prompt',
13
+ :description => 'Interactive prompt for template path discovery',
14
+ :boolean => true,
15
+ :default => false,
16
+ :proc => lambda {|val|
17
+ Chef::Config[:knife][:cloudformation][:file_path_prompt] = val
18
+ }
19
+ )
20
+ option(:apply_stacks,
21
+ :long => '--apply-stack NAME_OR_ID',
22
+ :description => 'Autofill parameters using existing stack outputs. Can be used multiple times',
23
+ :proc => lambda {|val|
24
+ Chef::Config[:knife][:cloudformation][:update] ||= Mash.new
25
+ Chef::Config[:knife][:cloudformation][:update][:apply_stacks] ||= []
26
+ Chef::Config[:knife][:cloudformation][:update][:apply_stacks].push(val).uniq!
27
+ }
28
+ )
11
29
 
12
- def create_stack(name, stack)
13
- begin
14
- res = aws_con.update_stack(name, stack)
15
- rescue => e
16
- ui.fatal "Failed to update stack #{name}. Reason: #{e}"
17
- _debug(e, "Generated template used:\n#{_format_json(stack['TemplateBody'])}")
30
+ banner 'knife cloudformation update NAME'
31
+
32
+ # Run the stack creation command
33
+ def _run
34
+ name = name_args.first
35
+ unless(name)
36
+ ui.fatal "Formation name must be specified!"
18
37
  exit 1
19
38
  end
39
+
40
+ stack = provider.stacks.get(name)
41
+
42
+ if(stack)
43
+ ui.info "#{ui.color('Cloud Formation:', :bold)} #{ui.color('update', :green)}"
44
+ file = load_template_file(:allow_missing)
45
+ stack_info = "#{ui.color('Name:', :bold)} #{name}"
46
+
47
+ if(Chef::Config[:knife][:cloudformation][:file])
48
+ stack_info << " #{ui.color('Path:', :bold)} #{Chef::Config[:knife][:cloudformation][:file]}"
49
+ else
50
+ stack_info << " #{ui.color('(no temlate update)', :yellow)}"
51
+ end
52
+ ui.info " -> #{stack_info}"
53
+
54
+ apply_stacks!(stack)
55
+
56
+ if(file)
57
+ redefault_stack_parameters(file, stack)
58
+ populate_parameters!(file)
59
+ file = translate_template(file)
60
+ stack.template = file
61
+ stack.parameters = Chef::Config[:knife][:cloudformation][:parameters]
62
+ else
63
+ stack_parameters_update!(stack)
64
+ end
65
+
66
+ stack.save
67
+
68
+ if(Chef::Config[:knife][:cloudformation][:poll])
69
+ poll_stack(stack.name)
70
+ provider.fetch_stacks
71
+ if(stack.success?)
72
+ ui.info "Stack update complete: #{ui.color('SUCCESS', :green)}"
73
+ provider.fetch_stacks
74
+ knife_output = Chef::Knife::CloudformationDescribe.new
75
+ knife_output.name_args.push(name)
76
+ knife_output.config[:outputs] = true
77
+ knife_output.run
78
+ else
79
+ ui.fatal "Update of stack #{ui.color(name, :bold)}: #{ui.color('FAILED', :red, :bold)}"
80
+ ui.info ""
81
+ knife_inspect = Chef::Knife::CloudformationInspect.new
82
+ knife_inspect.name_args.push(name)
83
+ knife_inspect.config[:instance_failure] = true
84
+ knife_inspect.run
85
+ exit 1
86
+ end
87
+ else
88
+ ui.warn 'Stack state polling has been disabled.'
89
+ ui.info "Stack update initialized for #{ui.color(name, :green)}"
90
+ end
91
+ else
92
+ ui.fatal "Failed to locate requested stack: #{ui.color(name, :red, :bold)}"
93
+ exit -1
94
+ end
95
+ end
96
+
97
+ # Update default values for parameters in template with
98
+ # currently used parameters on the existing stack
99
+ #
100
+ # @param template [Hash] stack template
101
+ # @param stack [Fog::Orchestration::Stack]
102
+ # @return [Hash]
103
+ def redefault_stack_parameters(template, stack)
104
+ stack.parameters.each do |key, value|
105
+ if(template['Parameters'][key])
106
+ template['Parameters'][key]['Default'] = value
107
+ end
108
+ end
109
+ template
20
110
  end
21
111
 
22
- def action_in_progress?(name)
23
- stack_status(name) == 'UPDATE_IN_PROGRESS'
112
+ # Apply any defined remote stacks
113
+ #
114
+ # @param stack [Miasma::Models::Orchestration::Stack]
115
+ # @return [Miasma::Models::Orchestration::Stack]
116
+ def apply_stacks!(stack)
117
+ remote_stacks = Chef::Config[:knife][:cloudformation].
118
+ fetch(:update, {}).fetch(:apply_stacks, [])
119
+ remote_stacks.each do |stack_name|
120
+ remote_stack = provider.stacks.get(stack_name)
121
+ if(remote_stack)
122
+ remote_stack.parameters.each do |key, value|
123
+ next if Chef::Config[:knife][:cloudformation][:stacks][:ignore_parameters].include?(key)
124
+ if(stack.parameters.has_key?(key))
125
+ stack.parameters[key] = value
126
+ end
127
+ end
128
+ else
129
+ ui.error "Failed to apply requested stack. Unable to locate. (#{stack_name})"
130
+ exit 1
131
+ end
132
+ end
133
+ stack
24
134
  end
25
135
 
26
- def action_successful?(name)
27
- stack_status(name) == 'UPDATE_COMPLETE'
136
+ # Update parameters within existing stack
137
+ #
138
+ # @param stack [Miasma::Models::Orchestration::Stack]
139
+ # @return [Miasma::Models::Orchestration::Stack]
140
+ def stack_parameters_update!(stack)
141
+ stack.parameters.each do |key, value|
142
+ answer = ui.ask_question("#{key.split(/([A-Z]+[^A-Z]*)/).find_all{|s|!s.empty?}.join(' ')}: ", :default => value)
143
+ stack.parameters[key] = answer
144
+ end
28
145
  end
29
146
 
30
147
  end
@@ -0,0 +1,35 @@
1
+ require 'pathname'
2
+ require 'sparkle_formation'
3
+ require 'knife-cloudformation'
4
+
5
+ class Chef
6
+ class Knife
7
+ # Cloudformation validate command
8
+ class CloudformationValidate < Knife
9
+
10
+ include KnifeCloudformation::Knife::Base
11
+ include KnifeCloudformation::Knife::Template
12
+
13
+ banner 'knife cloudformation validate'
14
+
15
+ def _run
16
+ file = load_template_file
17
+ ui.info "#{ui.color('Cloud Formation Validation: ', :bold)} #{Chef::Config[:knife][:cloudformation][:file].sub(Dir.pwd, '').sub(%r{^/}, '')}"
18
+ file = KnifeCloudformation::Utils::StackParameterScrubber.scrub!(file)
19
+ file = translate_template(file)
20
+ begin
21
+ result = provider.stacks.build(
22
+ :name => 'validation-stack',
23
+ :template => file
24
+ ).validate
25
+ ui.info ui.color(' -> VALID', :bold, :green)
26
+ rescue => e
27
+ ui.info ui.color(' -> INVALID', :bold, :red)
28
+ ui.fatal e.message
29
+ failed = true
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -1 +1,29 @@
1
1
  require 'knife-cloudformation/version'
2
+ require 'miasma'
3
+
4
+ module KnifeCloudformation
5
+
6
+ autoload :Provider, 'knife-cloudformation/provider'
7
+ autoload :Cache, 'knife-cloudformation/cache'
8
+ autoload :Export, 'knife-cloudformation/export'
9
+ autoload :Utils, 'knife-cloudformation/utils'
10
+ autoload :MonkeyPatch, 'knife-cloudformation/monkey_patch'
11
+ autoload :Knife, 'knife-cloudformation/knife'
12
+
13
+ end
14
+
15
+ class Chef
16
+ class Knife
17
+ autoload :CloudformationCreate, 'chef/knife/cloudformation_create'
18
+ autoload :CloudformationDescribe, 'chef/knife/cloudformation_describe'
19
+ autoload :CloudformationDestroy, 'chef/knife/cloudformation_destroy'
20
+ autoload :CloudformationEvents, 'chef/knife/cloudformation_events'
21
+ autoload :CloudformationExport, 'chef/knife/cloudformation_export'
22
+ autoload :CloudformationImport, 'chef/knife/cloudformation_import'
23
+ autoload :CloudformationInspect, 'chef/knife/cloudformation_inspect'
24
+ autoload :CloudformationList, 'chef/knife/cloudformation_list'
25
+ autoload :CloudformationPromote, 'chef/knife/cloudformation_promote'
26
+ autoload :CloudformationUpdate, 'chef/knife/cloudformation_update'
27
+ autoload :CloudformationValidate, 'chef/knife/cloudformation_validate'
28
+ end
29
+ end
@@ -1,15 +1,29 @@
1
1
  require 'digest/sha2'
2
+ require 'thread'
3
+ require 'knife-cloudformation'
4
+
5
+ begin
6
+ require 'redis-objects'
7
+ rescue LoadError
8
+ $stderr.puts 'The `redis-objects` gem is required for Cache support!'
9
+ raise
10
+ end
2
11
 
3
12
  module KnifeCloudformation
13
+ # Data caching helper
4
14
  class Cache
5
15
 
6
16
  class << self
7
17
 
18
+ # Configure the caching approach to use
19
+ #
20
+ # @param type [Symbol] :redis or :local
21
+ # @param args [Hash] redis connection arguments if used
8
22
  def configure(type, args={})
9
23
  type = type.to_sym
10
24
  case type
11
25
  when :redis
12
- require 'redis-objects'
26
+ @_pid = Process.pid
13
27
  Redis::Objects.redis = Redis.new(args)
14
28
  when :local
15
29
  else
@@ -18,14 +32,24 @@ module KnifeCloudformation
18
32
  enable(type)
19
33
  end
20
34
 
35
+ # Set enabled caching type
36
+ #
37
+ # @param type [Symbol]
38
+ # @return [Symbol]
21
39
  def enable(type)
22
40
  @type = type.to_sym
23
41
  end
24
42
 
43
+ # @return [Symbol] type of caching enabled
25
44
  def type
26
45
  @type || :local
27
46
  end
28
47
 
48
+ # Set/get time limit on data type
49
+ #
50
+ # @param kind [String, Symbol] data type
51
+ # @param seconds [Integer]
52
+ # return [Integer] seconds
29
53
  def apply_limit(kind, seconds=nil)
30
54
  @apply_limit ||= {}
31
55
  if(seconds)
@@ -34,62 +58,109 @@ module KnifeCloudformation
34
58
  @apply_limit[kind.to_sym].to_i
35
59
  end
36
60
 
61
+ # @return [Hash] default limits
37
62
  def default_limits
38
63
  (@apply_limit || {}).dup
39
64
  end
40
65
 
66
+ # Ping the redis connection and reconnect if dead
67
+ def redis_ping!
68
+ if((@_pid && @_pid != Process.pid) || !Redis::Objects.redis.connected?)
69
+ Redis::Objects.redis.client.reconnect
70
+ @_pid = Process.pid
71
+ end
72
+ end
73
+
41
74
  end
42
75
 
76
+ # @return [String] custom key for this cache
43
77
  attr_reader :key
44
- attr_reader :direct_store
45
78
 
79
+ # Create new instance
80
+ #
81
+ # @param key [String, Array]
46
82
  def initialize(key)
47
83
  if(key.respond_to?(:sort))
48
84
  key = key.flatten if key.respond_to?(:flatten)
49
85
  key = key.map(&:to_s).sort
50
86
  end
51
87
  @key = Digest::SHA256.hexdigest(key.to_s)
52
- @direct_store = {}
53
88
  @apply_limit = self.class.default_limits
54
89
  end
55
90
 
91
+ # Initialize a new data type
92
+ #
93
+ # @param name [Symbol] name of data
94
+ # @param kind [Symbol] data type
95
+ # @param args [Hash] options for data type
56
96
  def init(name, kind, args={})
57
- name = name.to_sym
58
- unless(@direct_store[name])
59
- full_name = [key, name.to_s].join('_')
60
- @direct_store[name] = get_storage(self.class.type, kind, full_name, args)
61
- end
97
+ get_storage(self.class.type, kind, name, args)
62
98
  true
63
99
  end
64
100
 
101
+ # @return [Hash] data registry
102
+ def registry
103
+ get_storage(self.class.type, :hash, "registry_#{key}")
104
+ end
105
+
106
+ # Clear data
107
+ #
108
+ # @param args [Symbol] list of names to delete
109
+ # @return [TrueClass]
110
+ # @note clears all data if no names provided
65
111
  def clear!(*args)
66
112
  internal_lock do
67
- args = @direct_store.keys if args.empty?
113
+ args = registry.keys if args.empty?
68
114
  args.each do |key|
69
- value = @direct_store[key]
115
+ value = self[key]
70
116
  if(value.respond_to?(:clear))
71
117
  value.clear
72
118
  elsif(value.respond_to?(:value))
73
119
  value.value = nil
74
120
  end
121
+ registry.delete(key)
75
122
  end
76
123
  yield if block_given?
77
124
  end
78
125
  true
79
126
  end
80
127
 
81
- def get_storage(store_type, data_type, full_name, args={})
128
+ # Fetch item from storage
129
+ #
130
+ # @param store_type [Symbol]
131
+ # @param data_type [Symbol]
132
+ # @param name [Symbol] name of data
133
+ # @param args [Hash] options for underlying storage
134
+ # @return [Object]
135
+ def get_storage(store_type, data_type, name, args={})
136
+ full_name = "#{key}_#{name}"
137
+ result = nil
82
138
  case store_type.to_sym
83
139
  when :redis
84
- get_redis_storage(data_type, full_name, args)
140
+ result = get_redis_storage(data_type, full_name.to_s, args)
85
141
  when :local
86
- get_local_storage(data_type, full_name, args)
142
+ @_local_cache ||= {}
143
+ unless(@_local_cache[full_name.to_s])
144
+ @_local_cache[full_name.to_s] = get_local_storage(data_type, full_name.to_s, args)
145
+ end
146
+ result = @_local_cache[full_name.to_s]
87
147
  else
88
148
  raise TypeError.new("Unsupported caching storage type encountered: #{store_type}")
89
149
  end
150
+ unless(full_name == "#{key}_registry_#{key}")
151
+ registry[name.to_s] = data_type
152
+ end
153
+ result
90
154
  end
91
155
 
156
+ # Fetch item from redis storage
157
+ #
158
+ # @param data_type [Symbol]
159
+ # @param full_name [Symbol]
160
+ # @param args [Hash]
161
+ # @return [Object]
92
162
  def get_redis_storage(data_type, full_name, args={})
163
+ self.class.redis_ping!
93
164
  case data_type.to_sym
94
165
  when :array
95
166
  Redis::List.new(full_name, {:marshal => true}.merge(args))
@@ -98,7 +169,7 @@ module KnifeCloudformation
98
169
  when :value
99
170
  Redis::Value.new(full_name, {:marshal => true}.merge(args))
100
171
  when :lock
101
- Redis::Lock.new(full_name, {:expiration => 3, :timeout => 0.1}.merge(args))
172
+ Redis::Lock.new(full_name, {:expiration => 60, :timeout => 0.1}.merge(args))
102
173
  when :stamped
103
174
  Stamped.new(full_name.sub("#{key}_", '').to_sym, get_redis_storage(:value, full_name), self)
104
175
  else
@@ -106,43 +177,76 @@ module KnifeCloudformation
106
177
  end
107
178
  end
108
179
 
180
+ # Fetch item from local storage
181
+ #
182
+ # @param data_type [Symbol]
183
+ # @param full_name [Symbol]
184
+ # @param args [Hash]
185
+ # @return [Object]
186
+ # @todo make proper singleton for local storage
109
187
  def get_local_storage(data_type, full_name, args={})
110
- case data_type.to_sym
111
- when :array
112
- []
113
- when :hash
114
- {}
115
- when :value
116
- LocalValue.new
117
- when :lock
118
- LocalLock.new
119
- when :stamped
120
- Stamped.new(full_name.sub("#{key}_", '').to_sym, get_local_storage(:value, full_name), self)
121
- else
122
- raise TypeError.new("Unsupported caching data type encountered: #{data_type}")
123
- end
188
+ @storage ||= {}
189
+ @storage[full_name] ||= case data_type.to_sym
190
+ when :array
191
+ []
192
+ when :hash
193
+ {}
194
+ when :value
195
+ LocalValue.new
196
+ when :lock
197
+ LocalLock.new(full_name, {:expiration => 60, :timeout => 0.1}.merge(args))
198
+ when :stamped
199
+ Stamped.new(full_name.sub("#{key}_", '').to_sym, get_local_storage(:value, full_name), self)
200
+ else
201
+ raise TypeError.new("Unsupported caching data type encountered: #{data_type}")
202
+ end
124
203
  end
125
204
 
205
+ # Execute block within internal lock
206
+ #
207
+ # @return [Object] result of yield
208
+ # @note for internal use
126
209
  def internal_lock
127
- get_storage(self.class.type, :lock, :internal_access, :timeout => 20).lock do
210
+ get_storage(self.class.type, :lock, :internal_access, :timeout => 20, :expiration => 120).lock do
128
211
  yield
129
212
  end
130
213
  end
131
214
 
215
+ # Fetch data
216
+ #
217
+ # @param name [String, Symbol]
218
+ # @return [Object, NilClass]
132
219
  def [](name)
133
- internal_lock do
134
- @direct_store[name.to_sym]
220
+ if(kind = registry[name.to_s])
221
+ get_storage(self.class.type, kind, name)
222
+ else
223
+ nil
135
224
  end
136
225
  end
137
226
 
227
+ # Set data
228
+ #
229
+ # @param key [Object]
230
+ # @param val [Object]
231
+ # @note this will never work, thus you should never use it
138
232
  def []=(key, val)
139
233
  raise 'Setting backend data is not allowed'
140
234
  end
141
235
 
236
+ # Check if cache time has expired
237
+ #
238
+ # @param key [String, Symbol] value key
239
+ # @param stamp [Time, Integer]
240
+ # @return [TrueClass, FalseClass]
142
241
  def time_check_allow?(key, stamp)
143
242
  Time.now.to_i - stamp.to_i > apply_limit(key)
144
243
  end
145
244
 
245
+ # Apply time limit for data type
246
+ #
247
+ # @param kind [String, Symbol] data type
248
+ # @param seconds [Integer]
249
+ # return [Integer]
146
250
  def apply_limit(kind, seconds=nil)
147
251
  @apply_limit ||= {}
148
252
  if(seconds)
@@ -151,50 +255,124 @@ module KnifeCloudformation
151
255
  @apply_limit[kind.to_sym].to_i
152
256
  end
153
257
 
258
+ # Perform action within lock
259
+ #
260
+ # @param lock_name [String, Symbol] name of lock
261
+ # @param raise_on_locked [TrueClass, FalseClass] raise execption if lock wait times out
262
+ # @return [Object] result of yield
263
+ def locked_action(lock_name, raise_on_locked=false)
264
+ begin
265
+ self[lock_name].lock do
266
+ yield
267
+ end
268
+ rescue Redis::Lock::LockTimeout
269
+ raise if raise_on_locked
270
+ end
271
+ end
154
272
 
273
+ # Simple value for memory cache
155
274
  class LocalValue
275
+ # @return [Object] value
156
276
  attr_accessor :value
157
277
  def initialize(*args)
158
278
  @value = nil
159
279
  end
160
280
  end
161
281
 
282
+ # Simple lock for memory cache
162
283
  class LocalLock
163
- def initialize(*args)
284
+
285
+ # @return [Symbol] key name
286
+ attr_reader :_key
287
+ # @return [Numeric] timeout
288
+ attr_reader :_timeout
289
+ # @return [Mutex] underlying lock
290
+ attr_reader :_lock
291
+
292
+ # Create new instance
293
+ #
294
+ # @param name [Symbol] name of lock
295
+ # @param args [Hash]
296
+ # @option args [Numeric] :timeout
297
+ def initialize(name, args={})
298
+ @_key = name
299
+ @_timeout = args.fetch(:timeout, -1).to_f
300
+ @_lock = Mutex.new
164
301
  end
165
302
 
303
+ # Aquire lock and yield
304
+ #
305
+ # @yield block to execute within lock
306
+ # @return [Object] result of yield
166
307
  def lock
167
- yield
308
+ locked = false
309
+ attempt_start = Time.now.to_f
310
+ while(!locked && (_timeout < 0 || Time.now.to_f - attempt_start < _timeout))
311
+ locked = _lock.try_lock
312
+ end
313
+ if(locked)
314
+ begin
315
+ yield
316
+ ensure
317
+ _lock.unlock if _lock.locked?
318
+ end
319
+ else
320
+ raise Redis::Lock::LockTimeout.new "Timeout on lock #{_key} exceeded #{_timeout} sec"
321
+ end
168
322
  end
169
323
 
324
+ # Clear the lock
325
+ #
326
+ # @note this is a noop
170
327
  def clear
328
+ # noop
171
329
  end
172
330
  end
173
331
 
332
+ # Wrapper to auto stamp values
174
333
  class Stamped
175
334
 
335
+ # Create new instance
336
+ #
337
+ # @param name [String, Symbol]
338
+ # @param base [Redis::Value, LocalValue]
339
+ # @param cache [Cache]
176
340
  def initialize(name, base, cache)
177
341
  @name = name.to_sym
178
342
  @base = base
179
343
  @cache = cache
180
344
  end
181
345
 
346
+ # @return [Object] value stored
182
347
  def value
183
348
  @base.value[:value] if set?
184
349
  end
185
350
 
351
+ # Store value and update timestamp
352
+ #
353
+ # @param v [Object] value
354
+ # @return [Object]
186
355
  def value=(v)
187
- @base.value = {:stamp => Time.now.to_i, :value => v}
356
+ @base.value = {:stamp => Time.now.to_f, :value => v}
357
+ v
188
358
  end
189
359
 
360
+ # @return [TrueClass, FalseClass] is value set
190
361
  def set?
191
362
  @base.value.is_a?(Hash)
192
363
  end
193
364
 
365
+ # @return [Float] timestamp of last set (or 0.0 if unset)
194
366
  def stamp
195
- @base.value[:stamp] if set?
367
+ set? ? @base.value[:stamp] : 0.0
368
+ end
369
+
370
+ # Force a timestamp update
371
+ def restamp!
372
+ self.value = value
196
373
  end
197
374
 
375
+ # @return [TrueClass, FalseClass] update is allowed based on stamp and limits
198
376
  def update_allowed?
199
377
  !set? || @cache.time_check_allow?(@name, @base.value[:stamp])
200
378
  end