knife-cloudformation 0.1.22 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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