queris 0.8.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.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +34 -0
- data/README.md +53 -0
- data/Rakefile +1 -0
- data/data/redis_scripts/add_low_ttl.lua +10 -0
- data/data/redis_scripts/copy_key_if_absent.lua +13 -0
- data/data/redis_scripts/copy_ttl.lua +13 -0
- data/data/redis_scripts/create_page_if_absent.lua +24 -0
- data/data/redis_scripts/debuq.lua +20 -0
- data/data/redis_scripts/delete_if_string.lua +8 -0
- data/data/redis_scripts/delete_matching_keys.lua +7 -0
- data/data/redis_scripts/expire_temp_query_keys.lua +7 -0
- data/data/redis_scripts/make_rangehack_if_needed.lua +30 -0
- data/data/redis_scripts/master_expire.lua +15 -0
- data/data/redis_scripts/match_key_type.lua +9 -0
- data/data/redis_scripts/move_key.lua +11 -0
- data/data/redis_scripts/multisize.lua +19 -0
- data/data/redis_scripts/paged_query_ready.lua +35 -0
- data/data/redis_scripts/periodic_zremrangebyscore.lua +9 -0
- data/data/redis_scripts/persist_reusable_temp_query_keys.lua +14 -0
- data/data/redis_scripts/query_ensure_existence.lua +23 -0
- data/data/redis_scripts/query_intersect_optimization.lua +31 -0
- data/data/redis_scripts/remove_from_keyspace.lua +27 -0
- data/data/redis_scripts/remove_from_sets.lua +13 -0
- data/data/redis_scripts/results_from_hash.lua +54 -0
- data/data/redis_scripts/results_with_ttl.lua +20 -0
- data/data/redis_scripts/subquery_intersect_optimization.lua +25 -0
- data/data/redis_scripts/subquery_intersect_optimization_cleanup.lua +5 -0
- data/data/redis_scripts/undo_add_low_ttl.lua +8 -0
- data/data/redis_scripts/unpaged_query_ready.lua +17 -0
- data/data/redis_scripts/unpersist_reusable_temp_query_keys.lua +11 -0
- data/data/redis_scripts/update_live_expiring_presence_index.lua +20 -0
- data/data/redis_scripts/update_query.lua +126 -0
- data/data/redis_scripts/update_rangehacks.lua +94 -0
- data/data/redis_scripts/zrangestore.lua +12 -0
- data/lib/queris.rb +400 -0
- data/lib/queris/errors.rb +8 -0
- data/lib/queris/indices.rb +735 -0
- data/lib/queris/mixin/active_record.rb +74 -0
- data/lib/queris/mixin/object.rb +398 -0
- data/lib/queris/mixin/ohm.rb +81 -0
- data/lib/queris/mixin/queris_model.rb +59 -0
- data/lib/queris/model.rb +455 -0
- data/lib/queris/profiler.rb +275 -0
- data/lib/queris/query.rb +1215 -0
- data/lib/queris/query/operations.rb +398 -0
- data/lib/queris/query/page.rb +101 -0
- data/lib/queris/query/timer.rb +42 -0
- data/lib/queris/query/trace.rb +108 -0
- data/lib/queris/query_store.rb +137 -0
- data/lib/queris/version.rb +3 -0
- data/lib/rails/log_subscriber.rb +22 -0
- data/lib/rails/request_timing.rb +29 -0
- data/lib/tasks/queris.rake +138 -0
- data/queris.gemspec +41 -0
- data/test.rb +39 -0
- data/test/current.rb +74 -0
- data/test/dsl.rb +35 -0
- data/test/ohm.rb +37 -0
- metadata +161 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Queris
|
2
|
+
class Query
|
3
|
+
class Timer
|
4
|
+
def initialize
|
5
|
+
@time_start={}
|
6
|
+
@time={}
|
7
|
+
@times_recorded={}
|
8
|
+
end
|
9
|
+
def start(attr)
|
10
|
+
attr = attr.to_sym
|
11
|
+
@time_start[attr]=Time.now.to_f
|
12
|
+
@time[attr] ||= '?'
|
13
|
+
end
|
14
|
+
def finish(attr)
|
15
|
+
attr = attr.to_sym
|
16
|
+
start_time = @time_start[attr]
|
17
|
+
raise "Query Profiling timing attribute #{attr} was never started." if start_time.nil?
|
18
|
+
t = Time.now.to_f - start_time
|
19
|
+
@time_start[attr]=nil
|
20
|
+
record attr, (Time.now.to_f - start_time)
|
21
|
+
end
|
22
|
+
def record(attr, val)
|
23
|
+
attr = attr.to_sym
|
24
|
+
@times_recorded[attr]||=0
|
25
|
+
@times_recorded[attr]+=1
|
26
|
+
@time[attr]=0 if @time[attr].nil? || @time[attr]=='?'
|
27
|
+
@time[attr]+=val
|
28
|
+
end
|
29
|
+
def to_s
|
30
|
+
mapped = @time.map do |k,v|
|
31
|
+
v = v.round(4) if Numeric === v
|
32
|
+
if (times = @times_recorded[k]) != 1
|
33
|
+
"#{k}(#{times} times):#{v}"
|
34
|
+
else
|
35
|
+
"#{k}:#{v}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
mapped.join ", "
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Queris
|
3
|
+
class Query
|
4
|
+
private
|
5
|
+
class Trace
|
6
|
+
class TraceBase
|
7
|
+
def indent(n=nil)
|
8
|
+
@indent=n if n
|
9
|
+
" " * indentation
|
10
|
+
end
|
11
|
+
def indentation
|
12
|
+
@indent || 0
|
13
|
+
end
|
14
|
+
def set_options(opt={})
|
15
|
+
@opt=opt
|
16
|
+
end
|
17
|
+
def fval(val) #future value
|
18
|
+
begin
|
19
|
+
Redis::Future === val ? val.value : val
|
20
|
+
rescue Redis::FutureNotReady => e
|
21
|
+
"unavailable"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
class TraceMessage < TraceBase
|
26
|
+
def initialize(txt)
|
27
|
+
@text=txt
|
28
|
+
end
|
29
|
+
def to_s
|
30
|
+
"#{indent}#{fval @text}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class TraceOp < TraceBase #query operation tracer
|
34
|
+
def initialize(redis, operation, operand, results_key)
|
35
|
+
@redis=redis
|
36
|
+
@op=operation
|
37
|
+
@operand=operand
|
38
|
+
@results_key=results_key
|
39
|
+
if Array === @operand.key
|
40
|
+
raise NotImplemented, "Only single-key operands can be traced."
|
41
|
+
end
|
42
|
+
prepare
|
43
|
+
end
|
44
|
+
|
45
|
+
def prepare
|
46
|
+
@index_key = @op.operand_key @operand
|
47
|
+
@futures= {
|
48
|
+
:results_size => Queris.run_script(:multisize, @redis, [@results_key]),
|
49
|
+
:results_type => @redis.type(@results_key),
|
50
|
+
:operand_size => Queris.run_script(:multisize, @redis, [@index_key]),
|
51
|
+
:operand_type => @redis.type(@index_key),
|
52
|
+
}
|
53
|
+
end
|
54
|
+
def fval(name)
|
55
|
+
val = @futures[name.to_sym]
|
56
|
+
super val
|
57
|
+
end
|
58
|
+
def to_s
|
59
|
+
op_info = fval(:operand_type) == 'none' ? "key absent" : "|#{fval :operand_type} key|=#{fval :operand_size}"
|
60
|
+
op_key = "#{"[#{@index_key}] " if @opt[:keys]}"
|
61
|
+
if @operand.is_query?
|
62
|
+
"#{indent}#{@op.symbol} subquery<#{@operand.index.id}> #{op_key}(#{op_info}) => #{fval :results_size}\r\n" +
|
63
|
+
"#{@operand.index.trace(@opt.merge(:output => false, :indent => indentation + 1))}"
|
64
|
+
else
|
65
|
+
"#{indent}#{@op.symbol} #{@operand.index.name}#{@operand.value.nil? ? '' : "<#{@operand.value}>"} #{op_key}(#{op_info}) => #{fval :results_size}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_accessor :buffer
|
74
|
+
def initialize(query, opt={})
|
75
|
+
@query=query
|
76
|
+
@buffer = []
|
77
|
+
@indentation = 0
|
78
|
+
@options= Hash === opt ? opt : {}
|
79
|
+
end
|
80
|
+
def to_s
|
81
|
+
out=[]
|
82
|
+
buffer.each do |line|
|
83
|
+
out << line.to_s
|
84
|
+
out << line.subquery.trace(@indentation + 1) if line.respond_to?(:subquery) && line.subquery
|
85
|
+
end
|
86
|
+
out.join "\r\n"
|
87
|
+
end
|
88
|
+
def indent(n=1)
|
89
|
+
@indentation += n
|
90
|
+
@buffer.each { |line| line.indent n }
|
91
|
+
self
|
92
|
+
end
|
93
|
+
def op(*arg)
|
94
|
+
t = TraceOp.new(@query.redis, *arg)
|
95
|
+
t.set_options @options
|
96
|
+
buffer << t
|
97
|
+
self
|
98
|
+
end
|
99
|
+
def message(text)
|
100
|
+
t = TraceMessage.new(text)
|
101
|
+
t.set_options @options
|
102
|
+
buffer << t
|
103
|
+
self
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Queris
|
2
|
+
class QueryStore < Queris::Model
|
3
|
+
index_attribute name: :index, attribute: :all_live_indices, key: :marshaled, value: (proc do |index|
|
4
|
+
case index
|
5
|
+
when Enumerable
|
6
|
+
index.map{|i| QueryStore.index_to_val i}
|
7
|
+
when Index
|
8
|
+
QueryStore.index_to_val index
|
9
|
+
else
|
10
|
+
index
|
11
|
+
end
|
12
|
+
end)
|
13
|
+
index_only
|
14
|
+
live_queries
|
15
|
+
|
16
|
+
class << self
|
17
|
+
@metaquery_ttl = 600
|
18
|
+
attr_accessor :metaquery_ttl
|
19
|
+
def redis(another_model = nil)
|
20
|
+
another_model = another_model.model if Query === another_model
|
21
|
+
if another_model == self
|
22
|
+
r = Queris.redis "metaquery:metaquery"
|
23
|
+
else
|
24
|
+
r= Queris.redis :'metaquery:slave', :metaquery
|
25
|
+
end
|
26
|
+
raise Error, "No appropriate redis connection found for QueryStore. Add a queris connection with the metaquery role (Queris.add_redis(r, :metaquery), or add live_queries to desired models." unless r
|
27
|
+
r
|
28
|
+
end
|
29
|
+
def redis_master
|
30
|
+
Queris.redis :metaquery, :master
|
31
|
+
end
|
32
|
+
|
33
|
+
def index_to_val(index)
|
34
|
+
Index === index ? "#{index.model.name}:#{index.class.name.split('::').last}:#{index.name}" : index
|
35
|
+
end
|
36
|
+
|
37
|
+
def add(query)
|
38
|
+
redis.pipelined do
|
39
|
+
redis_indices.each {|i| i.add query}
|
40
|
+
update query
|
41
|
+
end
|
42
|
+
#puts "added #{query} to QueryStore"
|
43
|
+
end
|
44
|
+
def remove(query)
|
45
|
+
redis.pipelined do
|
46
|
+
redis_indices.each { |i| i.remove query }
|
47
|
+
end
|
48
|
+
#puts "removed #{query} from QueryStore"
|
49
|
+
end
|
50
|
+
def update(query)
|
51
|
+
redis.pipelined do
|
52
|
+
redis.setex "Queris:Metaquery:expire:#{query.marshaled}", query.ttl, ""
|
53
|
+
end
|
54
|
+
#puts "updated #{query} for QueryStore"
|
55
|
+
end
|
56
|
+
|
57
|
+
#NOT EFFICIENT!
|
58
|
+
def all_metaqueries
|
59
|
+
q=query(self, :ttl => 20).static!
|
60
|
+
redis_indices(live: true).each { |i| q.union(i) }
|
61
|
+
q.results
|
62
|
+
end
|
63
|
+
|
64
|
+
def query(model=nil, arg={})
|
65
|
+
model ||= self
|
66
|
+
Metaquery.new(self, arg.merge(:target_model => model, :realtime => true))
|
67
|
+
end
|
68
|
+
def metaquery(arg={})
|
69
|
+
query self, arg
|
70
|
+
end
|
71
|
+
|
72
|
+
def load(marshaled)
|
73
|
+
Marshal.load(marshaled)
|
74
|
+
end
|
75
|
+
alias :find :load
|
76
|
+
|
77
|
+
#wipe all metaquery info
|
78
|
+
def clear!
|
79
|
+
querykeys = redis_master.keys query(self).results_key(nil, "*")
|
80
|
+
expirekeys = redis_master.keys "Queris:Metaquery:expire:*"
|
81
|
+
indexkeys = redis_master.keys redis_index(:index).key("*", nil, true)
|
82
|
+
total = querykeys.count + expirekeys.count + indexkeys.count
|
83
|
+
print "Clearing everything (#{total} keys) from QueryStore..."
|
84
|
+
[querykeys, expirekeys, indexkeys].each do |keys|
|
85
|
+
redis_master.multi do |r|
|
86
|
+
keys.each { |k| r.del k }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
puts "ok"
|
90
|
+
total
|
91
|
+
end
|
92
|
+
alias :clear_queries! :clear!
|
93
|
+
end
|
94
|
+
class Metaquery < QuerisModelQuery
|
95
|
+
def initialize(model, arg={})
|
96
|
+
@target_model = arg[:target_model]
|
97
|
+
arg[:profiler] = Queris::DummyProfiler.new
|
98
|
+
super model, arg
|
99
|
+
end
|
100
|
+
def redis_master
|
101
|
+
Queris.redis(model == QueryStore ? :'metaquery:metaquery' : :metaquery)
|
102
|
+
end
|
103
|
+
def redis
|
104
|
+
if model == QueryStore
|
105
|
+
Queris.redis :'metaquery:metaquery'
|
106
|
+
else
|
107
|
+
@redis || Queris::redis(:'metaquery:slave') || redis_master
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def results_with_gc
|
112
|
+
res = results(:replace_command => true) do |cmd, key, first, last, rangeopt|
|
113
|
+
redis.evalsha(Queris.script_hash(:results_with_ttl), [key], ["Queris:Metaquery:expire:%s"])
|
114
|
+
end
|
115
|
+
res = [[],[]] if res.empty?
|
116
|
+
res.first.map! do |marshaled|
|
117
|
+
QueryStore.load marshaled
|
118
|
+
end
|
119
|
+
#garbage-collect the expired stuff
|
120
|
+
res.last.each do |marshaled|
|
121
|
+
QueryStore.remove QueryStore.load(marshaled)
|
122
|
+
end
|
123
|
+
res.first
|
124
|
+
end
|
125
|
+
def exists?
|
126
|
+
super(redis)
|
127
|
+
end
|
128
|
+
def set_param_from_index(*arg); self; end
|
129
|
+
%w( union diff intersect ).each do |op|
|
130
|
+
define_method op do |index|
|
131
|
+
index = @target_model.redis_index(index)
|
132
|
+
super model.redis_index(:index), QueryStore.index_to_val(index)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Queris
|
2
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
3
|
+
def self.runtime=(value)
|
4
|
+
Thread.current["queris_redis_runtime"] = value
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.runtime
|
8
|
+
Thread.current["queris_redis_runtime"] ||= 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.reset_runtime
|
12
|
+
rt, self.runtime = runtime, 0
|
13
|
+
rt
|
14
|
+
end
|
15
|
+
|
16
|
+
def command(event)
|
17
|
+
self.class.runtime += event.duration
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Queris::LogSubscriber.attach_to :queris
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Queris
|
2
|
+
module ControllerRuntime
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def append_info_to_payload(payload)
|
8
|
+
super
|
9
|
+
payload[:redis_queris_runtime] = Queris::LogSubscriber.reset_runtime
|
10
|
+
if Queris.log_stats_per_request?
|
11
|
+
payload[:queris_stats] = Queris::RedisStats.summary
|
12
|
+
Queris::RedisStats.reset
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def log_process_action(payload)
|
18
|
+
messages, queris_runtime = super, payload[:redis_queris_runtime]
|
19
|
+
logger.info "Queris stats by server\r\n" + payload[:queris_stats] if payload[:queris_stats]
|
20
|
+
messages << ("Redis via Queris: %.1fms" % queris_runtime.to_f) if queris_runtime
|
21
|
+
messages
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveSupport.on_load(:action_controller) do
|
28
|
+
include Queris::ControllerRuntime
|
29
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
#!ruby
|
2
|
+
namespace :queris do
|
3
|
+
def confirm
|
4
|
+
if $stdin.respond_to? 'getch'
|
5
|
+
$stdin.getch.upcase == "Y"
|
6
|
+
else #ruby <= 1.9.2 doesn't have getch
|
7
|
+
$stdin.readline[0].upcase == "Y"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
def warn(action=nil, warning=nil, times=1)
|
11
|
+
puts warning if warning
|
12
|
+
if action then
|
13
|
+
q = "Do you #{times>1 ? 'really ' : ''}want to #{action}?"
|
14
|
+
else
|
15
|
+
if times == 1
|
16
|
+
q = "Did you back up everything you needed to back up?"
|
17
|
+
else
|
18
|
+
q = "Are you sure you want to proceed?"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
puts "#{q} [y/n]"
|
22
|
+
if confirm
|
23
|
+
if times <= 1
|
24
|
+
return true
|
25
|
+
else
|
26
|
+
return warn(nil, nil, times-1)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_index(index, check_existence=true, incremental=false)
|
32
|
+
if check_existence
|
33
|
+
model = index.model
|
34
|
+
print "Checking if index #{index.name} already exists..."
|
35
|
+
foundkeys = index.respond_to?('keypattern') ? model.redis.keys(index.keypattern) : []
|
36
|
+
if foundkeys.count > 0
|
37
|
+
puts "it does."
|
38
|
+
if incremental
|
39
|
+
puts "#{model.name} #{index.name} index data will be deleted incrementally, per element. This runs safely on live data."
|
40
|
+
else
|
41
|
+
puts "All #{model.name} #{index.name} index data will be deleted. Make sure you have a backup!"
|
42
|
+
end
|
43
|
+
return false unless warn
|
44
|
+
if incremental
|
45
|
+
puts "This index spans #{foundkeys.count} redis keys. Every element must be deleted from each of those keys. This may take a while and will slow down redis."
|
46
|
+
if foundkeys.count > 500
|
47
|
+
return false unless warn "continue"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
puts "it doesn't."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
puts "Building index #{index.name} for #{model.name}"
|
55
|
+
model.build_redis_index index.name, incremental
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_models
|
59
|
+
# Load all the application's models. Courtesy of random patch for Sunspot ()
|
60
|
+
Rails.application.eager_load! if defined? Rails
|
61
|
+
end
|
62
|
+
desc "Rebuild all queris indices, optionally deleting nearly everything beforehand"
|
63
|
+
task :rebuild, [:clear] => :environment do |t, args|
|
64
|
+
args.with_defaults(:clear => false)
|
65
|
+
abort unless warn "rebuild all redis indices", "All current redis indices and queries will be destroyed!", 3
|
66
|
+
load_models
|
67
|
+
Queris.rebuild!(args.clear)
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "Build all missing indices or a given redis index in the given model"
|
71
|
+
task :'build', [:model, :index, :incremental] => :environment do |t, args|
|
72
|
+
load_models
|
73
|
+
#abort "Please specify a model." if args.model.nil?
|
74
|
+
#abort "Please specify an index." if args.index.nil?
|
75
|
+
if args.model && args.model.length > 0 then
|
76
|
+
begin
|
77
|
+
model = Object.const_get args.model
|
78
|
+
models = [ model ]
|
79
|
+
rescue NameError
|
80
|
+
abort "No model #{args.model} found."
|
81
|
+
end
|
82
|
+
else
|
83
|
+
models = Queris.models
|
84
|
+
end
|
85
|
+
|
86
|
+
if args.index
|
87
|
+
begin
|
88
|
+
index = model.redis_index args.index
|
89
|
+
rescue
|
90
|
+
abort "No index named #{args.index} found in #{model.name}."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if model and index then
|
95
|
+
#just one index to build
|
96
|
+
build_index index, true, !!args.incremental
|
97
|
+
else
|
98
|
+
models.each do |model|
|
99
|
+
missing = []
|
100
|
+
model.redis_indices.each do |i|
|
101
|
+
missing << i unless i.skip_create? || i.exists?
|
102
|
+
end
|
103
|
+
if missing.count > 0
|
104
|
+
puts "#{model.name} is missing #{missing.count} #{missing.count == 1 ? 'index' : 'indices'}: #{missing.map(&:name).join(', ')}."
|
105
|
+
model.build_redis_indices missing
|
106
|
+
else
|
107
|
+
puts "#{model.name} indices already built."
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
desc "Clear all object caches"
|
114
|
+
task :clear_cache => :environment do
|
115
|
+
load_models
|
116
|
+
puts "Deleted #{Queris.clear_cache!} cache keys."
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "Clear all queries"
|
120
|
+
task :clear_queries => :environment do
|
121
|
+
load_models
|
122
|
+
abort unless warn "clear all queries", "All queries, live and otherwise, and all metaqueries will be deleted"
|
123
|
+
puts "Deleted #{Queris.clear_queries!} query keys."
|
124
|
+
end
|
125
|
+
|
126
|
+
desc "Clear all caches and queries"
|
127
|
+
task :clear => :environment do
|
128
|
+
load_models
|
129
|
+
abort unless warn "clear all queries and caches", "All caches and queries will be deleted", 2
|
130
|
+
puts "Deleted #{Queris.clear!} keys."
|
131
|
+
end
|
132
|
+
|
133
|
+
desc "Queris data sumaries"
|
134
|
+
task :info => :environment do
|
135
|
+
load_models
|
136
|
+
Queris.info
|
137
|
+
end
|
138
|
+
end
|