cellect-server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|