wonkavision 0.5.11 → 0.6.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 (51) hide show
  1. data/CHANGELOG.rdoc +3 -0
  2. data/lib/wonkavision.rb +28 -1
  3. data/lib/wonkavision/aggregation.rb +21 -0
  4. data/lib/wonkavision/event_coordinator.rb +19 -7
  5. data/lib/wonkavision/extensions/symbol.rb +55 -0
  6. data/lib/wonkavision/facts.rb +27 -0
  7. data/lib/wonkavision/local_job_queue.rb +28 -0
  8. data/lib/wonkavision/message_mapper.rb +2 -2
  9. data/lib/wonkavision/message_mapper/map.rb +60 -8
  10. data/lib/wonkavision/persistence/mongo.rb +95 -0
  11. data/lib/wonkavision/plugins.rb +2 -1
  12. data/lib/wonkavision/plugins/analytics/aggregation.rb +139 -0
  13. data/lib/wonkavision/plugins/analytics/aggregation/aggregation_spec.rb +53 -0
  14. data/lib/wonkavision/plugins/analytics/aggregation/attribute.rb +22 -0
  15. data/lib/wonkavision/plugins/analytics/aggregation/dimension.rb +64 -0
  16. data/lib/wonkavision/plugins/analytics/aggregation/measure.rb +240 -0
  17. data/lib/wonkavision/plugins/analytics/cellset.rb +171 -0
  18. data/lib/wonkavision/plugins/analytics/facts.rb +106 -0
  19. data/lib/wonkavision/plugins/analytics/handlers/apply_aggregation.rb +35 -0
  20. data/lib/wonkavision/plugins/analytics/handlers/split_by_aggregation.rb +60 -0
  21. data/lib/wonkavision/plugins/analytics/member_filter.rb +106 -0
  22. data/lib/wonkavision/plugins/analytics/mongo.rb +6 -0
  23. data/lib/wonkavision/plugins/analytics/persistence/hash_store.rb +59 -0
  24. data/lib/wonkavision/plugins/analytics/persistence/mongo_store.rb +85 -0
  25. data/lib/wonkavision/plugins/analytics/persistence/store.rb +105 -0
  26. data/lib/wonkavision/plugins/analytics/query.rb +76 -0
  27. data/lib/wonkavision/plugins/event_handling.rb +15 -3
  28. data/lib/wonkavision/version.rb +1 -1
  29. data/test/aggregation_spec_test.rb +99 -0
  30. data/test/aggregation_test.rb +170 -0
  31. data/test/analytics/test_aggregation.rb +78 -0
  32. data/test/apply_aggregation_test.rb +92 -0
  33. data/test/attribute_test.rb +26 -0
  34. data/test/cellset_test.rb +200 -0
  35. data/test/dimension_test.rb +186 -0
  36. data/test/facts_test.rb +146 -0
  37. data/test/hash_store_test.rb +112 -0
  38. data/test/log/test.log +96844 -0
  39. data/test/map_test.rb +48 -1
  40. data/test/measure_test.rb +146 -0
  41. data/test/member_filter_test.rb +143 -0
  42. data/test/mongo_store_test.rb +115 -0
  43. data/test/query_test.rb +106 -0
  44. data/test/split_by_aggregation_test.rb +114 -0
  45. data/test/store_test.rb +71 -0
  46. data/test/symbol_test.rb +62 -0
  47. data/test/test_activity_models.rb +1 -1
  48. data/test/test_aggregation.rb +42 -0
  49. data/test/test_data.tuples +100 -0
  50. data/test/test_helper.rb +7 -0
  51. metadata +57 -5
@@ -1,3 +1,6 @@
1
+ == 0.6.0
2
+ * Initial, experimental draft of real time analytics framework.
3
+
1
4
  == 0.5.11
2
5
  * Guess that last change doesn't work in Rails 3 :) Fixed things up a different way to get the same effect, but more compatible.
3
6
 
@@ -1,6 +1,7 @@
1
1
  require "rubygems"
2
2
  require "active_support"
3
3
  require "active_support/hash_with_indifferent_access" unless defined?(HashWithIndifferentAccess)
4
+ require "active_support/core_ext"
4
5
 
5
6
  dir = File.dirname(__FILE__)
6
7
  ["support",
@@ -9,9 +10,12 @@ dir = File.dirname(__FILE__)
9
10
  "event",
10
11
  "event_context",
11
12
  "event_namespace",
13
+ "local_job_queue",
12
14
  "event_coordinator",
13
15
  "event_binding",
14
16
  "event_handler",
17
+ "facts",
18
+ "aggregation",
15
19
  "message_mapper/indifferent_access",
16
20
  "message_mapper/map",
17
21
  "message_mapper",
@@ -20,14 +24,33 @@ dir = File.dirname(__FILE__)
20
24
  "plugins/business_activity/event_binding",
21
25
  "plugins/business_activity",
22
26
  "plugins/timeline",
27
+ "extensions/symbol",
28
+ "plugins/analytics/member_filter",
29
+ "plugins/analytics/persistence/store",
30
+ "plugins/analytics/persistence/hash_store",
31
+ "plugins/analytics/facts",
32
+ "plugins/analytics/aggregation/aggregation_spec",
33
+ "plugins/analytics/aggregation/attribute",
34
+ "plugins/analytics/aggregation/dimension",
35
+ "plugins/analytics/aggregation/measure",
36
+ "plugins/analytics/aggregation",
37
+ "plugins/analytics/cellset",
38
+ "plugins/analytics/query",
23
39
  "acts_as_oompa_loompa",
24
40
  "persistence/mongo_mapper_adapter",
25
- "persistence/mongoid_adapter"].each {|lib|require File.join(dir,'wonkavision',lib)}
41
+ "persistence/mongoid_adapter",
42
+ "plugins/analytics/mongo"
43
+ ].each {|lib|require File.join(dir,'wonkavision',lib)}
44
+
45
+
46
+
26
47
 
27
48
  #require File.join(dir,"cubicle","mongo_mapper","aggregate_plugin") if defined?(MongoMapper::Document)
28
49
 
29
50
  module Wonkavision
30
51
 
52
+ NaN = 0.0 / 0.0
53
+
31
54
  # def self.register_cubicle_directory(directory_path, recursive=true)
32
55
  # searcher = "#{recursive ? "*" : "**/*"}.rb"
33
56
  # Dir[File.join(directory_path,searcher)].each {|cubicle| require cubicle}
@@ -73,3 +96,7 @@ module Wonkavision
73
96
 
74
97
 
75
98
  end
99
+
100
+ #Load event handlers for analytics
101
+ # dir = File.dirname(__FILE__)
102
+ Dir[File.join(dir,"wonkavision","plugins/analytics/handlers/**/*.rb")].each {|lib|require lib}
@@ -0,0 +1,21 @@
1
+ module Wonkavision
2
+ module Aggregation
3
+
4
+ def self.all
5
+ Wonkavision::Plugins::Aggregation.all
6
+ end
7
+
8
+ def self.persistence
9
+ @persistence
10
+ end
11
+
12
+ def self.included(handler)
13
+ handler.class_eval do
14
+ extend Plugins
15
+ use Plugins::Aggregation
16
+ end
17
+
18
+ super
19
+ end
20
+ end
21
+ end
@@ -2,11 +2,13 @@ module Wonkavision
2
2
  class EventCoordinator
3
3
 
4
4
  attr_reader :root_namespace
5
+ attr_accessor :broadcast_transport, :job_queue
5
6
 
6
7
  def initialize
7
8
  @root_namespace = Wonkavision::EventNamespace.new
8
- @lock = Mutex.new
9
+ #@lock = Mutex.new
9
10
  #@event_cache = {}
11
+
10
12
  @incoming_event_filters = []
11
13
  end
12
14
 
@@ -22,7 +24,7 @@ module Wonkavision
22
24
  self.instance_eval(&block)
23
25
  end
24
26
 
25
- def map ()
27
+ def map
26
28
  yield root_namespace if block_given?
27
29
  end
28
30
 
@@ -33,17 +35,27 @@ module Wonkavision
33
35
  end
34
36
 
35
37
  def receive_event(event_path, event_data)
36
- @lock.synchronize do
37
- #If process_incoming_event returns nil or false, it means a filter chose to abort
38
- #the event processing, in which case we'll break for lunch.
38
+ #@lock.synchronize do
39
+ #If process_incoming_event returns nil or false, it means a filter chose to abort
40
+ #the event processing, in which case we'll break for lunch.
39
41
  return unless event_data = process_incoming_event(event_path,event_data)
40
42
 
41
43
  event_path = Wonkavision.normalize_event_path(event_path)
42
44
  targets = root_namespace.find_matching_segments(event_path).values
43
- #If the event wasn't matched, maybe someone is subscribing to '/*' ?
45
+ #If the event wasn't matched, maybe someone is subscribing to '/*' ?
44
46
  targets = [root_namespace] if targets.blank?
45
47
  targets.each{|target|target.notify_subscribers(event_data,event_path)}
46
- end
48
+ #end
49
+ end
50
+
51
+ def publish(event_path, event_data)
52
+ raise "No transport was configured with the EventCoordinator to deliver broadcast messages. Please set Wonkavision.event_coordinator.broadcast_transport = <some transport>." unless broadcast_transport
53
+ broadcast_transport.publish(event_path, event_data)
54
+ end
55
+
56
+ def submit_job(event_path, event_data)
57
+ job_queue ? job_queue.publish(event_path,event_data) :
58
+ receive_event(event_path, event_data)
47
59
  end
48
60
 
49
61
  protected
@@ -0,0 +1,55 @@
1
+ # encoding: UTF-8
2
+ # This concept is torn from the chest cavity of
3
+ # jnunemakers plucky library (https://github.com/jnunemaker/plucky/blob/master/lib/plucky/extensions/symbol.rb)
4
+ module Wonkavision
5
+ module Extensions
6
+ module Symbol
7
+
8
+ [:key, :caption, :sort].each do |dimension_attribute|
9
+ define_method(dimension_attribute) do
10
+ _filter(dimension_attribute, :member_type=>:dimension)
11
+ end unless method_defined?(dimension_attribute)
12
+ end
13
+
14
+ [:sum, :sum2, :count].each do |measure_attribute|
15
+ define_method(measure_attribute) do
16
+ _filter(measure_attribute, :member_type=>:measure)
17
+ end unless method_defined?(measure_attribute)
18
+ end
19
+
20
+ def[](name)
21
+ _filter(name)
22
+ end
23
+
24
+ def method_missing(name,*args)
25
+ _filter(name)
26
+ end
27
+
28
+ private
29
+ def _member_type
30
+ self == :measures ? :measure : :dimension
31
+ end
32
+
33
+ def _is_member_name?
34
+ [:dimensions,:measures].include?(self) == false
35
+ end
36
+
37
+ def _filter(name, options={})
38
+ options[:member_type] ||= _member_type
39
+ if _is_member_name?
40
+ member_name = self
41
+ options[:attribute_name] = name
42
+ else
43
+ member_name = name
44
+ end
45
+ Wonkavision::Analytics::MemberFilter.new(member_name,options)
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+
52
+
53
+ class Symbol
54
+ include Wonkavision::Extensions::Symbol
55
+ end
@@ -0,0 +1,27 @@
1
+ module Wonkavision
2
+ module Facts
3
+
4
+ def self.persistence
5
+ @persistence
6
+ end
7
+
8
+ #current only supports :mongo
9
+ def self.persistence=(backend)
10
+ case backend
11
+ when :mongo then require File.dirname(__FILE__) + "/plugins/analytics/mongo"
12
+ else
13
+ raise "#{backend} is not a supported back end for Wonkavision analytics"
14
+ end
15
+ @persistence = backend
16
+ end
17
+
18
+ def self.included(facts)
19
+ facts.class_eval do
20
+ extend Plugins
21
+ use Plugins::EventHandling
22
+ use Plugins::Callbacks
23
+ use Plugins::Facts
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ require 'thread'
2
+
3
+ module Wonkavision
4
+ class LocalJobQueue
5
+ attr_reader :queue
6
+ def initialize(options={})
7
+ worker_count = options[:workers] || 2
8
+ @queue = Queue.new
9
+ @workers = []
10
+ worker_count.times do
11
+ Thread.new do
12
+ while true
13
+ if msg = @queue.pop
14
+ Wonkavision.event_coordinator.receive_event(msg[0],msg[1])
15
+ else
16
+ sleep 0.1
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def publish(event_path,event)
24
+ @queue << [event_path, event]
25
+ end
26
+
27
+ end
28
+ end
@@ -11,11 +11,11 @@ module Wonkavision
11
11
  MessageMapper.maps[map_name] = block
12
12
  end
13
13
 
14
- def execute(map,data_source)
14
+ def execute(map,data_source,options={})
15
15
  map_block = map.kind_of?(Proc) ? map : MessageMapper.maps[map]
16
16
 
17
17
  raise "#{map} not found" unless map_block
18
- MessageMapper::Map.new.execute(data_source, map_block)
18
+ MessageMapper::Map.new.execute(data_source, map_block, options)
19
19
  end
20
20
 
21
21
  def register_map_directory(directory_path, recursive=true)
@@ -5,12 +5,14 @@ module Wonkavision
5
5
  include IndifferentAccess
6
6
 
7
7
  def initialize(context = nil)
8
+ @write_nils = true
8
9
  @context_stack = []
9
10
  @context_stack.push(context) if context
10
11
  @formats = default_formats
11
12
  end
12
13
 
13
- def execute(context,map_block)
14
+ def execute(context,map_block,options={})
15
+ @write_nils = options[:write_nils].nil? ? true : options[:write_nils]
14
16
  @context_stack.push(context)
15
17
  instance_eval(&map_block)
16
18
  @context_stack.clear
@@ -25,6 +27,14 @@ module Wonkavision
25
27
  @context_stack[-1]
26
28
  end
27
29
 
30
+ def ignore_nil!
31
+ @write_nils = false
32
+ end
33
+
34
+ def write_nil!
35
+ @write_nils = true
36
+ end
37
+
28
38
  def from (context,&block)
29
39
  raise "No block ws provided to 'from'" unless block
30
40
  return if context.nil?
@@ -146,7 +156,7 @@ module Wonkavision
146
156
  if kind_of?(Date)
147
157
  self
148
158
  elsif respond_to?(:to_date)
149
- to_time
159
+ to_date
150
160
  elsif (date_str=to_s) && date_str.length > 0
151
161
  begin
152
162
  Date.parse(date_str)
@@ -187,7 +197,45 @@ module Wonkavision
187
197
  end
188
198
  end
189
199
 
200
+ def duration(*args, &block)
201
+ opts = args.extract_options! || {}
202
+
203
+ from = opts.delete(:from)
204
+ to = opts.delete(:to)
205
+
206
+ return nil unless from || to
207
+
208
+ from ||= Time.now; to ||= Time.now
209
+
210
+ unit = opts.delete(:in) || opts.delete(:unit) || :seconds
211
+
212
+ duration = convert_seconds(to-from,unit)
213
+
214
+ assignment = {args.shift => duration}
215
+ args << assignment << opts
216
+
217
+ value(*args, &block)
218
+
219
+ end
220
+ alias :elapsed :duration
221
+
190
222
  private
223
+
224
+ def convert_seconds(duration, unit)
225
+ duration /
226
+ case unit.to_s
227
+ when "seconds" then 1
228
+ when "minutes" then 60
229
+ when "hours" then 60 * 60
230
+ when "days" then 60 * 60 * 24
231
+ when "weeks" then 60 * 60 * 24 * 7
232
+ when "months" then 60 * 60 * 24 * 30
233
+ when "years" then 60 * 60 * 24 * 365
234
+ else raise "Cannot convert duration to unknown time unit #{unit}"
235
+ end
236
+
237
+ end
238
+
191
239
  def format_value(val,opts={})
192
240
  val = opts[:default] || opts[:default_value] if val.nil?
193
241
  return val if val.nil?
@@ -202,16 +250,18 @@ module Wonkavision
202
250
  end
203
251
 
204
252
  def extract_value_from_context(context,field_name,block=nil)
205
- if context.respond_to?(field_name.to_sym)
206
- value = context.instance_eval("self.#{field_name}")
207
- elsif context.respond_to?(:[])
253
+ value = nil
254
+ if context.respond_to?(:[])
208
255
  value = context[field_name]
209
256
  if value.nil? && field_name
210
257
  value = context[field_name.to_sym] || context[field_name.to_s]
211
258
  end
212
- else
213
- value = nil
214
259
  end
260
+
261
+ if context.respond_to?(field_name.to_sym)
262
+ value = context.instance_eval("self.#{field_name}")
263
+ end unless value
264
+
215
265
  value = value.instance_eval(&block) if block
216
266
  value
217
267
  end
@@ -219,7 +269,9 @@ module Wonkavision
219
269
  def set_value(field_name,val,opts={})
220
270
  if prefix = opts[:prefix]; field_name = "#{prefix}#{field_name}"; end
221
271
  if suffix = opts[:suffix]; field_name = "#{field_name}#{suffix}"; end
222
- self[field_name] = format_value(val,opts)
272
+ unless val.nil? && !@write_nils
273
+ self[field_name] = format_value(val,opts)
274
+ end
223
275
  end
224
276
 
225
277
  def default_formats
@@ -0,0 +1,95 @@
1
+ # encoding: UTF-8
2
+ # Taken almost verbatim from https://github.com/jnunemaker/mongomapper/blob/master/lib/mongo_mapper/connection.rb
3
+ require 'uri'
4
+ require 'mongo'
5
+
6
+ module Wonkavision
7
+ module Mongo
8
+ extend self
9
+
10
+ def initialize(connection=nil)
11
+ @connection = connection
12
+ end
13
+
14
+ # @api public
15
+ def connection
16
+ @connection ||= ::Mongo::Connection.new
17
+ end
18
+
19
+ # @api public
20
+ def connection=(new_connection)
21
+ @connection = new_connection
22
+ end
23
+
24
+ # @api public
25
+ def logger
26
+ connection.logger
27
+ end
28
+
29
+ # @api public
30
+ def database=(name)
31
+ @database = nil
32
+ @database_name = name
33
+ end
34
+
35
+ # @api public
36
+ def database
37
+ if @database_name.blank?
38
+ raise 'You forgot to set the default database name: database = "foobar"'
39
+ end
40
+
41
+ @database ||= connection.db(@database_name)
42
+ end
43
+
44
+ def config=(hash)
45
+ @config = hash
46
+ end
47
+
48
+ def config
49
+ raise 'Set config before connecting. config = {...}' unless defined?(@config)
50
+ @config
51
+ end
52
+
53
+ # @api private
54
+ def config_for_environment(environment)
55
+ env = config[environment]
56
+ return env if env['uri'].blank?
57
+
58
+ uri = URI.parse(env['uri'])
59
+ raise InvalidScheme.new('must be mongodb') unless uri.scheme == 'mongodb'
60
+ {
61
+ 'host' => uri.host,
62
+ 'port' => uri.port,
63
+ 'database' => uri.path.gsub(/^\//, ''),
64
+ 'username' => uri.user,
65
+ 'password' => uri.password,
66
+ }
67
+ end
68
+
69
+ def connect(environment, options={})
70
+ raise 'Set config before connecting. config = {...}' if config.blank?
71
+ env = config_for_environment(environment)
72
+ self.connection = ::Mongo::Connection.new(env['host'], env['port'], options)
73
+ self.database = env['database']
74
+ database.authenticate(env['username'], env['password']) if env['username'] && env['password']
75
+ end
76
+
77
+ def setup(config, environment, options={})
78
+ handle_passenger_forking
79
+ self.config = config
80
+ connect(environment, options)
81
+ end
82
+
83
+ def handle_passenger_forking
84
+ if defined?(PhusionPassenger)
85
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
86
+ connection.connect if forked
87
+ end
88
+ end
89
+ end
90
+
91
+ class Connection
92
+ include Wonkavision::Mongo
93
+ end
94
+ end
95
+ end