statlysis 0.0.1
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.
- data/.document +5 -0
- data/.gitignore +51 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +110 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +43 -0
- data/Rakefile +11 -0
- data/lib/statlysis.rb +134 -0
- data/lib/statlysis/clock.rb +36 -0
- data/lib/statlysis/common.rb +27 -0
- data/lib/statlysis/configuration.rb +10 -0
- data/lib/statlysis/cron.rb +86 -0
- data/lib/statlysis/cron/count.rb +93 -0
- data/lib/statlysis/cron/top.rb +154 -0
- data/lib/statlysis/formula.rb +6 -0
- data/lib/statlysis/javascript/count.rb +37 -0
- data/lib/statlysis/map_reduce.rb +32 -0
- data/lib/statlysis/rake.rb +28 -0
- data/lib/statlysis/results.rb +17 -0
- data/lib/statlysis/similar.rb +89 -0
- data/lib/statlysis/timeseries.rb +41 -0
- data/statlysis.gemspec +30 -0
- data/test/helper.rb +17 -0
- data/test/models/company.rb +12 -0
- data/test/models/employee.rb +14 -0
- data/test/test_mapreduce.rb +26 -0
- data/test/test_statlysis.rb +76 -0
- data/test/test_timeseries.rb +6 -0
- metadata +216 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Statlysis
|
4
|
+
class Cron
|
5
|
+
attr_accessor :source, :time_column, :time_unit
|
6
|
+
include Common
|
7
|
+
|
8
|
+
DefaultWrongMessage = "not implement yet, please config it by subclass".freeze
|
9
|
+
def initialize source, opts = {}
|
10
|
+
cron.stat_table_name = opts[:stat_table_name] if opts[:stat_table_name]
|
11
|
+
cron.time_column = opts[:time_column]
|
12
|
+
cron.source = source
|
13
|
+
cron.time_unit = opts[:time_unit]
|
14
|
+
cron
|
15
|
+
end
|
16
|
+
def output; raise DefaultWrongMessage end
|
17
|
+
def setup_stat_table; raise DefaultWrongMessage end
|
18
|
+
def run; raise DefaultWrongMessage end
|
19
|
+
|
20
|
+
# overwrite to lazy load @source
|
21
|
+
def inspect
|
22
|
+
source_inspect = is_mysql? ? cron.source.to_sql : cron.source
|
23
|
+
str = "#<#{cron.class} @source=#{source_inspect} @stat_table_name=#{cron.stat_table_name} @time_column=#{cron.time_column} @stat_table=#{cron.stat_table}"
|
24
|
+
str << " @stat_model=#{cron.stat_model}" if cron.methods.index(:stat_model)
|
25
|
+
str << ">"
|
26
|
+
str
|
27
|
+
end
|
28
|
+
|
29
|
+
def source_where_array
|
30
|
+
# TODO follow index seq
|
31
|
+
a = cron.source.where("").where_values.map do |equality|
|
32
|
+
# use full keyvalue index name
|
33
|
+
equality.is_a?(String) ? equality.to_sym : "#{equality.operand1.name}#{equality.operand2}"
|
34
|
+
end if is_mysql?
|
35
|
+
a = cron.source.all.selector.reject {|k, v| k == 't' } if is_mongodb?
|
36
|
+
a.map {|s| s.to_s.split(//).select {|s| s.match(/[a-z0-9]/i) }.join }.sort.map(&:to_sym)
|
37
|
+
end
|
38
|
+
|
39
|
+
def source_name
|
40
|
+
@source_name ||= begin
|
41
|
+
m = :table_name if is_mysql?
|
42
|
+
m = :collection_name if is_mongodb?
|
43
|
+
cron.source.send(m)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# automode
|
48
|
+
# or
|
49
|
+
# specify TIME_RANGE and TIME_UNIT in shell to run
|
50
|
+
def time_range
|
51
|
+
return TimeSeries.parse(ENV['TIME_RANGE'], :unit => (ENV['TIME_UNIT'] || 'day')) if ENV['TIME_RANGE']
|
52
|
+
# 选择开始时间。取出统计表的最后时间,和数据表的最先时间对比,哪个最后就选择
|
53
|
+
begin_day = DateTime.now.beginning_of_day
|
54
|
+
st_timebegin = (a = cron.stat_table.order(:t).where("t >= ?", begin_day.yesterday).first) ? a[:t] : nil
|
55
|
+
cron.stat_table.where("t >= ?", begin_day.tomorrow).delete # 明天的数据没出来肯定统计不了
|
56
|
+
timebegin = (a = cron.source.first) ? a.send(cron.time_column) : (DateTime.now - 1.second)
|
57
|
+
timebegin = Time.at(timebegin) if is_time_column_integer?
|
58
|
+
timebegin = (st_timebegin > timebegin) ? st_timebegin : timebegin if st_timebegin
|
59
|
+
|
60
|
+
timeend = DateTime.now
|
61
|
+
puts "#{cron.source_name}'s range #{timebegin..timeend}"
|
62
|
+
# 把统计表的最后时间点也包含进去重新计算下
|
63
|
+
TimeSeries.parse(timebegin..timeend, :unit => cron.time_unit)
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
def is_mysql?; @_is_mysql ||= modules.grep(/ActiveRecord::Store/).any? end
|
68
|
+
def is_mongodb?; @_is_mongodb ||= modules.grep(/Mongoid::Document/).any? end
|
69
|
+
def modules; @_modules ||= cron.source.included_modules.map(&:to_s) end
|
70
|
+
|
71
|
+
# 兼容采用整数类型作时间字段
|
72
|
+
def is_time_column_integer?
|
73
|
+
if is_mysql?
|
74
|
+
cron.source.columns_hash[cron.time_column.to_s].type == :integer
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
require 'statlysis/cron/count'
|
86
|
+
require 'statlysis/cron/top'
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Statlysis
|
4
|
+
class Count < Cron
|
5
|
+
def initialize source, opts = {}
|
6
|
+
super
|
7
|
+
Statlysis.check_set_database
|
8
|
+
cron.setup_stat_table
|
9
|
+
Statlysis.setup_stat_table_and_model cron
|
10
|
+
cron
|
11
|
+
end
|
12
|
+
|
13
|
+
# 设置数据源,并保存结果入数据库
|
14
|
+
def run
|
15
|
+
cron.source = cron.source.order("#{cron.time_column} ASC") if is_mysql?
|
16
|
+
cron.source = cron.source.asc(cron.time_column) if is_mongodb?
|
17
|
+
|
18
|
+
(puts("#{cron.source_name} have no result!"); return false) if cron.output.blank?
|
19
|
+
# delete first in range
|
20
|
+
@output = cron.output
|
21
|
+
unless @output.any?
|
22
|
+
puts "没有数据"; return
|
23
|
+
end
|
24
|
+
@num_i = 0; @num_add = 999
|
25
|
+
Statlysis.sequel.transaction do
|
26
|
+
cron.stat_table.where("t >= ? AND t <= ?", cron.output[0][:t], cron.output[-1][:t]).delete
|
27
|
+
while !(_a = @output[@num_i..(@num_i+@num_add)]).blank? do
|
28
|
+
# batch insert all
|
29
|
+
cron.stat_table.multi_insert _a
|
30
|
+
@num_i += (@num_add + 1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def reoutput; @output = nil; output end
|
37
|
+
protected
|
38
|
+
def unit_range_query time, time_begin = nil
|
39
|
+
# time begin and end
|
40
|
+
tb = time # TODO 差八个小时 [.in_time_zone, .localtime, .utc] 对于Rails,计算结果还是一样的。
|
41
|
+
te = (time+1.send(cron.time_unit)-1.second)
|
42
|
+
tb, te = tb.to_i, te.to_i if is_time_column_integer?
|
43
|
+
tb = time_begin || tb
|
44
|
+
return ["#{cron.time_column} >= ? AND #{cron.time_column} < ?", tb, te] if is_mysql?
|
45
|
+
return {cron.time_column => {"$gte" => tb.utc, "$lt" => te.utc}} if is_mongodb? # .utc [fix undefined method `__bson_dump__' for Sun, 16 Dec 2012 16:00:00 +0000:DateTime]
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
class Timely < Count
|
51
|
+
def setup_stat_table
|
52
|
+
# TODO migration proc, merge into setup_stat_table_and_model
|
53
|
+
cron.stat_table_name = [cron.class.name.split("::")[-1], cron.source_name, cron.source_where_array.join, cron.time_unit[0]].map {|s| s.to_s.gsub('_','') }.reject {|s| s.blank? }.join('_').downcase
|
54
|
+
raise "mysql only support table_name in 64 characters, the size of '#{cron.stat_table_name}' is #{cron.stat_table_name.to_s.size}. please set cron.stat_table_name when you create a Cron instance" if cron.stat_table_name.to_s.size > 64
|
55
|
+
unless Statlysis.sequel.table_exists?(cron.stat_table_name)
|
56
|
+
Statlysis.sequel.transaction do
|
57
|
+
Statlysis.sequel.create_table cron.stat_table_name, DefaultTableOpts do
|
58
|
+
DateTime :t # alias for :time
|
59
|
+
end
|
60
|
+
|
61
|
+
# TODO Add cron.source_where_array before count_columns
|
62
|
+
count_columns = [:timely_c, :totally_c] # alias for :count
|
63
|
+
count_columns.each {|w| Statlysis.sequel.add_column cron.stat_table_name, w, Integer }
|
64
|
+
index_column_names = [:t] + count_columns
|
65
|
+
index_column_names_name = index_column_names.join("_")
|
66
|
+
index_column_names_name = index_column_names_name[-63..-1] if index_column_names_name.size > 64
|
67
|
+
|
68
|
+
Statlysis.sequel.add_index cron.stat_table_name, index_column_names, :name => index_column_names_name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def output
|
74
|
+
@output ||= (cron.time_range.map do |time|
|
75
|
+
timely_c = cron.source.where(unit_range_query(time)).count
|
76
|
+
_t = DateTime.parse("19700101")
|
77
|
+
_t = is_time_column_integer? ? _t.to_i : _t
|
78
|
+
totally_c = cron.source.where(unit_range_query(time, _t)).count
|
79
|
+
|
80
|
+
puts "#{time.in_time_zone} #{cron.source_name} timely_c:#{timely_c} totally_c:#{totally_c}"
|
81
|
+
if timely_c.zero? && totally_c.zero?
|
82
|
+
nil
|
83
|
+
else
|
84
|
+
{:t => time, :timely_c => timely_c, :totally_c => totally_c}
|
85
|
+
end
|
86
|
+
end.compact)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Dimensions < Count
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# TODO support ActiveRecord
|
3
|
+
|
4
|
+
module Statlysis
|
5
|
+
class Top < Cron
|
6
|
+
attr_accessor :result_limit, :logs
|
7
|
+
attr_accessor :stat_model
|
8
|
+
attr_accessor :pattern_proc, :user_id_proc, :user_info_proc
|
9
|
+
|
10
|
+
def initialize source, opts = {}
|
11
|
+
cron.result_limit = opts[:result_limit] || 100
|
12
|
+
if not opts[:test]
|
13
|
+
[:pattern_proc, :user_id_proc, :user_info_proc].each do |o|
|
14
|
+
raise "Please assign :#{o} params!" if opts[o].nil? && !cron.send(o)
|
15
|
+
cron.send "#{o}=", opts[o]
|
16
|
+
end
|
17
|
+
default_assign_attr :stat_table_name, opts
|
18
|
+
end
|
19
|
+
super
|
20
|
+
cron
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
cron.write
|
25
|
+
end
|
26
|
+
|
27
|
+
def write; raise DefaultWrongMessage end
|
28
|
+
|
29
|
+
|
30
|
+
def self.ensure_statlysis_table_and_model tn
|
31
|
+
Top.new("FakeLogSource", :test => true, :stat_table_name => tn).pattern_table_and_model tn
|
32
|
+
end
|
33
|
+
def ensure_statlysis_table_and_model tn
|
34
|
+
Top.ensure_statlysis_table_and_model tn
|
35
|
+
end
|
36
|
+
|
37
|
+
def default_assign_attr key_symbol, opts
|
38
|
+
if opts[key_symbol]
|
39
|
+
cron.send("#{key_symbol}=", opts[key_symbol])
|
40
|
+
else
|
41
|
+
raise "Please assign opts[:#{key_symbol}]"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# 博客最近用户访问计算实现流程讨论
|
47
|
+
# 问题分两个,一个是后端,一个是前端。对后端来说,用户每次blog/index|show访问都生成访问记录,后端需要进行排重和去掉未登陆用户。如果在该次访问里进行,特别是某个博客突然火了,必然每次访问都产生IO(磁盘或网络,因为多进程要共享信息),所以必定是异步的。
|
48
|
+
# 前端展示考虑到缓存,一般是页面片段缓存,或者ajax载入。
|
49
|
+
# 后端异步如何计算每个blog的最近访客,log.js记录了最近访问,一个后台常驻进程循环对日志表按时间记录来读取blog访问信息,把最近访客信息刷新到blog。相对单次请求全部处理,这里处理次数更少,资源更节约,当然瓶颈也在日志表的索引更新和读取。
|
50
|
+
class LastestVisits < Top
|
51
|
+
attr_accessor :clock
|
52
|
+
attr_accessor :reject_proc
|
53
|
+
|
54
|
+
# *pattern_proc* is a proc to extract user_id or url_prefix to compute the
|
55
|
+
# top visitors from log
|
56
|
+
# *user_id_proc* is a proc to extract user_id from log
|
57
|
+
# *user_info_proc* is a proc to extract visitor informations(like id, name, ...)
|
58
|
+
# *reject_proc* filter visitors
|
59
|
+
def initialize source, opts = {}
|
60
|
+
# set variables
|
61
|
+
cron.reclock opts[:default_time]
|
62
|
+
cron.reject_proc = opts[:reject_proc] || proc {|pattern, user_id| pattern.to_i == user_id.to_i }
|
63
|
+
super
|
64
|
+
cron.pattern_table_and_model cron.stat_table_name
|
65
|
+
cron
|
66
|
+
end
|
67
|
+
|
68
|
+
def output
|
69
|
+
cron.logs = cron.source.asc(cron.time_column).where(cron.time_column => {"$gte" => cron.clock.current}).limit(1000).to_a
|
70
|
+
return {} if cron.logs.blank?
|
71
|
+
cron.logs.inject({}) do |h, log|
|
72
|
+
pattern = cron.pattern_proc.call(log)
|
73
|
+
if pattern
|
74
|
+
h[pattern] ||= []
|
75
|
+
user_id = cron.user_id_proc.call(log).to_i
|
76
|
+
h[pattern] << user_id if not user_id.zero?
|
77
|
+
end
|
78
|
+
h
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def write
|
83
|
+
puts "#{Time.now.strftime('%H:%M:%S')} #{cron.stat_model} #{cron.output.inspect}"
|
84
|
+
cron.output.each do |pattern, user_ids|
|
85
|
+
s = cron.stat_model.find_or_create(:pattern => pattern)
|
86
|
+
old_array = (JSON.parse(s.result) rescue []).map {|i| Array(i)[0] }
|
87
|
+
new_user_ids = (old_array + user_ids).reverse.uniq.reverse # ensure the right items will overwrite the left [1,4,5,7,4,3,3,2,1,5].uniq => [1, 4, 5, 7, 3, 2]
|
88
|
+
s.update :result => new_user_ids.reject {|user_id| cron.reject_proc.call(pattern, user_id) rescue false }.map {|user_id| cron.user_info_proc.call(user_id) }.compact[0..cron.result_limit].to_json
|
89
|
+
end
|
90
|
+
cron.clock.update cron.logs.last.try(cron.time_column)
|
91
|
+
end
|
92
|
+
|
93
|
+
def reclock default_time = nil
|
94
|
+
cron.clock = Clock.new cron.stat_table_name, (default_time || cron.clock.current)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class SingleKv < Top
|
99
|
+
attr_accessor :time_ago, :stat_column_name
|
100
|
+
|
101
|
+
def initialize source, opts = {}
|
102
|
+
[:time_ago, :stat_column_name].each {|key_symbol| default_assign_attr key_symbol, opts }
|
103
|
+
raise "#{cron.class} only is kv store" if cron.stat_table_name # TODO
|
104
|
+
super
|
105
|
+
cron.ensure_statlysis_table_and_model [Statlysis.tablename_default_pre, 'single_kvs'].compact.join("_").freeze
|
106
|
+
cron
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
# 一般最近热门列表通常采用简单对一个字段记录访问数的算法,但是这可能会导致刷量等问题。
|
112
|
+
#
|
113
|
+
# 解决方法为从用户行为中去综合分析,具体流程为:
|
114
|
+
# 从URI中抽取item_id, 从访问日志抽取排重IP和user_id,从like,fav,comment表获取更深的用户行为,把前两者通过一定比例相加得到排行。
|
115
|
+
# 最后用时间降温来避免马太效应,必可动态提升比例以使最近稍微热门的替换掉之前太热门的。
|
116
|
+
#
|
117
|
+
# 线性计算速度很快
|
118
|
+
#
|
119
|
+
class HotestItems < SingleKv
|
120
|
+
attr_accessor :key, :id_to_score_and_time_hash_proc
|
121
|
+
attr_accessor :limit
|
122
|
+
|
123
|
+
def initialize key, id_to_score_and_time_hash_proc
|
124
|
+
cron.key = key
|
125
|
+
cron.id_to_score_and_time_hash_proc = id_to_score_and_time_hash_proc
|
126
|
+
cron.limit = 20
|
127
|
+
super
|
128
|
+
cron
|
129
|
+
end
|
130
|
+
|
131
|
+
def output
|
132
|
+
t = cron.id_to_score_and_time_hash_proc
|
133
|
+
while t.is_a?(Proc) do
|
134
|
+
t = t.call
|
135
|
+
end
|
136
|
+
@id_to_score_and_time_hash = t
|
137
|
+
@id_to_day_hash = @id_to_score_and_time_hash.inject({}) {|h, ab| h[ab[0]] = (((Time.now - ab[1][1]) / (3600*24)).round + 1); h }
|
138
|
+
|
139
|
+
@id_to_timecooldown_hash = @id_to_score_and_time_hash.inject({}) {|h, kv| h[kv[0]] = (kv[1][0] / Math.sqrt(@id_to_day_hash[kv[0]])); h }
|
140
|
+
array = @id_to_timecooldown_hash.sort {|a, b| b[1] <=> a[1] }.map(&:first)
|
141
|
+
{cron.key => array}
|
142
|
+
end
|
143
|
+
|
144
|
+
def write
|
145
|
+
cron.output.each do |key, array|
|
146
|
+
json = array[0..140].to_json
|
147
|
+
StSingleKv.find_or_create(:pattern => key).update :result => json
|
148
|
+
StSingleKvHistory.find_or_create(:pattern => "#{key}_#{Time.now.strftime('%Y%m%d')}").update :result => json
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Statlysis
|
4
|
+
module Javascript
|
5
|
+
class MultiDimensionalCount
|
6
|
+
attr_accessor :map_func, :reduce_func
|
7
|
+
|
8
|
+
def initialize *fields
|
9
|
+
fields = :_id if fields.blank?
|
10
|
+
emit_key = case fields
|
11
|
+
when Array
|
12
|
+
emit_key = fields.map {|dc| "#{dc}: this.#{dc}" }.join(", ")
|
13
|
+
emit_key = "{#{emit_key}}"
|
14
|
+
when Symbol, String
|
15
|
+
"this.#{fields}"
|
16
|
+
else
|
17
|
+
raise "Please assign symbol, string, or array of them"
|
18
|
+
end
|
19
|
+
|
20
|
+
self.map_func = "function() {
|
21
|
+
emit (#{emit_key}, {count: 1});
|
22
|
+
}"
|
23
|
+
|
24
|
+
self.reduce_func = "function(key, values) {
|
25
|
+
var count = 0;
|
26
|
+
|
27
|
+
values.forEach(function(v) {
|
28
|
+
count += v['count'];
|
29
|
+
});
|
30
|
+
|
31
|
+
return {count: count};
|
32
|
+
}"
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'javascript/count'
|
4
|
+
|
5
|
+
module Statlysis
|
6
|
+
class MapReduce
|
7
|
+
attr_reader :mongoid_scope, :mapreduce_javascript
|
8
|
+
attr_accessor :mr_collection, :results
|
9
|
+
attr_accessor :is_use_inline, :identify
|
10
|
+
def initialize mongoid_scope, mapreduce_javascript
|
11
|
+
mr.mongoid_scope = mongoid_scope
|
12
|
+
mr.mapreduce_javascript = mapreduce_javascript
|
13
|
+
mr.is_use_inline = true
|
14
|
+
mr.identify = Time.now.strftime("%m%d_%H%M%S")
|
15
|
+
mr
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
# TODO collection for large
|
20
|
+
mr.results = Results.new mr.mongoid_scope.map_reduce(mapreduce_javascript.map_func, mapreduce_javascript.reduce_func).out(:replace => out_collection_name)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def output
|
25
|
+
mr.results.output
|
26
|
+
end
|
27
|
+
|
28
|
+
def out_collection_name; "mr_#{mr.mongoid_scope.collection_name}_#{mr.identify}" end
|
29
|
+
def mr; self end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
namespace :statlysis do
|
6
|
+
Statlysis::Units.each do |unit|
|
7
|
+
desc "statistical in #{unit}"
|
8
|
+
only_one_task "#{unit}_count" => :environment do
|
9
|
+
Statlysis.send("#{unit}_crons").map(&:run)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "realtime process"
|
14
|
+
only_one_task :realtime_process => :environment do
|
15
|
+
loop { Statlysis.realtime_crons.map(&:run); sleep 1 }
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "similar process"
|
19
|
+
only_one_task :similar_process => :environment do
|
20
|
+
Statlysis.similar_crons.map(&:run)
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "hotest process"
|
24
|
+
only_one_task :hotest_process => :environment do
|
25
|
+
Statlysis.hotest_crons.map(&:run)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|