cellect-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +2 -0
  5. data/README.md +19 -0
  6. data/Rakefile +9 -0
  7. data/cellect-server.gemspec +40 -0
  8. data/cellect.gemspec +33 -0
  9. data/lib/cellect.rb +7 -0
  10. data/lib/cellect/node_set.rb +38 -0
  11. data/lib/cellect/server.rb +30 -0
  12. data/lib/cellect/server/adapters.rb +13 -0
  13. data/lib/cellect/server/adapters/default.rb +60 -0
  14. data/lib/cellect/server/adapters/postgres.rb +64 -0
  15. data/lib/cellect/server/api.rb +48 -0
  16. data/lib/cellect/server/api/helpers.rb +44 -0
  17. data/lib/cellect/server/api/sets.rb +21 -0
  18. data/lib/cellect/server/api/users.rb +32 -0
  19. data/lib/cellect/server/grouped_project.rb +65 -0
  20. data/lib/cellect/server/node_set.rb +19 -0
  21. data/lib/cellect/server/project.rb +123 -0
  22. data/lib/cellect/server/user.rb +66 -0
  23. data/lib/cellect/version.rb +3 -0
  24. data/spec/fixtures/project_data/grouped_pairwise_priority.json +109 -0
  25. data/spec/fixtures/project_data/grouped_pairwise_random.json +89 -0
  26. data/spec/fixtures/project_data/grouped_priority.json +59 -0
  27. data/spec/fixtures/project_data/grouped_random.json +49 -0
  28. data/spec/fixtures/project_data/pairwise_priority.json +49 -0
  29. data/spec/fixtures/project_data/pairwise_random.json +39 -0
  30. data/spec/fixtures/project_data/priority.json +49 -0
  31. data/spec/fixtures/project_data/random.json +39 -0
  32. data/spec/fixtures/user_data/complete_user.json +118 -0
  33. data/spec/fixtures/user_data/new_user.json +26 -0
  34. data/spec/fixtures/user_data/partial_user.json +58 -0
  35. data/spec/server/api/add_seen_spec.rb +26 -0
  36. data/spec/server/api/add_spec.rb +40 -0
  37. data/spec/server/api/remove_spec.rb +35 -0
  38. data/spec/server/api/sample_spec.rb +34 -0
  39. data/spec/server/api/user_load_spec.rb +25 -0
  40. data/spec/server/grouped_project_spec.rb +76 -0
  41. data/spec/server/node_set_spec.rb +13 -0
  42. data/spec/server/project_spec.rb +62 -0
  43. data/spec/server/server_spec.rb +24 -0
  44. data/spec/server/user_spec.rb +32 -0
  45. data/spec/spec_helper.rb +43 -0
  46. data/spec/support/cellect_helper.rb +12 -0
  47. data/spec/support/shared_api_context.rb +11 -0
  48. data/spec/support/shared_examples_for_node_set.rb +27 -0
  49. data/spec/support/shared_examples_for_project.rb +26 -0
  50. data/spec/support/shared_examples_for_set.rb +34 -0
  51. data/spec/support/spec_adapter.rb +43 -0
  52. data/spec/support/zk_setup.rb +26 -0
  53. 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,3 @@
1
+ module Cellect
2
+ VERSION = '0.0.1'
3
+ 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
+ }