cellect-server 2.1.1 → 3.0.0
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 +4 -4
- data/.travis.yml +7 -0
- data/cellect.gemspec +2 -1
- data/lib/cellect/server.rb +2 -0
- data/lib/cellect/server/api.rb +6 -6
- data/lib/cellect/server/grouped_loader.rb +13 -0
- data/lib/cellect/server/grouped_workflow.rb +27 -22
- data/lib/cellect/server/loader.rb +37 -0
- data/lib/cellect/server/user.rb +3 -1
- data/lib/cellect/server/workflow.rb +43 -13
- data/lib/cellect/version.rb +1 -1
- data/spec/cellect/server/api/reload_spec.rb +24 -0
- data/spec/cellect/server/grouped_loader_spec.rb +47 -0
- data/spec/cellect/server/grouped_workflow_spec.rb +113 -44
- data/spec/cellect/server/loader_spec.rb +31 -0
- data/spec/cellect/server/user_spec.rb +19 -0
- data/spec/cellect/server/workflow_spec.rb +7 -5
- data/spec/spec_helper.rb +2 -2
- data/spec/support/shared_examples_for_loader.rb +42 -0
- data/spec/support/shared_examples_for_workflow.rb +84 -8
- data/spec/support/spec_adapter.rb +6 -1
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2e026d8345a63a009cc6fe11553fc8bfc87fbd0
|
4
|
+
data.tar.gz: 101d9b968ddb2445505ffbd4aff4b3503c608721
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 650931de9ff9aed416f1388fbb8b31bb67b87b6c5d3890d8116548eda0aba018b5de847a075385df682c2eb6235a29091d5e8f38b1dc6f8a25c9df7b95024a67
|
7
|
+
data.tar.gz: 110a3703be03e56a8300c1c79aea3e39b753c97e97249a12dad8fbcba3fa79a60fa7793ca7ea91afa75dab8eccb63e7c20417cdf696114dc32e5dc915dcb9c07
|
data/.travis.yml
CHANGED
data/cellect.gemspec
CHANGED
@@ -29,7 +29,8 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_development_dependency 'puma', '~> 2.8'
|
30
30
|
spec.add_development_dependency 'pg', '~> 0.17'
|
31
31
|
spec.add_development_dependency 'connection_pool', '~> 2.0'
|
32
|
-
spec.add_development_dependency
|
32
|
+
spec.add_development_dependency "simplecov"
|
33
|
+
spec.add_development_dependency "codeclimate-test-reporter", "~> 1.0.0"
|
33
34
|
|
34
35
|
spec.add_runtime_dependency 'cellect-server', Cellect::VERSION
|
35
36
|
spec.add_runtime_dependency 'cellect-client', Cellect::VERSION
|
data/lib/cellect/server.rb
CHANGED
data/lib/cellect/server/api.rb
CHANGED
@@ -10,7 +10,7 @@ module Cellect
|
|
10
10
|
require 'cellect/server/api/users'
|
11
11
|
|
12
12
|
# GET /stats
|
13
|
-
#
|
13
|
+
#
|
14
14
|
# Provides system load information
|
15
15
|
get :stats do
|
16
16
|
instance = Attention.instance
|
@@ -32,7 +32,7 @@ module Cellect
|
|
32
32
|
resources :workflows do
|
33
33
|
|
34
34
|
# GET /workflows
|
35
|
-
#
|
35
|
+
#
|
36
36
|
# Returns a list of available workflows
|
37
37
|
get do
|
38
38
|
Cellect::Server.adapter.workflow_list
|
@@ -44,7 +44,7 @@ module Cellect
|
|
44
44
|
mount Users
|
45
45
|
|
46
46
|
# GET /workflows/:workflow_id/status
|
47
|
-
#
|
47
|
+
#
|
48
48
|
# Returns the workflow's status
|
49
49
|
get :status do
|
50
50
|
return four_oh_four unless workflow
|
@@ -52,15 +52,15 @@ module Cellect
|
|
52
52
|
end
|
53
53
|
|
54
54
|
# POST /workflows/:workflow_id/reload
|
55
|
-
#
|
55
|
+
#
|
56
56
|
# Reloads the workflow from the adapter
|
57
57
|
post :reload do
|
58
58
|
return four_oh_four unless workflow
|
59
|
-
workflow.
|
59
|
+
workflow.reload_data
|
60
60
|
end
|
61
61
|
|
62
62
|
# DELETE /workflows/:workflow_id
|
63
|
-
#
|
63
|
+
#
|
64
64
|
# Not implemented
|
65
65
|
delete do
|
66
66
|
# delete a workflow (maybe?)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class GroupedLoader < Loader
|
4
|
+
|
5
|
+
def run_load!(set)
|
6
|
+
Cellect::Server.adapter.load_data_for(workflow.name) do |hash|
|
7
|
+
set[hash['group_id']] ||= workflow.set_klass.new
|
8
|
+
set[hash['group_id']].add hash['id'], hash['priority']
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,29 +1,20 @@
|
|
1
1
|
module Cellect
|
2
2
|
module Server
|
3
3
|
class GroupedWorkflow < Workflow
|
4
|
-
attr_accessor :groups
|
5
4
|
|
6
5
|
# Sets up the new workflow
|
7
6
|
def initialize(name, pairwise: false, prioritized: false)
|
8
|
-
self.
|
7
|
+
self.subjects = { }
|
9
8
|
super
|
10
9
|
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
self.state = :initializing
|
15
|
-
self.groups = { }
|
16
|
-
klass = set_klass
|
17
|
-
Cellect::Server.adapter.load_data_for(name).each do |hash|
|
18
|
-
self.groups[hash['group_id']] ||= klass.new
|
19
|
-
self.groups[hash['group_id']].add hash['id'], hash['priority']
|
20
|
-
end
|
21
|
-
self.state = :ready
|
11
|
+
def groups
|
12
|
+
subjects
|
22
13
|
end
|
23
14
|
|
24
|
-
# Returns a group by id or samples one randomly
|
15
|
+
# Returns a group by id or samples one randomly with a fall back to a new group
|
25
16
|
def group(group_id = nil)
|
26
|
-
|
17
|
+
subjects[group_id] || subjects.values.sample || fetch_or_setup_group(group_id)
|
27
18
|
end
|
28
19
|
|
29
20
|
# Get unseen subjects from a group for a user
|
@@ -32,7 +23,7 @@ module Cellect
|
|
32
23
|
end
|
33
24
|
|
34
25
|
# Get a sample of subjects from a group for a user
|
35
|
-
#
|
26
|
+
#
|
36
27
|
# Accepts a hash in the form:
|
37
28
|
# {
|
38
29
|
# user_id: 123,
|
@@ -43,12 +34,12 @@ module Cellect
|
|
43
34
|
if opts[:user_id]
|
44
35
|
unseen_for opts[:user_id], group_id: opts[:group_id], limit: opts[:limit]
|
45
36
|
else
|
46
|
-
|
37
|
+
group(opts[:group_id]).sample opts[:limit]
|
47
38
|
end
|
48
39
|
end
|
49
40
|
|
50
41
|
# Adds or updates a subject in a group
|
51
|
-
#
|
42
|
+
#
|
52
43
|
# Accepts a hash in the form:
|
53
44
|
# {
|
54
45
|
# subject_id: 1,
|
@@ -56,28 +47,32 @@ module Cellect
|
|
56
47
|
# priority: 0.5 # (if the workflow is prioritized)
|
57
48
|
# }
|
58
49
|
def add(opts = { })
|
50
|
+
add_group = fetch_or_setup_group(opts[:group_id])
|
51
|
+
|
59
52
|
if prioritized?
|
60
|
-
|
53
|
+
add_group.add opts[:subject_id], opts[:priority]
|
61
54
|
else
|
62
|
-
|
55
|
+
add_group.add opts[:subject_id]
|
63
56
|
end
|
64
57
|
end
|
65
58
|
|
66
59
|
# Removes a subject from a group
|
67
|
-
#
|
60
|
+
#
|
68
61
|
# Accepts a hash in the form:
|
69
62
|
# {
|
70
63
|
# group_id: 1,
|
71
64
|
# subject_id: 2
|
72
65
|
# }
|
73
66
|
def remove(opts = { })
|
74
|
-
|
67
|
+
if group = subjects[opts[:group_id]]
|
68
|
+
group.remove opts[:subject_id]
|
69
|
+
end
|
75
70
|
end
|
76
71
|
|
77
72
|
# General information about this workflow
|
78
73
|
def status
|
79
74
|
# Get the number of subjects in each group
|
80
|
-
group_counts = Hash[*
|
75
|
+
group_counts = Hash[*subjects.collect{ |id, set| [id, set.size] }.flatten]
|
81
76
|
|
82
77
|
super.merge({
|
83
78
|
grouped: true,
|
@@ -89,6 +84,16 @@ module Cellect
|
|
89
84
|
def grouped?
|
90
85
|
true
|
91
86
|
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def data_loader
|
91
|
+
GroupedLoader.new(self)
|
92
|
+
end
|
93
|
+
|
94
|
+
def fetch_or_setup_group(group_id)
|
95
|
+
subjects[group_id] ||= set_klass.new
|
96
|
+
end
|
92
97
|
end
|
93
98
|
end
|
94
99
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class Loader
|
4
|
+
include Celluloid
|
5
|
+
|
6
|
+
attr_reader :workflow
|
7
|
+
|
8
|
+
def initialize(workflow)
|
9
|
+
@workflow = workflow
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_data
|
13
|
+
run_load!(workflow.subjects)
|
14
|
+
mark_workflow_as_loaded
|
15
|
+
end
|
16
|
+
|
17
|
+
def reload_data(set)
|
18
|
+
run_load!(set)
|
19
|
+
workflow.subjects = set
|
20
|
+
mark_workflow_as_loaded
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def mark_workflow_as_loaded
|
26
|
+
workflow.set_reload_at_time
|
27
|
+
workflow.state = :ready
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_load!(set)
|
31
|
+
Cellect::Server.adapter.load_data_for(workflow.name) do |hash|
|
32
|
+
set.add hash['id'], hash['priority']
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/cellect/server/user.rb
CHANGED
@@ -18,11 +18,13 @@ module Cellect
|
|
18
18
|
self.seen = DiffSet::RandomSet.new
|
19
19
|
monitor Workflow[workflow_name]
|
20
20
|
@ttl = ttl
|
21
|
-
|
21
|
+
self.state = :initializing
|
22
22
|
end
|
23
23
|
|
24
24
|
# Load the seen subjects for a user and restarts the TTL
|
25
25
|
def load_data
|
26
|
+
return if self.state == :ready
|
27
|
+
self.state = :loading
|
26
28
|
data = Cellect::Server.adapter.load_user(workflow_name, id) || []
|
27
29
|
data.each do |subject_id|
|
28
30
|
@seen.add subject_id
|
@@ -8,8 +8,11 @@ module Cellect
|
|
8
8
|
end
|
9
9
|
self.workflow_names = { }
|
10
10
|
|
11
|
-
attr_accessor :name, :users, :subjects, :state
|
12
|
-
|
11
|
+
attr_accessor :name, :users, :subjects, :state, :pairwise,
|
12
|
+
:prioritized, :can_reload_at
|
13
|
+
|
14
|
+
SKIP_RELOAD_STATES = [ :reloading, :loading, :initializing ].freeze
|
15
|
+
RELOAD_TIMEOUT = ENV.fetch('RELOAD_TIMEOUT', 600).to_i.freeze
|
13
16
|
|
14
17
|
# Look up and/or load a workflow
|
15
18
|
def self.[](name)
|
@@ -41,23 +44,32 @@ module Cellect
|
|
41
44
|
self.users = { }
|
42
45
|
self.pairwise = !!pairwise
|
43
46
|
self.prioritized = !!prioritized
|
44
|
-
self.subjects
|
47
|
+
self.subjects ||= set_klass.new
|
48
|
+
self.state = :initializing
|
45
49
|
end
|
46
50
|
|
47
51
|
# Loads subjects from the adapter
|
48
52
|
def load_data
|
49
|
-
|
50
|
-
self.
|
51
|
-
|
52
|
-
|
53
|
+
return if [:loading, :ready ].include? state
|
54
|
+
self.state = :loading
|
55
|
+
data_loader.async.load_data
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reloads subjects from the adapter
|
59
|
+
def reload_data
|
60
|
+
if can_reload_data?
|
61
|
+
self.state = :reloading
|
62
|
+
reload_set = subjects.class.new
|
63
|
+
data_loader.async.reload_data(reload_set)
|
53
64
|
end
|
54
|
-
self.state = :ready
|
55
65
|
end
|
56
66
|
|
57
67
|
# Look up and/or load a user
|
58
68
|
def user(id)
|
59
69
|
self.users[id] ||= User.supervise id, workflow_name: name
|
60
|
-
users[id].actors.first
|
70
|
+
user = self.users[id].actors.first
|
71
|
+
user.load_data
|
72
|
+
user
|
61
73
|
end
|
62
74
|
|
63
75
|
# Get unseen subjects for a user
|
@@ -79,7 +91,7 @@ module Cellect
|
|
79
91
|
end
|
80
92
|
|
81
93
|
# Get a sample of subjects for a user
|
82
|
-
#
|
94
|
+
#
|
83
95
|
# Accepts a hash in the form:
|
84
96
|
# {
|
85
97
|
# user_id: 123,
|
@@ -94,7 +106,7 @@ module Cellect
|
|
94
106
|
end
|
95
107
|
|
96
108
|
# Adds or updates a subject
|
97
|
-
#
|
109
|
+
#
|
98
110
|
# Accepts a hash in the form:
|
99
111
|
# {
|
100
112
|
# subject_id: 1,
|
@@ -109,7 +121,7 @@ module Cellect
|
|
109
121
|
end
|
110
122
|
|
111
123
|
# Removes a subject
|
112
|
-
#
|
124
|
+
#
|
113
125
|
# Accepts a hash in the form:
|
114
126
|
# {
|
115
127
|
# subject_id: 1
|
@@ -145,7 +157,7 @@ module Cellect
|
|
145
157
|
|
146
158
|
# Looks up the set class
|
147
159
|
def set_klass
|
148
|
-
SET_KLASS[[prioritized, pairwise]]
|
160
|
+
@set_klass ||= SET_KLASS[[prioritized, pairwise]]
|
149
161
|
end
|
150
162
|
|
151
163
|
# General information about this workflow
|
@@ -160,6 +172,24 @@ module Cellect
|
|
160
172
|
users: users.length
|
161
173
|
}
|
162
174
|
end
|
175
|
+
|
176
|
+
def can_reload_data?
|
177
|
+
if SKIP_RELOAD_STATES.include?(self.state)
|
178
|
+
false
|
179
|
+
elsif can_reload_at.nil?
|
180
|
+
true
|
181
|
+
else
|
182
|
+
can_reload_at <= Time.now
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def set_reload_at_time(time_stamp=Time.now + RELOAD_TIMEOUT)
|
187
|
+
self.can_reload_at = time_stamp
|
188
|
+
end
|
189
|
+
|
190
|
+
def data_loader
|
191
|
+
Loader.new(self)
|
192
|
+
end
|
163
193
|
end
|
164
194
|
end
|
165
195
|
end
|
data/lib/cellect/version.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cellect::Server
|
4
|
+
describe API do
|
5
|
+
include_context 'API'
|
6
|
+
|
7
|
+
{ 'Ungrouped' => nil, 'Grouped' => 'grouped' }.each_pair do |grouping_type, grouping|
|
8
|
+
SET_TYPES.shuffle.each do |set_type|
|
9
|
+
context "#{ grouping_type } #{ set_type }" do
|
10
|
+
let(:workflow_type){ [grouping, set_type].compact.join '_' }
|
11
|
+
let(:workflow){ Workflow[workflow_type] }
|
12
|
+
before(:each){ pass_until_state_of workflow, is: :ready }
|
13
|
+
|
14
|
+
it 'should call reload_data' do
|
15
|
+
expect(workflow).to receive(:reload_data)
|
16
|
+
post "/workflows/#{ workflow_type }/reload"
|
17
|
+
expect(last_response.status).to eq 201
|
18
|
+
expect(json).to be_nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cellect::Server
|
4
|
+
describe Loader do
|
5
|
+
SET_TYPES.collect{ |type| "grouped_#{ type }" }.each do |workflow_type|
|
6
|
+
it_behaves_like "loader" do
|
7
|
+
let(:workflow) { GroupedWorkflow.new(workflow_type) }
|
8
|
+
let(:loader) { GroupedLoader.new(workflow) }
|
9
|
+
let(:subjects) { {} }
|
10
|
+
let(:group_counts) do
|
11
|
+
fixtures.map { |f| f["group_id"] }.uniq.count
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#load_data" do
|
15
|
+
it "should setup the groups" do
|
16
|
+
expect(workflow.subjects)
|
17
|
+
.to receive(:[]=)
|
18
|
+
.exactly(group_counts)
|
19
|
+
.and_call_original
|
20
|
+
loader.load_data
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should add data to the workflow subjects" do
|
24
|
+
set = workflow.set_klass.new
|
25
|
+
allow(workflow.set_klass)
|
26
|
+
.to receive(:new)
|
27
|
+
.and_return(set)
|
28
|
+
expect(set)
|
29
|
+
.to receive(:add)
|
30
|
+
.exactly(fixture_count)
|
31
|
+
loader.load_data
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#reload_data" do
|
36
|
+
it "should add data to the workflow subjects" do
|
37
|
+
expect(subjects)
|
38
|
+
.to receive(:[]=)
|
39
|
+
.exactly(group_counts)
|
40
|
+
.and_call_original
|
41
|
+
loader.reload_data(subjects)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -4,67 +4,136 @@ module Cellect::Server
|
|
4
4
|
describe GroupedWorkflow do
|
5
5
|
SET_TYPES.collect{ |type| "grouped_#{ type }" }.each do |workflow_type|
|
6
6
|
context workflow_type do
|
7
|
-
|
8
|
-
let(:workflow){ GroupedWorkflow[workflow_type] }
|
7
|
+
let(:workflow){ GroupedWorkflow.new(workflow_type) }
|
9
8
|
let(:user){ workflow.user 123 }
|
10
9
|
let(:set_klass){ workflow.prioritized? ? DiffSet::PrioritySet : DiffSet::RandomSet }
|
11
|
-
before(:each){ pass_until_state_of workflow, is: :ready }
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
workflow.groups[1] = set_klass.new
|
16
|
-
expect(workflow.groups[1]).to receive(:subtract).with user.seen, 3
|
17
|
-
workflow.unseen_for 123, limit: 3
|
11
|
+
it_behaves_like 'workflow', :workflow do
|
12
|
+
let(:obj) { workflow }
|
18
13
|
end
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
describe "#group" do
|
16
|
+
context "no group is pre-defined" do
|
17
|
+
it "should be an empty set" do
|
18
|
+
expect(workflow.subjects).to be_empty
|
19
|
+
end
|
25
20
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
21
|
+
it "should setup a group" do
|
22
|
+
expect(workflow.group(1)).to be_instance_of(workflow.set_klass)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should setup a group if no group_id param" do
|
26
|
+
expect(workflow.group).to be_instance_of(workflow.set_klass)
|
27
|
+
end
|
28
|
+
end
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
context "with a pre-defined group" do
|
31
|
+
let(:set) { workflow.set_klass.new }
|
32
|
+
before do
|
33
|
+
workflow.subjects = { 1 => set }
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should retrieve a pre-defined group' do
|
37
|
+
expect(workflow.group(1)).to eq(set)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should randomly select if no group param' do
|
41
|
+
expect(workflow.group).to eq(set)
|
42
|
+
end
|
43
|
+
end
|
37
44
|
end
|
38
45
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
describe "#unseen_for" do
|
47
|
+
it 'should not fail if a group is not loaded' do
|
48
|
+
workflow.subjects = { }
|
49
|
+
expect { workflow.unseen_for 123, limit: 3 }.not_to raise_error
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should provide unseen from a random group for users' do
|
53
|
+
workflow.subjects = { }
|
54
|
+
workflow.groups[1] = set_klass.new
|
55
|
+
expect(workflow.groups[1]).to receive(:subtract).with user.seen, 3
|
56
|
+
workflow.unseen_for 123, limit: 3
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should provide unseen from a specific group for users' do
|
60
|
+
3.times{ |i| workflow.groups[i] = set_klass.new }
|
61
|
+
expect(workflow.group(1)).to receive(:subtract).with user.seen, 3
|
62
|
+
workflow.unseen_for 123, group_id: 1, limit: 3
|
63
|
+
end
|
44
64
|
end
|
45
65
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
66
|
+
describe "#sample" do
|
67
|
+
it 'should not fail if a group is not loaded' do
|
68
|
+
workflow.subjects = { }
|
69
|
+
expect { workflow.sample limit: 3 }.not_to raise_error
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should sample subjects from a random group without a user' do
|
73
|
+
workflow.subjects = { }
|
74
|
+
workflow.groups[1] = set_klass.new
|
75
|
+
expect(workflow.group(1)).to receive(:sample).with 3
|
76
|
+
workflow.sample limit: 3
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should sample subjects from a specific group without a user' do
|
80
|
+
3.times{ |i| workflow.groups[i] = set_klass.new }
|
81
|
+
expect(workflow.group(1)).to receive(:sample).with 3
|
82
|
+
workflow.sample group_id: 1, limit: 3
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should sample subjects from a random group for a user' do
|
86
|
+
workflow.subjects = { }
|
87
|
+
workflow.groups[1] = set_klass.new
|
88
|
+
expect(workflow.groups[1]).to receive(:subtract).with user.seen, 3
|
89
|
+
workflow.sample user_id: 123, limit: 3
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should sample subjects from a specific group for a user' do
|
93
|
+
3.times{ |i| workflow.groups[i] = set_klass.new }
|
94
|
+
expect(workflow.group(1)).to receive(:subtract).with user.seen, 3
|
95
|
+
workflow.sample user_id: 123, group_id: 1, limit: 3
|
96
|
+
end
|
50
97
|
end
|
51
98
|
|
52
|
-
|
53
|
-
|
99
|
+
describe "#add" do
|
100
|
+
let(:opts) do
|
101
|
+
opts = { subject_id: 123, group_id: 1 }
|
102
|
+
opts[:priority] = 456 if workflow.prioritized?
|
103
|
+
opts
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should add a data to a new group if a group is not loaded' do
|
107
|
+
workflow.subjects = { }
|
108
|
+
workflow.add opts
|
109
|
+
group_data = workflow.subjects[1]
|
110
|
+
expect(group_data.to_a).to_not eq(opts[:subject_id])
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should add subjects' do
|
114
|
+
workflow.groups[1] = set_klass.new
|
54
115
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
116
|
+
if workflow.prioritized?
|
117
|
+
expect(workflow.groups[1]).to receive(:add).with 123, 456
|
118
|
+
workflow.add opts
|
119
|
+
else
|
120
|
+
expect(workflow.groups[1]).to receive(:add).with 123
|
121
|
+
workflow.add opts
|
122
|
+
end
|
61
123
|
end
|
62
124
|
end
|
63
125
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
126
|
+
describe "#remove" do
|
127
|
+
it 'should not fail if a group is not loaded' do
|
128
|
+
workflow.subjects = { }
|
129
|
+
expect { workflow.remove subject_id: 123, group_id: 1 }.not_to raise_error
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should remove subjects' do
|
133
|
+
workflow.groups[1] = set_klass.new
|
134
|
+
expect(workflow.groups[1]).to receive(:remove).with 123
|
135
|
+
workflow.remove subject_id: 123, group_id: 1
|
136
|
+
end
|
68
137
|
end
|
69
138
|
|
70
139
|
it 'should be grouped' do
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cellect::Server
|
4
|
+
describe Loader do
|
5
|
+
SET_TYPES.each do |workflow_type|
|
6
|
+
it_behaves_like "loader" do
|
7
|
+
let(:workflow) { Workflow.new(workflow_type) }
|
8
|
+
let(:loader) { Loader.new(workflow) }
|
9
|
+
let(:subjects) { workflow.set_klass.new }
|
10
|
+
|
11
|
+
describe "#load_data" do
|
12
|
+
it "should add data to the workflow subjects" do
|
13
|
+
expect(workflow.subjects)
|
14
|
+
.to receive(:add)
|
15
|
+
.exactly(fixture_count)
|
16
|
+
loader.load_data
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#reload_data" do
|
21
|
+
it "should add data to the workflow subjects" do
|
22
|
+
expect(subjects)
|
23
|
+
.to receive(:add)
|
24
|
+
.exactly(fixture_count)
|
25
|
+
loader.reload_data(subjects)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -28,5 +28,24 @@ module Cellect::Server
|
|
28
28
|
user.ttl_expired!
|
29
29
|
expect(user.ttl_timer).to be_nil
|
30
30
|
end
|
31
|
+
|
32
|
+
describe '#load_data' do
|
33
|
+
it 'should request data from the adapater' do
|
34
|
+
expect(Cellect::Server.adapter)
|
35
|
+
.to receive(:load_user)
|
36
|
+
.with(user.workflow_name, user.id)
|
37
|
+
.and_return([])
|
38
|
+
user.load_data
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should add data to seens' do
|
42
|
+
expect { user.load_data }.to change { user.seen.size }
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should not add new subjects when already loaded' do
|
46
|
+
user.load_data
|
47
|
+
expect { user.load_data }.not_to change { user.seen.size }
|
48
|
+
end
|
49
|
+
end
|
31
50
|
end
|
32
51
|
end
|
@@ -9,10 +9,12 @@ module Cellect::Server
|
|
9
9
|
|
10
10
|
SET_TYPES.each do |workflow_type|
|
11
11
|
context workflow_type do
|
12
|
-
|
13
|
-
let(:
|
14
|
-
|
15
|
-
|
12
|
+
let(:workflow) { Workflow.new(workflow_type) }
|
13
|
+
let(:user) { workflow.user 123 }
|
14
|
+
|
15
|
+
it_behaves_like 'workflow', :workflow do
|
16
|
+
let(:obj) { workflow }
|
17
|
+
end
|
16
18
|
|
17
19
|
it 'should provide unseen for users' do
|
18
20
|
expect(workflow.subjects).to receive(:subtract).with user.seen, 3
|
@@ -46,7 +48,7 @@ module Cellect::Server
|
|
46
48
|
|
47
49
|
it 'should be notified of a user ttl expiry' do
|
48
50
|
async_workflow = double
|
49
|
-
expect(workflow).to receive(:async).and_return async_workflow
|
51
|
+
expect(Workflow[workflow.name]).to receive(:async).and_return async_workflow
|
50
52
|
expect(async_workflow).to receive(:remove_user).with user.id
|
51
53
|
user.ttl_expired!
|
52
54
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
shared_examples_for 'loader' do |name|
|
2
|
+
let(:fixtures) do
|
3
|
+
Cellect::Server
|
4
|
+
.adapter
|
5
|
+
.fixtures
|
6
|
+
.fetch(workflow.name, { })
|
7
|
+
.fetch('entries', [])
|
8
|
+
end
|
9
|
+
let(:fixture_count) { fixtures.count }
|
10
|
+
|
11
|
+
describe "#load_data" do
|
12
|
+
it "should use the server adapter to load data" do
|
13
|
+
expect(Cellect::Server.adapter)
|
14
|
+
.to receive(:load_data_for)
|
15
|
+
.with(workflow.name)
|
16
|
+
.and_call_original
|
17
|
+
loader.load_data
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should mark the workflow as loaded" do
|
21
|
+
expect { loader.load_data }.to change { workflow.state }.to(:ready)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#reload_data" do
|
26
|
+
it "should replace the existing subjects with the reloaded set" do
|
27
|
+
expect {
|
28
|
+
loader.reload_data(subjects)
|
29
|
+
}.to change {
|
30
|
+
workflow.subjects
|
31
|
+
}.to(subjects)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should mark the workflow as ready" do
|
35
|
+
expect {
|
36
|
+
loader.reload_data(subjects)
|
37
|
+
}.to change {
|
38
|
+
workflow.state
|
39
|
+
}.to(:ready)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,10 +1,4 @@
|
|
1
1
|
shared_examples_for 'workflow' do |name|
|
2
|
-
let(:obj){ send name }
|
3
|
-
|
4
|
-
before(:each) do
|
5
|
-
Cellect::Server.adapter.load_workflow obj.name
|
6
|
-
end
|
7
|
-
|
8
2
|
it 'should add singleton instances to the registry' do
|
9
3
|
expect(obj.class[obj.name]).to be_a_kind_of Cellect::Server::Workflow
|
10
4
|
expect(obj.class[obj.name].object_id).to eq obj.class[obj.name].object_id
|
@@ -14,8 +8,12 @@ shared_examples_for 'workflow' do |name|
|
|
14
8
|
expect(obj.name).to be_a String
|
15
9
|
expect(obj.users).to be_a Hash
|
16
10
|
|
17
|
-
|
18
|
-
|
11
|
+
if obj.grouped?
|
12
|
+
expect(obj.subjects).to eq({})
|
13
|
+
else
|
14
|
+
set_klass = obj.prioritized? ? DiffSet::PrioritySet : DiffSet::RandomSet
|
15
|
+
expect(obj.subjects).to be_a set_klass
|
16
|
+
end
|
19
17
|
end
|
20
18
|
|
21
19
|
it 'should provide a user lookup' do
|
@@ -23,4 +21,82 @@ shared_examples_for 'workflow' do |name|
|
|
23
21
|
expect(obj.user(1).object_id).to eq obj.user(1).object_id
|
24
22
|
expect(obj.users.keys).to include 1
|
25
23
|
end
|
24
|
+
|
25
|
+
context "with a celluloid stubbed async loader" do
|
26
|
+
let(:loader) do
|
27
|
+
if obj.grouped?
|
28
|
+
Cellect::Server::GroupedLoader.new(obj)
|
29
|
+
else
|
30
|
+
Cellect::Server::Loader.new(obj)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
let(:celluloid_target) { loader.wrapped_object }
|
34
|
+
|
35
|
+
before do
|
36
|
+
allow(obj.wrapped_object).to receive(:data_loader).and_return(loader)
|
37
|
+
allow(loader).to receive(:async).and_return(loader)
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#load_data' do
|
41
|
+
it 'should request data from the loader' do
|
42
|
+
expect(celluloid_target).to receive(:load_data)
|
43
|
+
obj.load_data
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should not attempt to reload subjects when in ready state' do
|
47
|
+
obj.state = :ready
|
48
|
+
expect(celluloid_target).not_to receive(:load_data)
|
49
|
+
obj.load_data
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#reload_data' do
|
54
|
+
context "able to reload" do
|
55
|
+
before do
|
56
|
+
obj.can_reload_at = Time.now - 1
|
57
|
+
obj.state = :ready
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should request data from the loader' do
|
61
|
+
expect(celluloid_target)
|
62
|
+
.to receive(:reload_data)
|
63
|
+
.with(instance_of(obj.subjects.class))
|
64
|
+
obj.reload_data
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should not reload subjects when state is in any kind of loading' do
|
68
|
+
%i(reloading loading).each do |state|
|
69
|
+
obj.state = state
|
70
|
+
expect(celluloid_target).not_to receive(:reload_data)
|
71
|
+
obj.reload_data
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should not allow reloading until after loading' do
|
77
|
+
expect(celluloid_target).not_to receive(:reload_data)
|
78
|
+
obj.reload_data
|
79
|
+
end
|
80
|
+
|
81
|
+
context "after initial data load" do
|
82
|
+
before do
|
83
|
+
obj.state = :ready
|
84
|
+
obj.set_reload_at_time
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should not allow reloading after first load' do
|
88
|
+
expect(celluloid_target).not_to receive(:reload_data)
|
89
|
+
obj.reload_data
|
90
|
+
end
|
91
|
+
|
92
|
+
context "when reload time gaurds has past" do
|
93
|
+
it 'should allow reloading after timer has reset' do
|
94
|
+
obj.can_reload_at = Time.now - 1
|
95
|
+
expect(celluloid_target).to receive(:reload_data)
|
96
|
+
obj.reload_data
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
26
102
|
end
|
@@ -10,7 +10,12 @@ class SpecAdapter < Cellect::Server::Adapters::Default
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def load_data_for(workflow_name)
|
13
|
-
fixtures.fetch(workflow_name, { }).fetch 'entries', []
|
13
|
+
entries = fixtures.fetch(workflow_name, { }).fetch 'entries', []
|
14
|
+
if block_given?
|
15
|
+
entries.each { |entry| yield entry }
|
16
|
+
else
|
17
|
+
entries
|
18
|
+
end
|
14
19
|
end
|
15
20
|
|
16
21
|
def fixtures
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cellect-server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Parrish
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -266,7 +266,9 @@ files:
|
|
266
266
|
- lib/cellect/server/api/helpers.rb
|
267
267
|
- lib/cellect/server/api/sets.rb
|
268
268
|
- lib/cellect/server/api/users.rb
|
269
|
+
- lib/cellect/server/grouped_loader.rb
|
269
270
|
- lib/cellect/server/grouped_workflow.rb
|
271
|
+
- lib/cellect/server/loader.rb
|
270
272
|
- lib/cellect/server/node_set.rb
|
271
273
|
- lib/cellect/server/user.rb
|
272
274
|
- lib/cellect/server/workflow.rb
|
@@ -274,10 +276,13 @@ files:
|
|
274
276
|
- log/.gitkeep
|
275
277
|
- spec/cellect/server/api/add_seen_spec.rb
|
276
278
|
- spec/cellect/server/api/add_spec.rb
|
279
|
+
- spec/cellect/server/api/reload_spec.rb
|
277
280
|
- spec/cellect/server/api/remove_spec.rb
|
278
281
|
- spec/cellect/server/api/sample_spec.rb
|
279
282
|
- spec/cellect/server/api/user_load_spec.rb
|
283
|
+
- spec/cellect/server/grouped_loader_spec.rb
|
280
284
|
- spec/cellect/server/grouped_workflow_spec.rb
|
285
|
+
- spec/cellect/server/loader_spec.rb
|
281
286
|
- spec/cellect/server/node_set_spec.rb
|
282
287
|
- spec/cellect/server/server_spec.rb
|
283
288
|
- spec/cellect/server/user_spec.rb
|
@@ -296,6 +301,7 @@ files:
|
|
296
301
|
- spec/spec_helper.rb
|
297
302
|
- spec/support/cellect_helper.rb
|
298
303
|
- spec/support/shared_api_context.rb
|
304
|
+
- spec/support/shared_examples_for_loader.rb
|
299
305
|
- spec/support/shared_examples_for_set.rb
|
300
306
|
- spec/support/shared_examples_for_workflow.rb
|
301
307
|
- spec/support/spec_adapter.rb
|
@@ -327,10 +333,13 @@ summary: ''
|
|
327
333
|
test_files:
|
328
334
|
- spec/cellect/server/api/add_seen_spec.rb
|
329
335
|
- spec/cellect/server/api/add_spec.rb
|
336
|
+
- spec/cellect/server/api/reload_spec.rb
|
330
337
|
- spec/cellect/server/api/remove_spec.rb
|
331
338
|
- spec/cellect/server/api/sample_spec.rb
|
332
339
|
- spec/cellect/server/api/user_load_spec.rb
|
340
|
+
- spec/cellect/server/grouped_loader_spec.rb
|
333
341
|
- spec/cellect/server/grouped_workflow_spec.rb
|
342
|
+
- spec/cellect/server/loader_spec.rb
|
334
343
|
- spec/cellect/server/node_set_spec.rb
|
335
344
|
- spec/cellect/server/server_spec.rb
|
336
345
|
- spec/cellect/server/user_spec.rb
|
@@ -349,6 +358,7 @@ test_files:
|
|
349
358
|
- spec/spec_helper.rb
|
350
359
|
- spec/support/cellect_helper.rb
|
351
360
|
- spec/support/shared_api_context.rb
|
361
|
+
- spec/support/shared_examples_for_loader.rb
|
352
362
|
- spec/support/shared_examples_for_set.rb
|
353
363
|
- spec/support/shared_examples_for_workflow.rb
|
354
364
|
- spec/support/spec_adapter.rb
|