tally 1.0.0.beta1

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 (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