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.
- checksums.yaml +7 -0
- data/README.md +217 -0
- data/Rakefile +32 -0
- data/app/assets/config/tally_manifest.js +2 -0
- data/app/assets/javascripts/tally/application.js +15 -0
- data/app/assets/stylesheets/tally/application.css +15 -0
- data/app/controllers/tally/application_controller.rb +17 -0
- data/app/controllers/tally/days_controller.rb +19 -0
- data/app/controllers/tally/keys_controller.rb +19 -0
- data/app/controllers/tally/records_controller.rb +21 -0
- data/app/helpers/tally/application_helper.rb +4 -0
- data/app/helpers/tally/increment_helper.rb +15 -0
- data/app/jobs/tally/application_job.rb +4 -0
- data/app/jobs/tally/calculator_runner_job.rb +12 -0
- data/app/models/tally/application_record.rb +7 -0
- data/app/models/tally/record.rb +30 -0
- data/app/presenters/tally/record_presenter.rb +29 -0
- data/bin/annotate +29 -0
- data/bin/appraisal +29 -0
- data/bin/bundle +114 -0
- data/bin/coderay +29 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/nokogiri +29 -0
- data/bin/pry +29 -0
- data/bin/rackup +29 -0
- data/bin/rails +14 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/sprockets +29 -0
- data/bin/thor +29 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20181016172358_create_tally_records.rb +15 -0
- data/lib/tally.rb +77 -0
- data/lib/tally/archiver.rb +64 -0
- data/lib/tally/calculator.rb +30 -0
- data/lib/tally/calculator_runner.rb +56 -0
- data/lib/tally/calculators.rb +29 -0
- data/lib/tally/countable.rb +23 -0
- data/lib/tally/daily.rb +64 -0
- data/lib/tally/engine.rb +28 -0
- data/lib/tally/increment.rb +25 -0
- data/lib/tally/key_finder.rb +123 -0
- data/lib/tally/key_finder/entry.rb +66 -0
- data/lib/tally/keyable.rb +55 -0
- data/lib/tally/record_searcher.rb +110 -0
- data/lib/tally/sweeper.rb +46 -0
- data/lib/tally/version.rb +5 -0
- data/lib/tasks/tally_tasks.rake +16 -0
- metadata +256 -0
data/lib/tally/daily.rb
ADDED
@@ -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
|
data/lib/tally/engine.rb
ADDED
@@ -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
|