pulse_meter_core 0.4.13

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 (81) hide show
  1. data/.gitignore +19 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +8 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE +22 -0
  8. data/README.md +40 -0
  9. data/Rakefile +20 -0
  10. data/lib/pulse_meter/command_aggregator/async.rb +83 -0
  11. data/lib/pulse_meter/command_aggregator/sync.rb +18 -0
  12. data/lib/pulse_meter/command_aggregator/udp.rb +48 -0
  13. data/lib/pulse_meter/mixins/dumper.rb +87 -0
  14. data/lib/pulse_meter/mixins/utils.rb +155 -0
  15. data/lib/pulse_meter/observer.rb +118 -0
  16. data/lib/pulse_meter/observer/extended.rb +32 -0
  17. data/lib/pulse_meter/sensor.rb +61 -0
  18. data/lib/pulse_meter/sensor/base.rb +88 -0
  19. data/lib/pulse_meter/sensor/configuration.rb +106 -0
  20. data/lib/pulse_meter/sensor/counter.rb +39 -0
  21. data/lib/pulse_meter/sensor/hashed_counter.rb +36 -0
  22. data/lib/pulse_meter/sensor/hashed_indicator.rb +24 -0
  23. data/lib/pulse_meter/sensor/indicator.rb +35 -0
  24. data/lib/pulse_meter/sensor/multi.rb +97 -0
  25. data/lib/pulse_meter/sensor/timeline.rb +236 -0
  26. data/lib/pulse_meter/sensor/timeline_reduce.rb +68 -0
  27. data/lib/pulse_meter/sensor/timelined/average.rb +32 -0
  28. data/lib/pulse_meter/sensor/timelined/counter.rb +23 -0
  29. data/lib/pulse_meter/sensor/timelined/hashed_counter.rb +31 -0
  30. data/lib/pulse_meter/sensor/timelined/hashed_indicator.rb +30 -0
  31. data/lib/pulse_meter/sensor/timelined/indicator.rb +23 -0
  32. data/lib/pulse_meter/sensor/timelined/max.rb +19 -0
  33. data/lib/pulse_meter/sensor/timelined/median.rb +14 -0
  34. data/lib/pulse_meter/sensor/timelined/min.rb +19 -0
  35. data/lib/pulse_meter/sensor/timelined/multi_percentile.rb +34 -0
  36. data/lib/pulse_meter/sensor/timelined/percentile.rb +22 -0
  37. data/lib/pulse_meter/sensor/timelined/uniq_counter.rb +22 -0
  38. data/lib/pulse_meter/sensor/timelined/zset_based.rb +37 -0
  39. data/lib/pulse_meter/sensor/uniq_counter.rb +24 -0
  40. data/lib/pulse_meter/server.rb +0 -0
  41. data/lib/pulse_meter/server/command_line_options.rb +0 -0
  42. data/lib/pulse_meter/server/config_options.rb +0 -0
  43. data/lib/pulse_meter/server/sensors.rb +0 -0
  44. data/lib/pulse_meter/udp_server.rb +45 -0
  45. data/lib/pulse_meter_core.rb +66 -0
  46. data/pulse_meter_core.gemspec +33 -0
  47. data/spec/pulse_meter/command_aggregator/async_spec.rb +53 -0
  48. data/spec/pulse_meter/command_aggregator/sync_spec.rb +25 -0
  49. data/spec/pulse_meter/command_aggregator/udp_spec.rb +45 -0
  50. data/spec/pulse_meter/mixins/dumper_spec.rb +162 -0
  51. data/spec/pulse_meter/mixins/utils_spec.rb +212 -0
  52. data/spec/pulse_meter/observer/extended_spec.rb +92 -0
  53. data/spec/pulse_meter/observer_spec.rb +207 -0
  54. data/spec/pulse_meter/sensor/base_spec.rb +106 -0
  55. data/spec/pulse_meter/sensor/configuration_spec.rb +103 -0
  56. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  57. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +43 -0
  58. data/spec/pulse_meter/sensor/hashed_indicator_spec.rb +39 -0
  59. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  60. data/spec/pulse_meter/sensor/multi_spec.rb +137 -0
  61. data/spec/pulse_meter/sensor/timeline_spec.rb +88 -0
  62. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  63. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  64. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  65. data/spec/pulse_meter/sensor/timelined/hashed_indicator_spec.rb +8 -0
  66. data/spec/pulse_meter/sensor/timelined/indicator_spec.rb +6 -0
  67. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  68. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  69. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  70. data/spec/pulse_meter/sensor/timelined/multi_percentile_spec.rb +21 -0
  71. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  72. data/spec/pulse_meter/sensor/timelined/uniq_counter_spec.rb +9 -0
  73. data/spec/pulse_meter/sensor/uniq_counter_spec.rb +28 -0
  74. data/spec/pulse_meter/udp_server_spec.rb +36 -0
  75. data/spec/pulse_meter_spec.rb +73 -0
  76. data/spec/shared_examples/timeline_sensor.rb +439 -0
  77. data/spec/shared_examples/timelined_subclass.rb +23 -0
  78. data/spec/spec_helper.rb +37 -0
  79. data/spec/support/matchers.rb +34 -0
  80. data/spec/support/observered.rb +40 -0
  81. metadata +342 -0
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.sw?
19
+ /.idea
data/.rbenv-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p125
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color -fd
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ branches:
6
+ only:
7
+ - master
8
+ - develop
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ilya Averyanov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ [![Gem Version](https://badge.fury.io/rb/pulse_meter_core.png)](http://badge.fury.io/rb/pulse_meter_core)
2
+ [![Build Status](https://secure.travis-ci.org/savonarola/pulse_meter_core.png)](http://travis-ci.org/savonarola/pulse_meter_core)
3
+ [![Dependency Status](https://gemnasium.com/savonarola/pulse_meter_core.png)](https://gemnasium.com/savonarola/pulse_meter_core)
4
+ [![Code Climate](https://codeclimate.com/github/savonarola/pulse_meter_core.png)](https://codeclimate.com/github/savonarola/pulse_meter_core)
5
+
6
+ # PulseMeter
7
+
8
+ PulseMeter is a gem for fast and convenient realtime aggregating of software internal stats through Redis.
9
+
10
+ ## Live Demo
11
+
12
+ A small live demo of [pulse-meter](https://github.com/savonarola/pulse-meter) gem is located here: [rubybox.ru](http://rubybox.ru), its source code can be found here: [https://github.com/savonarola/pulse-meter-demo](https://github.com/savonarola/pulse-meter-demo)
13
+
14
+ ## Features
15
+
16
+ Gem contains core functionality for [pulse-meter](https://github.com/savonarola/pulse-meter) gem.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'pulse_meter_core'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself as:
31
+
32
+ $ gem install pulse_meter_core
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'yard'
5
+ require 'yard/rake/yardoc_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ YARD::Rake::YardocTask.new(:yard)
10
+
11
+ ROOT = File.dirname(__FILE__)
12
+
13
+ task :default => :spec
14
+
15
+ namespace :yard do
16
+ desc "Open doc index in a browser"
17
+ task :open do
18
+ system 'open', "#{ROOT}/doc/index.html"
19
+ end
20
+ end
@@ -0,0 +1,83 @@
1
+ require 'singleton'
2
+ require 'thread'
3
+
4
+ module PulseMeter
5
+ module CommandAggregator
6
+ class Async
7
+ include Singleton
8
+
9
+ MAX_QUEUE_LENGTH = 10_000
10
+
11
+ attr_reader :max_queue_length
12
+
13
+ def initialize
14
+ @max_queue_length = MAX_QUEUE_LENGTH
15
+ @queue = Queue.new
16
+ @buffer = []
17
+ @in_multi = false
18
+ @consumer_thread = run_consumer
19
+ at_exit{ wait_for_pending_events }
20
+ end
21
+
22
+ def multi
23
+ @in_multi = true
24
+ yield
25
+ ensure
26
+ @in_multi = false
27
+ send_buffer_to_queue
28
+ end
29
+
30
+ def method_missing(*args)
31
+ @buffer << args
32
+ send_buffer_to_queue unless @in_multi
33
+ end
34
+
35
+ def wait_for_pending_events(max_seconds = 1)
36
+ left_to_wait = max_seconds.to_f
37
+ sleep_step = 0.01
38
+ while has_pending_events? && left_to_wait > 0
39
+ left_to_wait -= sleep_step
40
+ sleep(sleep_step)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def has_pending_events?
47
+ !@queue.empty?
48
+ end
49
+
50
+ def send_buffer_to_queue
51
+ if @queue.size < @max_queue_length
52
+ @queue << @buffer
53
+ end
54
+ @buffer = []
55
+ end
56
+
57
+ def redis
58
+ PulseMeter.redis
59
+ end
60
+
61
+ def consume_commands
62
+ # redis and @queue are threadsafe
63
+ while commands = @queue.pop
64
+ begin
65
+ redis.multi do
66
+ commands.each do |command|
67
+ redis.send(*command)
68
+ end
69
+ end
70
+ rescue StandardError => e
71
+ PulseMeter.error "error in consumer thread: #{e}, #{e.backtrace.join("\n")}"
72
+ end
73
+ end
74
+ end
75
+
76
+ def run_consumer
77
+ Thread.new do
78
+ consume_commands
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,18 @@
1
+ require 'singleton'
2
+
3
+ module PulseMeter
4
+ module CommandAggregator
5
+ class Sync
6
+ include Singleton
7
+
8
+ def redis
9
+ PulseMeter.redis
10
+ end
11
+
12
+ def method_missing(*args, &block)
13
+ redis.send(*args, &block)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,48 @@
1
+ require 'socket'
2
+ require 'fcntl'
3
+
4
+ module PulseMeter
5
+ module CommandAggregator
6
+ class UDP
7
+
8
+ def initialize(host, port = nil)
9
+ @servers = if host.is_a?(Array)
10
+ host
11
+ else
12
+ [[host, port]]
13
+ end
14
+ raise ArgumentError, "No servers specified" if @servers.empty?
15
+ @buffer = []
16
+ @in_multi = false
17
+ @sock = UDPSocket.new
18
+ @sock.fcntl(Fcntl::F_SETFL, @sock.fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
19
+ end
20
+
21
+ def multi
22
+ @in_multi = true
23
+ yield
24
+ ensure
25
+ @in_multi = false
26
+ send_buffer
27
+ end
28
+
29
+ def method_missing(*args)
30
+ @buffer << args
31
+ send_buffer unless @in_multi
32
+ end
33
+
34
+ private
35
+
36
+ def send_buffer
37
+ data = @buffer.to_json
38
+ @sock.send(data, 0, *@servers.sample)
39
+ rescue StandardError => e
40
+ PulseMeter.error "error sending data: #{e}, #{e.backtrace.join("\n")}"
41
+ ensure
42
+ @buffer = []
43
+ end
44
+
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,87 @@
1
+ require 'yaml'
2
+
3
+ module PulseMeter
4
+ module Mixins
5
+ # Mixin with dumping utilities
6
+ module Dumper
7
+ # Prefix for Redis keys with dumped sensors' metadata
8
+ DUMP_REDIS_KEY = "pulse_meter:dump"
9
+
10
+ module InstanceMethods
11
+ # Serializes object and saves it to Redis
12
+ # @param safe [Boolean] forbids dump if sensor has already been dumped
13
+ # @raise [DumpConflictError] if object class conflicts with stored object class
14
+ # @raise [DumpError] if dumping fails for any reason
15
+ def dump!(safe = true)
16
+ ensure_storability!
17
+ serialized_obj = to_yaml
18
+ if safe
19
+ unless redis.hsetnx(DUMP_REDIS_KEY, name, serialized_obj)
20
+ stored = self.class.restore(name)
21
+ unless stored.class == self.class
22
+ raise DumpConflictError, "Attempt to create sensor #{name} of class #{self.class} but it already has class #{stored.class}"
23
+ end
24
+ end
25
+ else
26
+ redis.hset(DUMP_REDIS_KEY, name, serialized_obj)
27
+ end
28
+ rescue DumpError, RestoreError => exc
29
+ raise exc
30
+ rescue StandardError => exc
31
+ raise DumpError, "object cannot be dumped: #{exc}"
32
+ end
33
+
34
+ # Ensures that object is dumpable
35
+ # @raise [DumpError] if object cannot be dumped
36
+ def ensure_storability!
37
+ raise DumpError, "#name attribute must be readable" unless self.respond_to?(:name)
38
+ raise DumpError, "#redis attribute must be available" unless self.respond_to?(:redis) && self.redis
39
+ end
40
+
41
+ # Cleans up object dump in Redis
42
+ def cleanup_dump
43
+ redis.hdel(DUMP_REDIS_KEY, self.name)
44
+ end
45
+ end
46
+
47
+ module ClassMethods
48
+ # Restores object from Redis
49
+ # @param name [String] object name
50
+ # @return [Object]
51
+ # @raise [RestoreError] if object cannot be restored for any reason
52
+ def restore(name)
53
+ serialized_obj = PulseMeter.redis.hget(DUMP_REDIS_KEY, name)
54
+ YAML::load(serialized_obj)
55
+ rescue
56
+ raise RestoreError, "cannot restore #{name}"
57
+ end
58
+
59
+ # Lists all dumped objects' names
60
+ # @return [Array<String>]
61
+ # @raise [RestoreError] if list cannot be retrieved for any reason
62
+ def list_names
63
+ PulseMeter.redis.hkeys(DUMP_REDIS_KEY)
64
+ rescue
65
+ raise RestoreError, "cannot get data from redis"
66
+ end
67
+
68
+ # Safely restores all dumped objects
69
+ # @return [Array<Object>]
70
+ def list_objects
71
+ list_names.each_with_object([]) do |name, objects|
72
+ begin
73
+ objects << restore(name)
74
+ rescue
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def self.included(base)
81
+ base.send :include, InstanceMethods
82
+ base.send :extend, ClassMethods
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,155 @@
1
+ require 'securerandom'
2
+
3
+ module PulseMeter
4
+ module Mixins
5
+ # Mixin with various useful functions
6
+ module Utils
7
+ # Tries to find a class with the name specified in the argument string
8
+ # @param const_name [String] class name
9
+ # @return [Class] if given class definde
10
+ # @return [NilClass] if given class is not defined
11
+ def constantize(const_name)
12
+ return unless const_name.respond_to?(:to_s)
13
+ const_name.to_s.split('::').reduce(Module, :const_get)
14
+ rescue NameError
15
+ nil
16
+ end
17
+
18
+ # Ensures that hash value specified by key is Array
19
+ # @param options [Hash] hash to be looked at
20
+ # @param key [Object] hash key
21
+ # @param default [Object] default value to be returned
22
+ # @raise [ArgumentError] unless value is Array
23
+ # @return [Array]
24
+ def assert_array!(options, key, default = nil)
25
+ value = options[key] || default
26
+ raise ArgumentError, "#{key} should be defined" unless value
27
+ raise ArgumentError, "#{key} should be array" unless value.is_a?(Array)
28
+ value
29
+ end
30
+
31
+ # Ensures that hash value specified by key can be converted to positive integer.
32
+ # In case it can makes in-place conversion and returns the value.
33
+ # @param options [Hash] hash to be looked at
34
+ # @param key [Object] hash key
35
+ # @param default [Object] default value to be returned
36
+ # @raise [ArgumentError] unless value is positive integer
37
+ # @return [Fixnum]
38
+ def assert_positive_integer!(options, key, default = nil)
39
+ value = options[key] || default
40
+ raise ArgumentError, "#{key} should be defined" unless value
41
+ raise ArgumentError, "#{key} should be integer" unless value.respond_to?(:to_i)
42
+ raise ArgumentError, "#{key} should be positive" unless value.to_i > 0
43
+ options[key] = value.to_i
44
+ end
45
+
46
+ # Ensures that hash value specified by key is can be converted to float
47
+ # and it is within given range.
48
+ # In case it can makes in-place conversion and returns the value.
49
+ # @param options [Hash] hash to be looked at
50
+ # @param key [Object] hash key
51
+ # @param from [Float] lower bound
52
+ # @param to [Float] upper bound
53
+ # @raise [ArgumentError] unless value is float within given range
54
+ # @return [Float]
55
+ def assert_ranged_float!(options, key, from, to)
56
+ f = options[key]
57
+ raise ArgumentError, "#{key} should be defined" unless f
58
+ raise ArgumentError, "#{key} should be float" unless f.respond_to?(:to_f)
59
+ f = f.to_f
60
+ raise ArgumentError, "#{key} should be between #{from} and #{to}" unless f >= from && f <= to
61
+ options[key] = f
62
+ end
63
+
64
+ # Generates uniq random string
65
+ # @return [String]
66
+ def uniqid
67
+ SecureRandom.hex(32)
68
+ end
69
+
70
+ # Capitalizes the first letter of each word in string
71
+ # @param str [String]
72
+ # @return [String]
73
+ # @raise [ArgumentError] unless passed value responds to to_s
74
+ def titleize(str)
75
+ raise ArgumentError unless str.respond_to?(:to_s)
76
+ str.to_s.split(/[\s_]+/).map(&:capitalize).join(' ')
77
+ end
78
+
79
+ # Converts string from snake_case to CamelCase
80
+ # @param str [String] string to be camelized
81
+ # @param first_letter_upper [TrueClass, FalseClass] says if the first letter must be uppercased
82
+ # @return [String]
83
+ # @raise [ArgumentError] unless passed value responds to to_s
84
+ def camelize(str, first_letter_upper = false)
85
+ raise ArgumentError unless str.respond_to?(:to_s)
86
+ terms = str.to_s.split(/_/)
87
+ first = terms.shift
88
+ (first_letter_upper ? first.capitalize : first.downcase) + terms.map(&:capitalize).join
89
+ end
90
+
91
+ # Converts string from CamelCase to snake_case
92
+ # @param str [String] string to be underscore
93
+ # @return [String]
94
+ # @raise [ArgumentError] unless passed value responds to to_s
95
+ def underscore(str)
96
+ raise ArgumentError unless str.respond_to?(:to_s)
97
+ str.to_s.gsub(/::/, '/').
98
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
99
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
100
+ tr("-", "_").
101
+ downcase
102
+ end
103
+
104
+ # Converts string of the form YYYYmmddHHMMSS (considered as UTC) to Time
105
+ # @param str [String] string to be converted
106
+ # @return [Time]
107
+ # @raise [ArgumentError] unless passed value responds to to_s
108
+ def parse_time(str)
109
+ raise ArgumentError unless str.respond_to?(:to_s)
110
+ m = str.to_s.match(/\A(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\z/)
111
+ if m
112
+ Time.gm(*m.captures.map(&:to_i))
113
+ else
114
+ raise ArgumentError, "`#{str}' is not a YYYYmmddHHMMSS time"
115
+ end
116
+ end
117
+
118
+ # Symbolizes hash keys
119
+ def symbolize_keys(h)
120
+ h.each_with_object({}) do |(k, v), acc|
121
+ new_k = if k.respond_to?(:to_sym)
122
+ k.to_sym
123
+ else
124
+ k
125
+ end
126
+ acc[new_k] = v
127
+ end
128
+ end
129
+
130
+ # Deeply capitalizes Array values or Hash keys
131
+ def camelize_keys(item)
132
+ case item
133
+ when Array
134
+ item.map{|i| camelize_keys(i)}
135
+ when Hash
136
+ item.each_with_object({}) { |(k, v), h| h[camelize(k)] = camelize_keys(v)}
137
+ else
138
+ item
139
+ end
140
+ end
141
+
142
+ # Yields block for each subset of given array
143
+ # @param array [Array] given array
144
+ def each_subset(array)
145
+ subsets_of(array).each {|subset| yield(subset)}
146
+ end
147
+
148
+ # Returs all array's subsets
149
+ # @param array [Array]
150
+ def subsets_of(array)
151
+ 0.upto(array.length).flat_map { |n| array.combination(n).to_a }
152
+ end
153
+ end
154
+ end
155
+ end