wonkavision 0.5.11 → 0.6.0

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