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.
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
+ }