cellect-server 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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/README.md +19 -0
- data/Rakefile +9 -0
- data/cellect-server.gemspec +40 -0
- data/cellect.gemspec +33 -0
- data/lib/cellect.rb +7 -0
- data/lib/cellect/node_set.rb +38 -0
- data/lib/cellect/server.rb +30 -0
- data/lib/cellect/server/adapters.rb +13 -0
- data/lib/cellect/server/adapters/default.rb +60 -0
- data/lib/cellect/server/adapters/postgres.rb +64 -0
- data/lib/cellect/server/api.rb +48 -0
- data/lib/cellect/server/api/helpers.rb +44 -0
- data/lib/cellect/server/api/sets.rb +21 -0
- data/lib/cellect/server/api/users.rb +32 -0
- data/lib/cellect/server/grouped_project.rb +65 -0
- data/lib/cellect/server/node_set.rb +19 -0
- data/lib/cellect/server/project.rb +123 -0
- data/lib/cellect/server/user.rb +66 -0
- data/lib/cellect/version.rb +3 -0
- data/spec/fixtures/project_data/grouped_pairwise_priority.json +109 -0
- data/spec/fixtures/project_data/grouped_pairwise_random.json +89 -0
- data/spec/fixtures/project_data/grouped_priority.json +59 -0
- data/spec/fixtures/project_data/grouped_random.json +49 -0
- data/spec/fixtures/project_data/pairwise_priority.json +49 -0
- data/spec/fixtures/project_data/pairwise_random.json +39 -0
- data/spec/fixtures/project_data/priority.json +49 -0
- data/spec/fixtures/project_data/random.json +39 -0
- data/spec/fixtures/user_data/complete_user.json +118 -0
- data/spec/fixtures/user_data/new_user.json +26 -0
- data/spec/fixtures/user_data/partial_user.json +58 -0
- data/spec/server/api/add_seen_spec.rb +26 -0
- data/spec/server/api/add_spec.rb +40 -0
- data/spec/server/api/remove_spec.rb +35 -0
- data/spec/server/api/sample_spec.rb +34 -0
- data/spec/server/api/user_load_spec.rb +25 -0
- data/spec/server/grouped_project_spec.rb +76 -0
- data/spec/server/node_set_spec.rb +13 -0
- data/spec/server/project_spec.rb +62 -0
- data/spec/server/server_spec.rb +24 -0
- data/spec/server/user_spec.rb +32 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/cellect_helper.rb +12 -0
- data/spec/support/shared_api_context.rb +11 -0
- data/spec/support/shared_examples_for_node_set.rb +27 -0
- data/spec/support/shared_examples_for_project.rb +26 -0
- data/spec/support/shared_examples_for_set.rb +34 -0
- data/spec/support/spec_adapter.rb +43 -0
- data/spec/support/zk_setup.rb +26 -0
- metadata +337 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class API
|
4
|
+
class Sets < Grape::API
|
5
|
+
get do
|
6
|
+
project.sample selector_params
|
7
|
+
end
|
8
|
+
|
9
|
+
put :add do
|
10
|
+
project.add update_params
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
put :remove do
|
15
|
+
project.remove update_params
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class API
|
4
|
+
class Users < Grape::API
|
5
|
+
resources :users do
|
6
|
+
segment '/:user_id' do
|
7
|
+
put :add_seen do
|
8
|
+
user_id = param_to_int :user_id
|
9
|
+
subject_id = param_to_int :subject_id
|
10
|
+
|
11
|
+
if user_id && user_id > 0 && subject_id && subject_id > 0
|
12
|
+
project.async.add_seen_for user_id, subject_id
|
13
|
+
end
|
14
|
+
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
post :load do
|
19
|
+
user_id = param_to_int :user_id
|
20
|
+
|
21
|
+
if user_id && user_id > 0
|
22
|
+
project.async.user user_id
|
23
|
+
end
|
24
|
+
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class GroupedProject < Project
|
4
|
+
attr_accessor :groups
|
5
|
+
|
6
|
+
def initialize(name, pairwise: false, prioritized: false)
|
7
|
+
self.groups = { }
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_data
|
12
|
+
self.state = :initializing
|
13
|
+
self.groups = { }
|
14
|
+
klass = set_klass
|
15
|
+
Cellect::Server.adapter.load_data_for(name).each do |hash|
|
16
|
+
self.groups[hash['group_id']] ||= klass.new
|
17
|
+
self.groups[hash['group_id']].add hash['id'], hash['priority']
|
18
|
+
end
|
19
|
+
self.state = :ready
|
20
|
+
end
|
21
|
+
|
22
|
+
def group(group_id = nil)
|
23
|
+
groups[group_id] || groups.values.sample
|
24
|
+
end
|
25
|
+
|
26
|
+
def unseen_for(user_name, group_id: nil, limit: 5)
|
27
|
+
group(group_id).subtract user(user_name).seen, limit
|
28
|
+
end
|
29
|
+
|
30
|
+
def sample(opts = { })
|
31
|
+
if opts[:user_id]
|
32
|
+
unseen_for opts[:user_id], group_id: opts[:group_id], limit: opts[:limit]
|
33
|
+
else
|
34
|
+
group(opts[:group_id]).sample opts[:limit]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add(opts = { })
|
39
|
+
if prioritized?
|
40
|
+
groups[opts[:group_id]].add opts[:subject_id], opts[:priority]
|
41
|
+
else
|
42
|
+
groups[opts[:group_id]].add opts[:subject_id]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove(opts = { })
|
47
|
+
groups[opts[:group_id]].remove opts[:subject_id]
|
48
|
+
end
|
49
|
+
|
50
|
+
def status
|
51
|
+
group_counts = Hash[*groups.collect{ |id, set| [id, set.size] }.flatten]
|
52
|
+
|
53
|
+
super.merge({
|
54
|
+
grouped: true,
|
55
|
+
subjects: group_counts.values.inject(:+),
|
56
|
+
groups: group_counts
|
57
|
+
})
|
58
|
+
end
|
59
|
+
|
60
|
+
def grouped?
|
61
|
+
true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'cellect/node_set'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Cellect
|
5
|
+
module Server
|
6
|
+
class NodeSet < Cellect::NodeSet
|
7
|
+
attr_accessor :id
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def setup
|
12
|
+
zk.mkdir_p '/nodes'
|
13
|
+
ip = Socket.ip_address_list.find{ |address| address.ipv4? && !address.ipv4_loopback? }.ip_address
|
14
|
+
path = zk.create '/nodes/node', data: ip, mode: :ephemeral_sequential
|
15
|
+
self.id = path.sub /^\/nodes\//, ''
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class Project
|
4
|
+
include Celluloid
|
5
|
+
|
6
|
+
attr_accessor :name, :users, :subjects, :state
|
7
|
+
attr_accessor :pairwise, :prioritized
|
8
|
+
|
9
|
+
def self.[](name, pairwise: false, prioritized: false)
|
10
|
+
key = "project_#{ name }".to_sym
|
11
|
+
Actor[key] ||= supervise name, pairwise: pairwise, prioritized: prioritized
|
12
|
+
Actor[key].actors.first
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.names
|
16
|
+
actor_names = Celluloid.actor_system.registry.names.collect &:to_s
|
17
|
+
project_actors = actor_names.select{ |key| key =~ /^project_/ }
|
18
|
+
project_actors.collect{ |name| name.sub(/^project_/, '').to_sym }
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all
|
22
|
+
names.collect{ |name| Project[name] }
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(name, pairwise: false, prioritized: false)
|
26
|
+
self.name = name
|
27
|
+
self.users = { }
|
28
|
+
self.pairwise = !!pairwise
|
29
|
+
self.prioritized = !!prioritized
|
30
|
+
self.subjects = set_klass.new
|
31
|
+
load_data
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_data
|
35
|
+
self.state = :initializing
|
36
|
+
self.subjects = set_klass.new
|
37
|
+
Cellect::Server.adapter.load_data_for(name).each do |hash|
|
38
|
+
subjects.add hash['id'], hash['priority']
|
39
|
+
end
|
40
|
+
self.state = :ready
|
41
|
+
end
|
42
|
+
|
43
|
+
def user(id)
|
44
|
+
self.users[id] ||= User.supervise id, project_name: name
|
45
|
+
users[id].actors.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def unseen_for(user_id, limit: 5)
|
49
|
+
subjects.subtract user(user_id).seen, limit
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_seen_for(user_id, *subject_ids)
|
53
|
+
[subject_ids].flatten.compact.each do |subject_id|
|
54
|
+
user(user_id).seen.add subject_id
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove_user(user_id)
|
59
|
+
removed = self.users.delete user_id
|
60
|
+
removed.terminate if removed
|
61
|
+
end
|
62
|
+
|
63
|
+
def sample(opts = { })
|
64
|
+
if opts[:user_id]
|
65
|
+
unseen_for opts[:user_id], limit: opts[:limit]
|
66
|
+
else
|
67
|
+
subjects.sample opts[:limit]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def add(opts = { })
|
72
|
+
if prioritized?
|
73
|
+
subjects.add opts[:subject_id], opts[:priority]
|
74
|
+
else
|
75
|
+
subjects.add opts[:subject_id]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def remove(opts = { })
|
80
|
+
subjects.remove opts[:subject_id]
|
81
|
+
end
|
82
|
+
|
83
|
+
def pairwise?
|
84
|
+
!!pairwise
|
85
|
+
end
|
86
|
+
|
87
|
+
def prioritized?
|
88
|
+
!!prioritized
|
89
|
+
end
|
90
|
+
|
91
|
+
def grouped?
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
def ready?
|
96
|
+
state == :ready
|
97
|
+
end
|
98
|
+
|
99
|
+
SET_KLASS = {
|
100
|
+
# priority, pairwise
|
101
|
+
[ false, false ] => DiffSet::RandomSet,
|
102
|
+
[ false, true ] => DiffSet::PairwiseRandomSet,
|
103
|
+
[ true, false ] => DiffSet::PrioritySet,
|
104
|
+
[ true, true ] => DiffSet::PairwisePrioritySet
|
105
|
+
}
|
106
|
+
|
107
|
+
def set_klass
|
108
|
+
SET_KLASS[[prioritized, pairwise]]
|
109
|
+
end
|
110
|
+
|
111
|
+
def status
|
112
|
+
{
|
113
|
+
state: state,
|
114
|
+
grouped: false,
|
115
|
+
prioritized: prioritized,
|
116
|
+
pairwise: pairwise,
|
117
|
+
subjects: subjects.size,
|
118
|
+
users: users.length
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Cellect
|
2
|
+
module Server
|
3
|
+
class User
|
4
|
+
include Celluloid
|
5
|
+
include Celluloid::Logger
|
6
|
+
|
7
|
+
trap_exit :project_crashed
|
8
|
+
finalizer :cancel_ttl_timer
|
9
|
+
|
10
|
+
attr_accessor :id, :project_name, :seen, :state
|
11
|
+
attr_accessor :ttl, :ttl_timer
|
12
|
+
|
13
|
+
def initialize(id, project_name: nil, ttl: nil)
|
14
|
+
self.id = id
|
15
|
+
self.project_name = project_name
|
16
|
+
self.seen = DiffSet::RandomSet.new
|
17
|
+
monitor Project[project_name]
|
18
|
+
@ttl = ttl
|
19
|
+
load_data
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_data
|
23
|
+
data = Cellect::Server.adapter.load_user(project_name, id) || []
|
24
|
+
data.each do |subject_id|
|
25
|
+
@seen.add subject_id
|
26
|
+
end
|
27
|
+
self.state = :ready
|
28
|
+
restart_ttl_timer
|
29
|
+
end
|
30
|
+
|
31
|
+
def seen
|
32
|
+
restart_ttl_timer
|
33
|
+
@seen
|
34
|
+
end
|
35
|
+
|
36
|
+
def state_changed(topic, state)
|
37
|
+
restart_ttl_timer if state == :ready && ttl
|
38
|
+
end
|
39
|
+
|
40
|
+
def restart_ttl_timer
|
41
|
+
self.ttl_timer ||= after(ttl){ ttl_expired! }
|
42
|
+
ttl_timer.reset
|
43
|
+
end
|
44
|
+
|
45
|
+
def cancel_ttl_timer
|
46
|
+
ttl_timer.cancel if ttl_timer
|
47
|
+
self.ttl_timer = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def ttl_expired!
|
51
|
+
debug "User #{ id } TTL expired"
|
52
|
+
cancel_ttl_timer
|
53
|
+
Project[project_name].async.remove_user(id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def ttl
|
57
|
+
@ttl || 60 * 15 # 15 minutes
|
58
|
+
end
|
59
|
+
|
60
|
+
def project_crashed(actor, reason)
|
61
|
+
cancel_ttl_timer
|
62
|
+
terminate
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
{
|
2
|
+
"id": 8,
|
3
|
+
"name": "grouped_pairwise_priority",
|
4
|
+
"prioritized": true,
|
5
|
+
"pairwise": true,
|
6
|
+
"grouped": true,
|
7
|
+
"entries": [
|
8
|
+
{
|
9
|
+
"id": 16,
|
10
|
+
"priority": -6,
|
11
|
+
"group_id": 1
|
12
|
+
},
|
13
|
+
{
|
14
|
+
"id": 17,
|
15
|
+
"priority": -7,
|
16
|
+
"group_id": 2
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"id": 3,
|
20
|
+
"priority": 7,
|
21
|
+
"group_id": 3
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"id": 8,
|
25
|
+
"priority": 2,
|
26
|
+
"group_id": 2
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"id": 7,
|
30
|
+
"priority": 3,
|
31
|
+
"group_id": 1
|
32
|
+
},
|
33
|
+
{
|
34
|
+
"id": 13,
|
35
|
+
"priority": -3,
|
36
|
+
"group_id": 1
|
37
|
+
},
|
38
|
+
{
|
39
|
+
"id": 14,
|
40
|
+
"priority": -4,
|
41
|
+
"group_id": 2
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"id": 4,
|
45
|
+
"priority": 6,
|
46
|
+
"group_id": 1
|
47
|
+
},
|
48
|
+
{
|
49
|
+
"id": 6,
|
50
|
+
"priority": 4,
|
51
|
+
"group_id": 3
|
52
|
+
},
|
53
|
+
{
|
54
|
+
"id": 18,
|
55
|
+
"priority": -8,
|
56
|
+
"group_id": 3
|
57
|
+
},
|
58
|
+
{
|
59
|
+
"id": 12,
|
60
|
+
"priority": -2,
|
61
|
+
"group_id": 3
|
62
|
+
},
|
63
|
+
{
|
64
|
+
"id": 1,
|
65
|
+
"priority": 9,
|
66
|
+
"group_id": 1
|
67
|
+
},
|
68
|
+
{
|
69
|
+
"id": 11,
|
70
|
+
"priority": -1,
|
71
|
+
"group_id": 2
|
72
|
+
},
|
73
|
+
{
|
74
|
+
"id": 10,
|
75
|
+
"priority": 0,
|
76
|
+
"group_id": 1
|
77
|
+
},
|
78
|
+
{
|
79
|
+
"id": 9,
|
80
|
+
"priority": 1,
|
81
|
+
"group_id": 3
|
82
|
+
},
|
83
|
+
{
|
84
|
+
"id": 20,
|
85
|
+
"priority": -10,
|
86
|
+
"group_id": 2
|
87
|
+
},
|
88
|
+
{
|
89
|
+
"id": 2,
|
90
|
+
"priority": 8,
|
91
|
+
"group_id": 2
|
92
|
+
},
|
93
|
+
{
|
94
|
+
"id": 15,
|
95
|
+
"priority": -5,
|
96
|
+
"group_id": 3
|
97
|
+
},
|
98
|
+
{
|
99
|
+
"id": 5,
|
100
|
+
"priority": 5,
|
101
|
+
"group_id": 2
|
102
|
+
},
|
103
|
+
{
|
104
|
+
"id": 19,
|
105
|
+
"priority": -9,
|
106
|
+
"group_id": 1
|
107
|
+
}
|
108
|
+
]
|
109
|
+
}
|