tally 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +217 -0
  3. data/Rakefile +32 -0
  4. data/app/assets/config/tally_manifest.js +2 -0
  5. data/app/assets/javascripts/tally/application.js +15 -0
  6. data/app/assets/stylesheets/tally/application.css +15 -0
  7. data/app/controllers/tally/application_controller.rb +17 -0
  8. data/app/controllers/tally/days_controller.rb +19 -0
  9. data/app/controllers/tally/keys_controller.rb +19 -0
  10. data/app/controllers/tally/records_controller.rb +21 -0
  11. data/app/helpers/tally/application_helper.rb +4 -0
  12. data/app/helpers/tally/increment_helper.rb +15 -0
  13. data/app/jobs/tally/application_job.rb +4 -0
  14. data/app/jobs/tally/calculator_runner_job.rb +12 -0
  15. data/app/models/tally/application_record.rb +7 -0
  16. data/app/models/tally/record.rb +30 -0
  17. data/app/presenters/tally/record_presenter.rb +29 -0
  18. data/bin/annotate +29 -0
  19. data/bin/appraisal +29 -0
  20. data/bin/bundle +114 -0
  21. data/bin/coderay +29 -0
  22. data/bin/htmldiff +29 -0
  23. data/bin/ldiff +29 -0
  24. data/bin/nokogiri +29 -0
  25. data/bin/pry +29 -0
  26. data/bin/rackup +29 -0
  27. data/bin/rails +14 -0
  28. data/bin/rake +29 -0
  29. data/bin/rspec +29 -0
  30. data/bin/rubocop +29 -0
  31. data/bin/ruby-parse +29 -0
  32. data/bin/ruby-rewrite +29 -0
  33. data/bin/sprockets +29 -0
  34. data/bin/thor +29 -0
  35. data/config/routes.rb +12 -0
  36. data/db/migrate/20181016172358_create_tally_records.rb +15 -0
  37. data/lib/tally.rb +77 -0
  38. data/lib/tally/archiver.rb +64 -0
  39. data/lib/tally/calculator.rb +30 -0
  40. data/lib/tally/calculator_runner.rb +56 -0
  41. data/lib/tally/calculators.rb +29 -0
  42. data/lib/tally/countable.rb +23 -0
  43. data/lib/tally/daily.rb +64 -0
  44. data/lib/tally/engine.rb +28 -0
  45. data/lib/tally/increment.rb +25 -0
  46. data/lib/tally/key_finder.rb +123 -0
  47. data/lib/tally/key_finder/entry.rb +66 -0
  48. data/lib/tally/keyable.rb +55 -0
  49. data/lib/tally/record_searcher.rb +110 -0
  50. data/lib/tally/sweeper.rb +46 -0
  51. data/lib/tally/version.rb +5 -0
  52. data/lib/tasks/tally_tasks.rake +16 -0
  53. metadata +256 -0
@@ -0,0 +1,64 @@
1
+ module Tally
2
+ # Contains all keys set for the given day
3
+ #
4
+ # Can be used to iterate over a large amount of smembers in the daily set
5
+ class Daily < KeyFinder
6
+
7
+ def daily_key
8
+ "#{ Tally.config.prefix }@#{ day_key }"
9
+ end
10
+
11
+ private
12
+
13
+ def all_keys
14
+ @keys ||= build_keys_from_redis
15
+ end
16
+
17
+ def build_keys_from_redis
18
+ result = []
19
+ cursor = ""
20
+
21
+ scan = scan_from_redis
22
+
23
+ while cursor != "0"
24
+ result << scan.last
25
+ cursor = scan.first
26
+
27
+ scan = scan_from_redis(cursor: cursor)
28
+ end
29
+
30
+ result.flatten
31
+ end
32
+
33
+ def day_key
34
+ @day_key ||= day.strftime(Tally.config.date_format)
35
+ end
36
+
37
+ def entry_regex
38
+ @entry_regex ||= Regexp.new("(?<record>[^:]+:[\\d]+)?:?(?<key>[^:]+)")
39
+ end
40
+
41
+ def scan_from_redis(cursor: "0")
42
+ Tally.redis do |conn|
43
+ conn.sscan(daily_key, cursor, match: scan_key, count: 25)
44
+ end
45
+ end
46
+
47
+ def scan_key
48
+ @scan_key ||= if key.present? && record.present?
49
+ "#{ record.model_name.i18n_key }:#{ record.id }:#{ key }"
50
+ elsif key.present? && type.present?
51
+ "#{ type }:*:#{ key }"
52
+ elsif record.present?
53
+ "#{ record.model_name.i18n_key }:#{ record.id }:*"
54
+ elsif type.present?
55
+ "#{ type }:*:*"
56
+ elsif key.present?
57
+ "*#{ key }"
58
+ else
59
+ "*"
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tally
4
+ class Engine < ::Rails::Engine
5
+
6
+ isolate_namespace Tally
7
+
8
+ engine_name "tally"
9
+
10
+ # Run migrations in the main app without copying
11
+ # From: https://content.pivotal.io/blog/leave-your-migrations-in-your-rails-engines
12
+ initializer "tally.append_migrations" do |app|
13
+ unless app.root.to_s.match root.to_s
14
+ config.paths["db/migrate"].expanded.each do |expanded_path|
15
+ app.config.paths["db/migrate"] << expanded_path
16
+ end
17
+ end
18
+ end
19
+
20
+ # Allow approved helpers to be included in the main app
21
+ initializer "tally.expose_helpers" do |app|
22
+ ActiveSupport.on_load :action_controller do
23
+ ActionController::Base.helper Tally::IncrementHelper
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ module Tally
2
+ class Increment
3
+
4
+ include Keyable
5
+
6
+ def increment(by = 1)
7
+ Tally.redis do |conn|
8
+ conn.multi do
9
+ conn.incrby(redis_key, by)
10
+ conn.expire(redis_key, Tally.config.ttl) if Tally.config.ttl.present?
11
+
12
+ conn.sadd(daily_key, simple_key)
13
+ conn.expire(daily_key, Tally.config.ttl) if Tally.config.ttl.present?
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.increment(key, record = nil, by = 1)
19
+ instance = new(key, record)
20
+ instance.increment(by)
21
+ instance = nil
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,123 @@
1
+ module Tally
2
+ # Locates keys for a given day, key, and/or record
3
+ class KeyFinder
4
+
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Entry
8
+
9
+ attr_reader :key
10
+ attr_reader :record
11
+ attr_reader :type
12
+
13
+ def initialize(key: nil, day: nil, record: nil, type: nil)
14
+ @key = key.to_s.gsub(":", ".").downcase.strip if key.present?
15
+ @day = day
16
+ @record = record
17
+ @type = type unless record.present?
18
+
19
+ @day = @day.to_date if Time === @day
20
+ end
21
+
22
+ def day
23
+ @day ||= Time.current.utc.to_date
24
+ end
25
+
26
+ def entries
27
+ @entries ||= all_keys.map do |key|
28
+ if match = key.match(entry_regex)
29
+ Entry.new(match, key, day)
30
+ end
31
+ end.compact
32
+ end
33
+
34
+ def keys
35
+ entries.map(&:key).compact.uniq
36
+ end
37
+
38
+ def raw_keys
39
+ all_keys
40
+ end
41
+
42
+ # load up all records for the given keys, tries to batch them by model type
43
+ # so there's not an N+1 here
44
+ def records
45
+ models = entries.reduce({}) do |result, entry|
46
+ result[entry.type] ||= []
47
+ result[entry.type].push(entry.id)
48
+ result
49
+ end
50
+
51
+ models.map do |model, ids|
52
+ next unless ids.compact.any?
53
+
54
+ if klass = model.to_s.classify.safe_constantize
55
+ klass.where(id: ids)
56
+ end
57
+ end.compact.flatten
58
+ end
59
+
60
+ def types
61
+ entries.map(&:type).compact.uniq
62
+ end
63
+
64
+ def self.find(*args)
65
+ new(*args).entries
66
+ end
67
+
68
+ private
69
+
70
+ def all_keys
71
+ @keys ||= build_keys_from_redis
72
+ end
73
+
74
+ def build_keys_from_redis
75
+ result = []
76
+ cursor = ""
77
+
78
+ scan = scan_from_redis
79
+
80
+ while cursor != "0"
81
+ result << scan.last
82
+ cursor = scan.first
83
+
84
+ scan = scan_from_redis(cursor: cursor)
85
+ end
86
+
87
+ result.flatten
88
+ end
89
+
90
+ def day_key
91
+ @day_key ||= if day == "*"
92
+ "*"
93
+ else
94
+ day.strftime(Tally.config.date_format)
95
+ end
96
+ end
97
+
98
+ def entry_regex
99
+ @entry_regex ||= Regexp.new("#{ Tally.config.prefix }:?(?<record>[^:]+:[\\d]+)?:?(?<key>[^:]+)?@")
100
+ end
101
+
102
+ def scan_from_redis(cursor: "0")
103
+ Tally.redis { |conn| conn.scan(cursor, match: scan_key) }
104
+ end
105
+
106
+ def scan_key
107
+ @scan_key ||= if key.present? && record.present?
108
+ "#{ Tally.config.prefix }:#{ record.model_name.i18n_key }:#{ record.id }:#{ key }@#{ day_key }"
109
+ elsif key.present? && type.present?
110
+ "#{ Tally.config.prefix }:#{ type }:*:#{ key }@#{ day_key }"
111
+ elsif record.present?
112
+ "#{ Tally.config.prefix }:#{ record.model_name.i18n_key }:#{ record.id }:*@#{ day_key }"
113
+ elsif type.present?
114
+ "#{ Tally.config.prefix }:#{ type }:*:*@#{ day_key }"
115
+ elsif key.present?
116
+ "#{ Tally.config.prefix }:*#{ key }@#{ day_key }"
117
+ else
118
+ "#{ Tally.config.prefix }*@#{ day_key }"
119
+ end
120
+ end
121
+
122
+ end
123
+ end
@@ -0,0 +1,66 @@
1
+ module Tally
2
+ class KeyFinder::Entry
3
+
4
+ attr_reader :raw_key
5
+
6
+ def initialize(match, raw_key, date)
7
+ @match = match
8
+ @raw_key = raw_key
9
+ @date = date if Date === date
10
+ end
11
+
12
+ def date
13
+ @date ||= build_date_from_raw_key
14
+ end
15
+
16
+ def id
17
+ @id ||= if match[:record]
18
+ match[:record].split(":").last.to_i
19
+ end
20
+ end
21
+
22
+ def key
23
+ match[:key]
24
+ end
25
+
26
+ def record
27
+ return nil unless type.present?
28
+ return nil unless id.present? && id > 0
29
+
30
+ if model = type.classify.safe_constantize
31
+ model.find_by(id: id)
32
+ end
33
+ end
34
+
35
+ def type
36
+ @type ||= if match[:record]
37
+ match[:record].split(":").first
38
+ end
39
+ end
40
+
41
+ def value
42
+ Tally.redis do |conn|
43
+ conn.get(key_for_value_lookup).to_i
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :match
50
+
51
+ def build_date_from_raw_key
52
+ if raw_key.to_s =~ /@/
53
+ Date.parse(raw_key.to_s.split("@").last)
54
+ end
55
+ end
56
+
57
+ def key_for_value_lookup
58
+ if raw_key.starts_with?("#{ Tally.config.prefix }:")
59
+ raw_key
60
+ else
61
+ "#{ Tally.config.prefix }:#{ raw_key }@#{ date&.strftime('%Y-%m-%d') }"
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ require "active_support/concern"
2
+
3
+ module Tally
4
+ module Keyable
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_reader :key
10
+ attr_reader :record
11
+ end
12
+
13
+ def initialize(key, record = nil)
14
+ @key = key.to_s.gsub(":", ".").downcase.strip
15
+ @record = record
16
+ end
17
+
18
+ def day
19
+ @day ||= Time.current.utc.to_date
20
+ end
21
+
22
+ private
23
+
24
+ def daily_key
25
+ "#{ prefix }@#{ date_key }"
26
+ end
27
+
28
+ def date_key
29
+ @date_key ||= day.strftime(Tally.config.date_format)
30
+ end
31
+
32
+ def prefix
33
+ Tally.config.prefix
34
+ end
35
+
36
+ def record_key
37
+ if record
38
+ "#{ record.model_name.i18n_key }:#{ record.id }"
39
+ end
40
+ end
41
+
42
+ def redis_key
43
+ @redis_key ||= "#{ prefix }:#{ simple_key }@#{ date_key }"
44
+ end
45
+
46
+ def simple_key
47
+ @simple_key ||= if record.respond_to?(:model_name)
48
+ "#{ record_key }:#{ key }"
49
+ else
50
+ key
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,110 @@
1
+ module Tally
2
+ class RecordSearcher
3
+
4
+ attr_reader :params
5
+
6
+ def initialize(params = {})
7
+ @params = params || {}
8
+
9
+ if ActionController::Parameters === params
10
+ if params.permitted?
11
+ @params = params.to_h
12
+ else
13
+ @params = {}
14
+ end
15
+ end
16
+
17
+ @params = @params.symbolize_keys
18
+ end
19
+
20
+ def days
21
+ @keys ||= build_search_scope.select(:day).distinct.reorder(day: :desc)
22
+ end
23
+
24
+ def keys
25
+ @keys ||= build_search_scope.select(:key).distinct.reorder(:key)
26
+ end
27
+
28
+ def records
29
+ @records ||= build_search_scope
30
+ end
31
+
32
+ def self.search(*args)
33
+ new(*args).records
34
+ end
35
+
36
+ private
37
+
38
+ def build_recordable_from_params
39
+ id = params[:id].to_i
40
+ model = params[:type].to_s.classify.safe_constantize
41
+
42
+ if id > 0 && model.respond_to?(:find_by_id)
43
+ model.find_by_id(id)
44
+ end
45
+ end
46
+
47
+ def build_search_scope
48
+ scope = Record.all
49
+
50
+ if recordable
51
+ scope = scope.where(recordable: recordable)
52
+ elsif params[:type].present?
53
+ scope = scope.where(recordable_type: params[:type])
54
+ end
55
+
56
+ if key
57
+ scope = scope.where(key: key)
58
+ end
59
+
60
+ if start_date && end_date
61
+ scope = scope.where(day: (start_date..end_date))
62
+ elsif start_date
63
+ scope = scope.where(day: start_date..Float::INFINITY)
64
+ elsif end_date
65
+ scope = scope.where(Record.arel_table[:day].lteq(end_date))
66
+ end
67
+
68
+ scope.order(day: :desc)
69
+ end
70
+
71
+ def end_date
72
+ if params[:end_date]
73
+ @end_date ||= to_date(:end_date)
74
+ end
75
+ end
76
+
77
+ def key
78
+ if params[:key].present?
79
+ @key ||= params[:key].to_s.gsub(":", ".").downcase.strip
80
+ end
81
+ end
82
+
83
+ def recordable
84
+ @recordable ||= if ActiveRecord::Base === params[:record]
85
+ params[:record]
86
+ elsif params[:id] && params[:type]
87
+ build_recordable_from_params
88
+ end
89
+ end
90
+
91
+ def start_date
92
+ if params[:start_date]
93
+ @start_date ||= to_date(:start_date)
94
+ end
95
+ end
96
+
97
+ def to_date(param_key)
98
+ value = params[param_key].presence
99
+ return nil unless value.present?
100
+ return value if value.is_a?(Date)
101
+ return value.to_date if value.is_a?(DateTime)
102
+ return value.to_date if value.is_a?(Time)
103
+
104
+ Date.parse(value)
105
+ rescue
106
+ nil
107
+ end
108
+
109
+ end
110
+ end