seapig-server 0.0.8 → 0.1.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/bin/seapig-server +582 -189
  3. data/lib/seapig/version.rb +1 -1
  4. metadata +71 -123
  5. data/test/dummy/README.rdoc +0 -28
  6. data/test/dummy/Rakefile +0 -6
  7. data/test/dummy/app/assets/javascripts/application.js +0 -15
  8. data/test/dummy/app/assets/javascripts/json-patch.js +0 -392
  9. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  10. data/test/dummy/app/controllers/application_controller.rb +0 -9
  11. data/test/dummy/app/helpers/application_helper.rb +0 -2
  12. data/test/dummy/app/views/application/index.html.slim +0 -10
  13. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  14. data/test/dummy/bin/bundle +0 -3
  15. data/test/dummy/bin/rails +0 -4
  16. data/test/dummy/bin/rake +0 -4
  17. data/test/dummy/bin/setup +0 -29
  18. data/test/dummy/config.ru +0 -4
  19. data/test/dummy/config/application.rb +0 -26
  20. data/test/dummy/config/boot.rb +0 -5
  21. data/test/dummy/config/database.yml +0 -85
  22. data/test/dummy/config/environment.rb +0 -5
  23. data/test/dummy/config/environments/development.rb +0 -41
  24. data/test/dummy/config/environments/production.rb +0 -79
  25. data/test/dummy/config/environments/test.rb +0 -42
  26. data/test/dummy/config/initializers/assets.rb +0 -11
  27. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  28. data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  29. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  30. data/test/dummy/config/initializers/inflections.rb +0 -16
  31. data/test/dummy/config/initializers/mime_types.rb +0 -4
  32. data/test/dummy/config/initializers/session_store.rb +0 -3
  33. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  34. data/test/dummy/config/locales/en.yml +0 -23
  35. data/test/dummy/config/routes.rb +0 -56
  36. data/test/dummy/config/secrets.yml +0 -22
  37. data/test/dummy/lib/seapigs/random.rb +0 -14
  38. data/test/dummy/public/404.html +0 -67
  39. data/test/dummy/public/422.html +0 -67
  40. data/test/dummy/public/500.html +0 -66
  41. data/test/dummy/public/favicon.ico +0 -0
  42. data/test/integration/navigation_test.rb +0 -8
  43. data/test/seapig_test.rb +0 -7
  44. data/test/test_helper.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a12affdf7b5e5fae828d4ec4711c9912dbb877e6
4
- data.tar.gz: 0cff8ba163170572dc32683835dfd8a5b5492474
3
+ metadata.gz: c92d464df0fe01cbc76a752b14954adb3382f2c7
4
+ data.tar.gz: 5e3523365c94ab376bbe71f45eddff0dccafd519
5
5
  SHA512:
6
- metadata.gz: 2e3e2ee217ad73eedbfb54bbff9e19267a721fa78e2b4123658a0e03fb3b98ed58434bce5a72c0fe82c680413daa33bd931f38c0cc5e0f6d6d4081ddd7ba25b9
7
- data.tar.gz: 61398b760963440170c373f8e1d6a4a29179070a5625353449afe15b4a477c165d0e945e69528ea009ead439916cb9c1eab0d7c65b10e22156c6faf8eb07b84c
6
+ metadata.gz: d61241a434eea5aa514014603e9ee54fecb35486e0eeae099479fdd0924fdfbd2875d2b878a0d77c40355c34663cabe0cbd27301ca1d77e3347d1319958b1ea2
7
+ data.tar.gz: a270181bcef678e4742cb03074dc629fe6509e98602b93b7dac0c6ca5201d2c22e5b1c90da3683dc1f5a88c6a48db67da3081e6e66aa19eb4db75eb013c5120a
@@ -2,13 +2,41 @@
2
2
  # coding: utf-8
3
3
 
4
4
  require 'websocket-eventmachine-server'
5
- require 'json'
6
- require 'jsondiff'
5
+ require 'narray'
6
+ require 'oj'
7
+ require 'json-diff'
7
8
  require 'hana'
8
9
  require 'set'
9
10
 
11
+
10
12
  DEBUG = (ARGV[0] == "debug")
11
13
  INFO = (DEBUG or ARGV[0] == "info")
14
+ HOST = (ARGV[1] or "127.0.0.1").split(":")[0]
15
+ PORT = ((ARGV[1] or '').split(':')[1] or "3001").to_i
16
+
17
+ OBJECT_CACHE_SIZE = 1
18
+
19
+ $stdout.sync = true
20
+
21
+ Oj.default_options = { mode: :strict }
22
+
23
+
24
+ module WebSocket
25
+ module Frame
26
+ class Data < String
27
+ def getbytes(start_index, count)
28
+ data = self[start_index, count]
29
+ if @masking_key
30
+ payload_na = NArray.to_na(data,"byte")
31
+ mask_na = NArray.to_na((@masking_key.pack("C*")*((data.size/4) + 1))[0...data.size],"byte")
32
+ data = (mask_na ^ payload_na).to_s
33
+ end
34
+ data
35
+ end
36
+ end
37
+ end
38
+ end
39
+
12
40
 
13
41
 
14
42
  class String
@@ -25,165 +53,416 @@ class String
25
53
  end
26
54
 
27
55
 
56
+ Signal.trap("USR1") {
57
+ t1 = Time.new; GC.start ;d = Time.new - t1
58
+ puts "Long GC run:\n        %.3fs"%(d) if DEBUG and d > 0.05
59
+ }
28
60
 
29
- class SeapigObject
30
61
 
31
- attr_reader :id, :version, :valid
62
+ #
63
+ # Code is layered, with each layer only communicating with neighbouring layers (e.g. object store never directly talks to em or sockets).
64
+ #
65
+ # Object Store is a singleton responsible for:
66
+ # * managing objects' lifetime
67
+ # * dependency tracking and triggering object rebuilds (aka. production)
68
+ # * tracking of available producers and consumers
69
+ #
70
+ # Client class is responsible for:
71
+ # * keeping track of clients and their state
72
+ # * keeping network communication efficient (diffing)
73
+ #
74
+ # Eventmachine main loop is a:
75
+ # * router between physical world and Client class / instances
76
+ #
32
77
 
33
- @@objects_by_id = Hash.new { |hash,object_id|
34
- object = SeapigObject.new(object_id)
35
- puts "Creating: "+object.id if DEBUG
36
- hash[object_id] = object
37
- }
38
78
 
79
+ module SeapigObjectStore
80
+
81
+ @@objects_by_id = {} # {id => object}; stores all existing SeapigObjects
82
+
83
+ @@producers = {} # {pattern_or_id => {client}}; for assessing spawning possibility
84
+ @@consumers = {} # {pattern_or_id => {client}}; for assessing spawning need, for assessing holding need
85
+
86
+ @@dependents = {} # {id_depended_on => {id_depending}}; for assessing spawning need, for assessing holding need, for assessing reproduction need
87
+ @@dependencies = {} # {id_depending => {id_depended_on}}; for updating dependents
88
+
89
+ @@queue = [] # [object]; objects in need of production
90
+ @@producing = {} # {client => object}; for assessing client busy status
91
+ @@produced = {} # {id_being_produced => {version}}; for assessing enqueuing/dequeuing need
39
92
 
40
- def self.[](id)
41
- @@objects_by_id[id]
93
+
94
+ def self.consumer_register(pattern_or_id, client)
95
+ @@consumers[pattern_or_id] = Set.new if not @@consumers[pattern_or_id]
96
+ @@consumers[pattern_or_id].add(client)
97
+ self.matching(pattern_or_id, @@producers.merge(@@objects_by_id)).each { |matching_id|
98
+ @@objects_by_id[matching_id].consumer_register(pattern_or_id, client) if @@objects_by_id[matching_id]
99
+ self.spawn(matching_id) if not @@objects_by_id[matching_id]
100
+ }
42
101
  end
43
102
 
44
103
 
45
- def self.all
46
- @@objects_by_id.values
104
+ def self.producer_register(pattern_or_id, client)
105
+ @@producers[pattern_or_id] = Set.new if not @@producers[pattern_or_id]
106
+ @@producers[pattern_or_id].add(client)
107
+ self.matching(pattern_or_id, @@consumers.merge(@@dependents)).each { |matching_id|
108
+ @@objects_by_id[matching_id].producer_register(pattern_or_id, client) if @@objects_by_id[matching_id]
109
+ self.spawn(matching_id) if not @@objects_by_id[matching_id]
110
+ }
111
+ self.dequeue(client,nil) if not @@producing[client]
47
112
  end
48
113
 
49
114
 
50
- def self.matching(id)
51
- @@objects_by_id.values.select { |object| object.matches?(id) }
115
+ def self.consumer_unregister(pattern_or_id, client)
116
+ raise "Unregister without register" if not @@consumers[pattern_or_id].include?(client)
117
+ @@consumers[pattern_or_id].delete(client)
118
+ @@consumers.delete(pattern_or_id) if @@consumers[pattern_or_id].size == 0
119
+ self.matching(pattern_or_id,@@producers.merge(@@objects_by_id)).each { |matching_id|
120
+ @@objects_by_id[matching_id].consumer_unregister(pattern_or_id, client) if @@objects_by_id[matching_id]
121
+ self.despawn(@@objects_by_id[matching_id]) if @@objects_by_id[matching_id] and (not @@objects_by_id[matching_id].alive?) and (not @@dependents[pattern_or_id])
122
+ }
52
123
  end
53
124
 
54
125
 
55
- def matches?(id)
56
- @id =~ id.starexp
126
+ def self.producer_unregister(pattern_or_id,client)
127
+ raise "Unregister without register" if not @@producers[pattern_or_id].include?(client)
128
+ @@producers[pattern_or_id].delete(client)
129
+ @@producers.delete(pattern_or_id) if @@producers[pattern_or_id].size == 0
130
+ self.matching(pattern_or_id,@@consumers.merge(@@dependents)).each { |matching_id|
131
+ @@objects_by_id[matching_id].producer_unregister(pattern_or_id, client) if @@objects_by_id[matching_id]
132
+ self.despawn(@@objects_by_id[matching_id]) if @@objects_by_id[matching_id] and (not @@objects_by_id[matching_id].alive?) and (not @@dependents[pattern_or_id])
133
+ }
57
134
  end
58
135
 
59
-
60
- def initialize(id)
61
- @id = id
62
- @valid = false
63
- @object = {}
64
- @version = 0
65
- @stall_after = nil
66
- @@objects_by_id[@id] = self
136
+
137
+ def self.version_get(client,id,version)
138
+ raise "version_get called on starexp, that doesn't make sense" if id.starexp?
139
+ return [0,{}] if not @@objects_by_id.has_key?(id)
140
+ @@objects_by_id[id].version_get(version)
67
141
  end
68
142
 
69
-
70
- def self.gc
71
- used_object_ids = Set.new
72
- Client.all.each { |client| used_object_ids << client.producing.id if client.producing } #objects currently being produced
73
- Client.all.each { |client| used_object_ids.merge(client.consumes.map { |object| object.id }) } #objects with direct consuments (no pattern matching)
74
- @@objects_by_id.values.each { |object| used_object_ids << object.id if Client.all.find { |client| client.produces.include?(object.id) } and Client.all.find { |client| client.consumes.find { |consumed| consumed.id.starexp? and object.id =~ consumed.id.starexp } } } #objects having producers AND wildcard consuments
75
- @@objects_by_id.values.each { |object| object.version.keys.each { |key| used_object_ids << key } if object.version.kind_of?(Hash) } # objects that others depend on
76
- @@objects_by_id.keys.select { |id| not used_object_ids.include?(id) }.each { |id|
77
- Client.all.select { |client| client.consumes.find { |object| object.id.starexp? and id =~ object.id.starexp } }.each { |client|
78
- puts "Destroying: "+id if DEBUG
79
- client.socket.send JSON.dump(action: 'object-destroy', id: id)
80
- }
81
- puts "Deleting: "+id if DEBUG
82
- @@objects_by_id.delete(id)
83
- }
143
+
144
+ def self.version_set(client,id,version,data,requested_version)
145
+ raise "Update of pattern doesn't make sense" if id.starexp?
146
+
147
+ if requested_version != false
148
+ raise "client not in @@producing" if not @@producing[client]
149
+ raise "requested_version (%s) not in @@produced[id] (%s)"%[requested_version.inspect,@@produced[id].inspect] if not @@produced[id].include?(requested_version)
150
+ @@producing.delete(client)
151
+ @@produced[id].delete(requested_version) # also on disconnection / unproducer / test
152
+ @@produced.delete(id) if @@produced[id].size == 0
153
+ end
154
+
155
+ if @@objects_by_id.has_key?(id) or @@dependents[id] or @@consumers.keys.find { |pattern| id =~ pattern.starexp }
156
+ object = (@@objects_by_id[id] or self.spawn(id))
157
+ accepted = object.version_set(data, version, requested_version)
158
+ if accepted
159
+ (@@dependents[id] or Set.new).each { |dependent_id|
160
+ raise if not @@objects_by_id.has_key?(dependent_id)
161
+ next if not (dependent = @@objects_by_id[dependent_id])
162
+ if dependent.version_needed and dependent.version_needed[id] and version.kind_of?(Integer) and dependent.version_needed[id].kind_of?(Integer) and dependent.version_needed[id] < version
163
+ dependent.version_needed[id] = version
164
+ enqueue(dependent)
165
+ end
166
+ }
167
+ if version.kind_of? Hash
168
+ object.version_needed = {} if not object.version_needed
169
+ old_dependencies = (@@dependencies[id] or Set.new)
170
+ new_dependencies = (@@dependencies[id] = Set.new(version.keys))
171
+ (new_dependencies - old_dependencies).each { |added_dependency|
172
+ object.version_needed[added_dependency] = [(@@objects_by_id[added_dependency] ? @@objects_by_id[added_dependency].version_latest : 0), (version[added_dependency] or 0)].max
173
+ dependent_add(added_dependency, object.id)
174
+ }
175
+ (old_dependencies & new_dependencies).each { |kept_dependency|
176
+ object.version_needed[kept_dependency] = [(@@objects_by_id[kept_dependency] ? @@objects_by_id[kept_dependency].version_latest : 0), (version[kept_dependency] or 0)].max
177
+ }
178
+ (old_dependencies - new_dependencies).each { |removed_dependency|
179
+ object.version_needed.delete(removed_dependency)
180
+ dependent_remove(removed_dependency, object.id)
181
+ }
182
+ else
183
+ object.version_needed = version
184
+ end
185
+ end
186
+ enqueue(object)
187
+ end
188
+
189
+ dequeue(client,nil) if requested_version != false and not @@producing[client]
84
190
  end
85
191
 
86
192
 
87
- def patch(patch, value, from_version, new_version)
88
- print "Patching:\n        version:"+@version.inspect+"\n        from_version: "+from_version.inspect+"\n        new_version: "+new_version.inspect+"\n        patch_size: "+(patch and patch.size.to_s or "nil")+"\n        --> " if DEBUG
89
- if from_version == @version or from_version == 0
90
- puts 'clean' if DEBUG
91
- if value and @stall_after
92
- puts '        UNSTALL' if DEBUG
93
- @version = @stall_after
94
- @stall_after = nil
193
+ def self.cache_get(object_id, key)
194
+ return nil if not @@objects_by_id.has_key?(object_id)
195
+ @@objects_by_id[object_id].cache_get(key)
196
+ end
197
+
198
+
199
+ def self.cache_set(object_id, key, value)
200
+ return value if not @@objects_by_id.has_key?(object_id)
201
+ @@objects_by_id[object_id].cache_set(key, value)
202
+ value
203
+ end
204
+
205
+
206
+ private
207
+
208
+
209
+ class SeapigObject
210
+
211
+ attr_reader :id, :versions, :direct_producers, :wildcard_producers
212
+ attr_accessor :version_needed
213
+
214
+ def initialize(id)
215
+ @id = id
216
+ @versions = [ [0, {}] ]
217
+ @direct_consumers = Set.new
218
+ @wildcard_consumers = {}
219
+ @direct_producers = Set.new
220
+ @wildcard_producers = {}
221
+ @version_needed = nil
222
+ @cache = []
223
+ end
224
+
225
+
226
+ def destroy
227
+ @wildcard_consumers.keys.each { |client|
228
+ client.object_destroy(@id)
229
+ }
230
+ end
231
+
232
+
233
+ def version_get(object_version)
234
+ @versions.assoc(object_version) or [0,{}]
235
+ end
236
+
237
+
238
+ def version_set(data,version,requested_version)
239
+ return false if data == nil
240
+ return false if not version_newer?(version)
241
+ @versions << [version,data]
242
+ (Set.new(@wildcard_consumers.keys)+@direct_consumers).each { |client| client.object_update(@id, version, data) } if data
243
+ versions_with_valid_data = 0
244
+ discard_below = @versions.size - 1
245
+ while discard_below > 0 and versions_with_valid_data < 1
246
+ versions_with_valid_data += 1 if @versions[discard_below][1]
247
+ discard_below -= 1
95
248
  end
96
- old_object = JSON.load(JSON.dump(@object))
97
- old_version = @version
98
- @object.clear if from_version == 0 or value != nil
99
- begin
100
- Hana::Patch.new(patch).apply(@object) if patch
101
- rescue Exception => e
102
- puts "Patching failed!\n        Old object: "+old_object.inspect+"\n        Patch: "+patch.inspect if DEBUG
103
- raise e
249
+ discard_below.times { @versions.shift }
250
+ true
251
+ end
252
+
253
+
254
+ def version_latest
255
+ return nil if not @versions[-1]
256
+ @versions[-1][0]
257
+ end
258
+
259
+
260
+ def version_newer?(vb)
261
+ latest = version_latest
262
+ return latest <=> vb if (not latest.kind_of?(Hash)) and (not vb.kind_of?(Hash))
263
+ return -1 if (not latest.kind_of?(Hash)) and ( vb.kind_of?(Hash))
264
+ return 1 if ( latest.kind_of?(Hash)) and (not vb.kind_of?(Hash))
265
+ (latest.keys & vb.keys).each { |key|
266
+ # return true if latest[key] and (vb[key] == nil or vb[key] > latest[key])
267
+ return true if vb[key] > latest[key]
268
+ }
269
+ return vb.size < latest.size #THINK: is this the right way to go...
270
+ end
271
+
272
+
273
+ def consumer_register(pattern,client)
274
+ return false if ((not pattern.starexp?) and @direct_consumers.include?(client)) or (pattern.starexp? and @wildcard_consumers[client] and @wildcard_consumers[client].include?(pattern))
275
+ if pattern.starexp?
276
+ (@wildcard_consumers[client] ||= Set.new).add(pattern)
277
+ else
278
+ @direct_consumers.add(client)
104
279
  end
105
- if value == false and not @stall_after
106
- puts '        STALL' if DEBUG
107
- @stall_after = old_version
280
+ latest_known_version, latest_known_data = @versions.reverse.find { |version,data| data }
281
+ (Set.new(@wildcard_consumers.keys)+@direct_consumers).each { |client| client.object_update(@id, latest_known_version, latest_known_data) }
282
+ end
283
+
284
+
285
+ def producer_register(pattern,client)
286
+ return false if ((not pattern.starexp?) and @direct_producers.include?(client)) or (pattern.starexp? and @wildcard_producers[client] and @wildcard_producers[client].include?(pattern))
287
+ if pattern.starexp?
288
+ (@wildcard_producers[client] ||= Set.new).add(pattern)
289
+ else
290
+ @direct_producers.add(client)
108
291
  end
109
- @object.merge!(value) if value
110
- @version = new_version
111
- Client.all.each { |client| upload(client, old_version, old_object) }
112
- SeapigObject.all.each { |object| object.check_validity }
113
- elsif from_version > @version
114
- puts "lost some updates, reinitializing object" if DEBUG
115
- @version = 0
116
- @object.clear
117
- @valid = false
118
- #assign mb?
292
+ end
293
+
294
+
295
+ def consumer_unregister(pattern,client)
296
+ raise "Unregister without register" if (not @direct_consumers.include?(client)) and ((not @wildcard_consumers.has_key?(client)) or (not @wildcard_consumers[client].include?(pattern)))
297
+ if pattern.starexp?
298
+ @wildcard_consumers[client].delete(pattern)
299
+ @wildcard_consumers.delete(client) if @wildcard_consumers[client].size == 0
300
+ else
301
+ @direct_consumers.delete(client)
302
+ end
303
+ end
304
+
305
+
306
+ def producer_unregister(pattern,client)
307
+ raise "Unregister without register" if (not @direct_producers.include?(client)) and ((not @wildcard_producers.has_key?(client)) or (not @wildcard_producers[client].include?(pattern)))
308
+ if pattern.starexp?
309
+ @wildcard_producers[client].delete(pattern)
310
+ @wildcard_producers.delete(client) if @wildcard_producers[client].size == 0
311
+ else
312
+ @direct_producers.delete(client)
313
+ end
314
+ end
315
+
316
+
317
+ def cache_get(key)
318
+ ret = @cache.assoc(key)
319
+ puts "Cache "+(ret ? "hit" : "miss") if DEBUG
320
+ ret and ret[1]
321
+ end
322
+
323
+
324
+ def cache_set(key, value)
325
+ @cache.delete(old_entry) if old_entry = @cache.assoc(key)
326
+ @cache << [key,value] if OBJECT_CACHE_SIZE > 0
327
+ @cache = @cache[-OBJECT_CACHE_SIZE..-1] if @cache.size > OBJECT_CACHE_SIZE
328
+ end
329
+
330
+
331
+ def alive?
332
+ (@direct_consumers.size > 0 or (@wildcard_consumers.size > 0 and @direct_producers.size > 0))
333
+ end
334
+
335
+
336
+ def inspect
337
+ '<SO:%s:%s:%s:%s:%s:%s:%s>'%[@id, @versions.map { |v| v[0] }.inspect,@direct_producers.map(&:id).inspect,@wildcard_producers.keys.map(&:id).inspect,@direct_consumers.map(&:id).inspect,@wildcard_consumers.keys.map(&:id).inspect,@version_needed.inspect]
338
+ end
339
+
340
+ end
341
+
342
+
343
+ def self.matching(pattern,check_against)
344
+ if pattern.starexp?
345
+ check_against.each_key.map { |id|
346
+ id if (not id.starexp?) and (id =~ pattern.starexp)
347
+ }.compact
348
+ else
349
+ (check_against.each_key.find { |id|
350
+ (id.starexp? and pattern =~ id.starexp) or ((not id.starexp?) and pattern == id)
351
+ }) ? [pattern] : []
352
+ end
353
+ end
354
+
355
+
356
+ def self.spawn(id)
357
+ puts "Creating:\n        "+id if DEBUG
358
+ @@objects_by_id[id] = object = SeapigObject.new(id)
359
+ @@producers.each_pair.map { |pattern,clients| clients.each { |client| object.producer_register(pattern,client) if pattern.starexp? and (id =~ pattern.starexp) or (id == pattern) } }
360
+ @@consumers.each_pair.map { |pattern,clients| clients.each { |client| object.consumer_register(pattern,client) if pattern.starexp? and (id =~ pattern.starexp) or (id == pattern) } }
361
+ enqueue(object)
362
+ object
363
+ end
364
+
365
+
366
+ def self.despawn(object)
367
+ puts "Deleting:\n        "+object.id if DEBUG
368
+ raise "Despawning object that should stay alive" if object.alive? or @@dependents[object.id]
369
+ object.destroy
370
+ (@@dependencies.delete(object.id) or []).each { |dependency_id|
371
+ dependent_remove(dependency_id, object.id)
372
+ }
373
+ @@objects_by_id.delete(object.id)
374
+ end
375
+
376
+
377
+ def self.enqueue(object)
378
+ if object.version_needed and object.version_latest == object.version_needed
379
+ @@queue.delete(object)
119
380
  else
120
- puts "late update, ignoring" if DEBUG
381
+ return if @@queue.include?(object) or (@@produced[object.id] and @@produced[object.id].include?(object.version_needed))
382
+ @@queue << object
383
+ (Set.new(object.direct_producers) + object.wildcard_producers.keys).find { |client|
384
+ dequeue(client, object) if not @@producing[client]
385
+ }
121
386
  end
122
387
  end
123
388
 
124
389
 
125
- def upload(client, old_version, old_object, patch = nil)
126
- return false if @stall_after
127
- return false if not client.consumes.find { |object| (object == self) or self.matches?(object.id) }
128
- old_version, old_object = [0, {}] if not client.versions[self] == old_version
129
- json = JSON.dump(
130
- action: 'object-update',
131
- id: @id,
132
- old_version: old_version,
133
- new_version: @version,
134
- patch: (patch or JsonDiff.generate(old_object, @object)))
135
- puts "Sending %8iB %s to %s"%[json.size, self.id, client.id] if DEBUG
136
- client.versions[self] = @version
137
- client.socket.send json
390
+ def self.dequeue(client,object)
391
+ object = @@queue.find { |candidate_object| candidate_object.direct_producers.include?(client) or candidate_object.wildcard_producers.has_key?(client) } if not object
392
+ return false if not @@queue.include?(object)
393
+ version_snapshot = (object.version_needed == nil ? nil : object.version_needed.clone)
394
+ client.object_produce(object.id, version_snapshot)
395
+ @@queue.delete(object)
396
+ @@producing[client] = object
397
+ (@@produced[object.id] ||= Set.new) << version_snapshot
138
398
  end
139
399
 
140
-
141
- def check_validity
142
- @valid = if not @version.kind_of?(Hash) then (@version > 0) else not @version.to_a.find { |dependency_id, dependency_version|
143
- SeapigObject[dependency_id] and SeapigObject[dependency_id].version and SeapigObject[dependency_id].version > dependency_version
144
- } end
400
+
401
+ def self.dependent_add(id, dependent)
402
+ @@dependents[id] = Set.new if not @@dependents[id]
403
+ @@dependents[id] << dependent
404
+ self.matching(id, @@producers).each { |matching_id|
405
+ self.spawn(matching_id) if not @@objects_by_id[matching_id]
406
+ }
145
407
  end
146
408
 
147
409
 
148
- def inspect
149
- '<SO:%s:%s:%s%s>'%[@id, @version, (@valid and 'V' or 'I'), (@stall_after and 'S' or 'U')]
410
+ def self.dependent_remove(id, dependent)
411
+ @@dependents[id].delete(dependent)
412
+ @@dependents.delete(id) if @@dependents[id].size == 0
413
+ self.despawn(@@objects_by_id[id]) if @@objects_by_id.include?(id) and (not @@objects_by_id[id].alive?) and (not @@dependents[id])
150
414
  end
151
-
415
+
416
+
417
+ def self.pp
418
+ [
419
+ "Objects:", @@objects_by_id.values.map { |object| "        %s"%[object.inspect] }.join("\n"),
420
+ "Queue:", @@queue.map { |object| "        %s"%[object.inspect] }.join("\n"),
421
+ "Producing:", @@producing.map { |client,object| "        %s - %s"%[client.id,object.id] }.join("\n"),
422
+ "Produced:", @@produced.map { |object,versions| "        %s - %s"%[object,versions.inspect] }.join("\n")
423
+ ].select { |str| str.size > 0 }.join("\n")+"\n"
424
+ end
425
+
426
+
427
+
428
+
429
+
430
+
152
431
  end
153
432
 
154
433
 
155
434
 
435
+ #TODO:
436
+ # * Refactor to have ClientSpace class/module with Clients inside
437
+
438
+
156
439
  class Client
157
440
 
158
- attr_reader :produces, :consumes, :socket, :producing, :index, :versions
441
+ attr_reader :produces, :consumes, :socket, :producing, :index, :pong_time
159
442
  attr_accessor :options
160
443
 
161
444
  @@clients_by_socket = {}
162
445
  @@count = 0
163
446
 
164
-
165
- def self.[](id)
166
- @@clients_by_socket[id]
167
- end
168
-
169
447
 
170
- def self.all
171
- @@clients_by_socket.values
448
+ def self.[](socket)
449
+ @@clients_by_socket[socket]
172
450
  end
173
451
 
174
452
 
175
453
  def initialize(socket)
176
454
  @index = @@count += 1
177
- puts 'Client connected: '+@index.to_s if DEBUG
455
+ puts "Client connected:\n        "+@index.to_s if DEBUG
178
456
  @socket = socket
179
457
  @options = {}
180
- @produces = []
181
- @consumes = []
458
+ @produces = Set.new
459
+ @consumes = Set.new
182
460
  @versions = {}
183
461
  @producing = nil
184
462
  @@clients_by_socket[socket] = self
463
+ self.pong
185
464
  end
186
-
465
+
187
466
 
188
467
  def id
189
468
  (@options['name'] or "") + ':' + @index.to_s
@@ -191,140 +470,254 @@ class Client
191
470
 
192
471
 
193
472
  def destroy
194
- puts 'Client disconnected: '+@index.to_s if DEBUG
473
+ puts "Client disconnected:\n        "+@index.to_s if DEBUG
195
474
  @@clients_by_socket.delete(@socket)
196
- SeapigObject.gc
197
- Client.all.find { |client| client.assign(@producing) } if @producing and SeapigObject.all.include?(@producing)
475
+ @produces.each { |pattern| SeapigObjectStore.producer_unregister(pattern,self) }
476
+ @consumes.each { |pattern| SeapigObjectStore.consumer_unregister(pattern,self) }
477
+ producing = @producing
478
+ @producing = nil
479
+ SeapigObjectStore.version_set(self,producing[0],nil,nil,producing[1]) if producing
198
480
  end
199
481
 
200
482
 
201
483
  def producer_register(pattern)
202
- @produces.push(pattern)
203
- SeapigObject.all.each { |object| self.assign(object) }
484
+ @produces.add(pattern)
485
+ SeapigObjectStore.producer_register(pattern, self)
486
+ end
487
+
488
+
489
+ def producer_unregister(pattern)
490
+ @produces.delete(pattern)
491
+ SeapigObjectStore.producer_unregister(pattern, self)
492
+ if @producing and (pattern.starexp? ? (@producing[0] =~ pattern.starexp) : (@producing[0] == pattern)) #NOTE: overlaping production patterns are not supported
493
+ producing = @producing
494
+ @producing = nil
495
+ SeapigObjectStore.version_set(self,producing[0],nil,nil,producing[1])
496
+ end
497
+ end
498
+
499
+
500
+ def consumer_register(pattern)
501
+ @consumes.add(pattern)
502
+ SeapigObjectStore.consumer_register(pattern, self)
204
503
  end
205
504
 
206
505
 
207
- def consumer_register(object)
208
- @consumes.push(object)
209
- Client.all.each { |client| client.produces.each { |pattern| SeapigObject[pattern] if (not pattern.starexp?) and (pattern =~ object.id.starexp) } } if object.id.starexp?
210
- SeapigObject.matching(object.id).each { |object|
211
- Client.all.find { |client| client.assign(object) }
212
- object.upload(self,0,{}) if object.valid
506
+ def consumer_unregister(pattern)
507
+ @consumes.delete(pattern)
508
+ SeapigObjectStore.consumer_unregister(pattern, self)
509
+ gc_versions
510
+ end
511
+
512
+
513
+ def gc_versions
514
+ @versions.keys.each { |object_id|
515
+ @versions.delete(object_id) if not (@consumes).find { |pattern|
516
+ pattern.starexp? and (object_id =~ pattern.starexp) or (pattern == object_id)
517
+ }
213
518
  }
214
519
  end
215
520
 
216
521
 
217
- def consumer_unregister(object)
218
- @consumes.delete(object)
219
- @versions.delete(object)
220
- SeapigObject.gc
522
+ def object_update(object_id, object_version, object_data)
523
+ #THINK: should we propagate stalls to clients?
524
+ return if object_version == 0 or object_version == @versions[object_id]
525
+ old_version, old_data = SeapigObjectStore.version_get(self,object_id,(@versions[object_id] or 0))
526
+ data = if old_version == 0
527
+ { "value" => object_data }
528
+ else
529
+ diff = SeapigObjectStore.cache_get(object_id,[:diff,old_version,object_version])
530
+ diff = SeapigObjectStore.cache_set(object_id,[:diff,old_version,object_version],JsonDiff.diff(old_data, object_data)) if not diff
531
+ { "patch" => diff }
532
+ end
533
+
534
+ json = Oj.dump({
535
+ "action" => 'object-update',
536
+ "id" => object_id,
537
+ "old_version" => old_version,
538
+ "new_version" => object_version,
539
+ }.merge(data))
540
+ puts "Sending:\n        %8iB %s to %s"%[json.size, object_id, id] if DEBUG
541
+ @versions[object_id] = object_version
542
+ @socket.send json
221
543
  end
222
544
 
223
545
 
224
- def assign(object)
225
- puts 'Assign? %20s <> %-30s - %s'%[self.id, object.id, [object.valid,object.id.starexp?,@producing,Client.all.find { |client| client.producing == object },(not @produces.find { |pattern| object.id =~ pattern.starexp })].map { |b| b and 'T' or 'F' }.join('')] if DEBUG
226
- return true if object.valid
227
- return true if object.id.starexp?
228
- return false if @producing
229
- return true if Client.all.find { |client| client.producing == object }
230
- return false if not @produces.find { |pattern| object.id =~ pattern.starexp }
231
- puts 'Assigning: '+object.id+' to: '+self.id if DEBUG
232
- @socket.send JSON.dump(action: 'object-produce', id: object.id)
233
- @producing = object
546
+ def object_destroy(object_id)
547
+ @socket.send Oj.dump("action" => 'object-destroy', "id" => object_id)
234
548
  end
235
549
 
236
550
 
237
- def release(object)
238
- puts 'Releasing: '+object.id+' from: '+self.id if DEBUG
239
- @producing = nil if @producing == object
551
+ def object_patch(object_id, patch, value, from_version, to_version)
552
+ raise "patching wildcard object. no." if object_id.starexp?
553
+ requested_object_id, requested_version = @producing
554
+ if requested_object_id == object_id
555
+ @producing = nil
556
+ else
557
+ requested_version = false
558
+ end
559
+ new_version = to_version
560
+
561
+ new_data = if patch
562
+ object_version, object_data = SeapigObjectStore.version_get(self,object_id,from_version)
563
+ print "Patching:\n        version: "+object_version.inspect+"\n        from_version: "+from_version.inspect+"\n        to_version: "+to_version.inspect+"\n        patch_size: "+(patch and patch.size.to_s or "nil")+"\n        --> " if DEBUG
564
+ if from_version == object_version
565
+ puts 'clean' if DEBUG
566
+ new_data = Oj.load(Oj.dump(object_data))
567
+ begin
568
+ Hana::Patch.new(patch).apply(new_data) if patch
569
+ rescue Exception => e
570
+ puts "Patching failed!\n        Old object: "+object_data.inspect+"\n        Patch: "+patch.inspect if DEBUG
571
+ raise e
572
+ end
573
+ new_data
574
+ else
575
+ puts "can't update object, couldn't find base version" if DEBUG
576
+ nil
577
+ end
578
+ elsif value != nil
579
+ print "Setting:\n        version: "+object_version.inspect+"\n        from_version: "+from_version.inspect+"\n        to_version: "+to_version.inspect+"\n        value_size: "+(value.inspect.size.to_s)+"\n" if DEBUG
580
+ value
581
+ else
582
+ nil
583
+ end
584
+
585
+ SeapigObjectStore.version_set(self,object_id,new_version,new_data,requested_version)
240
586
  end
241
587
 
242
-
243
- def ping
244
- @socket.ping
588
+
589
+ def object_produce(object_id, object_version)
590
+ raise "Can't produce a wildcard object" if object_id.starexp?
591
+ raise "Client already producing something (producing: %s, trying to assign: %s)"%[@producing.inspect, [object_id,object_version].inspect] if @producing
592
+ raise "Can't produce that pattern: "+@produces.inspect+" "+object_id.inspect if not @produces.find { |pattern| object_id =~ pattern.starexp }
593
+ puts "Assigning:\n        "+object_id+':'+object_version.inspect+' to: '+self.id if DEBUG
594
+ @socket.send Oj.dump("action" => 'object-produce', "id" => object_id)
595
+ @producing = [object_id, object_version]
245
596
  end
246
597
 
247
-
598
+
248
599
  def pong
249
600
  @pong_time = Time.new
250
601
  end
251
602
 
252
-
253
- def check_ping_timeout
254
- @socket.close if Time.new - pong > 60
603
+
604
+ def self.send_pings
605
+ @@clients_by_socket.keys.each { |socket| socket.ping }
606
+ end
607
+
608
+
609
+ def self.send_heartbeats
610
+ @@clients_by_socket.each_pair { |socket,client| socket.send Oj.dump(action: 'heartbeat') if client.options['heartbeat'] }
611
+ end
612
+
613
+
614
+ def self.check_ping_timeouts
615
+ @@clients_by_socket.each_pair { |socket,client| socket.close if Time.new - client.pong_time > 60 }
616
+ end
617
+
618
+
619
+ def self.pp
620
+ "Clients:\n"+@@clients_by_socket.values.map { |client| "        %-20s produces:%s consumes:%s"%[client.id,client.produces.to_a,client.consumes.to_a] }.join("\n")+"\n"
255
621
  end
622
+ end
256
623
 
257
624
 
258
- def send_heartbeat
259
- @socket.send JSON.dump(action: 'heartbeat') if @options['heartbeat']
625
+ class InternalClient
626
+
627
+ def self.produce
628
+ end
629
+
630
+ def initialize
631
+ SeapigObjectStore.producer_register("SeapigServer::Objects", self)
632
+ end
633
+
634
+ def object_produce(object_id, object_version)
635
+ objects =
636
+ SeapigObjectStore.version_set(object_id,new_version,objects,object_version)
260
637
  end
261
638
 
262
639
  end
263
640
 
264
641
 
642
+ #TODO:
643
+ # * change protocol to use "pattern" instead of "id"
644
+ # * change "object-patch" to something nicer
645
+
646
+
265
647
  processing_times = []
266
648
  processing_times_sum = 0
267
649
 
268
650
  EM.run {
269
651
 
270
652
 
271
- WebSocket::EventMachine::Server.start(host: "0.0.0.0", port: 3001) { |client_socket|
272
-
653
+ WebSocket::EventMachine::Server.start(host: HOST, port: PORT) { |client_socket|
654
+
273
655
  client_socket.onmessage { |message|
274
- started_at = Time.new
275
- client = Client[client_socket]
276
- message = JSON.load message
277
- puts "-"*80 + ' ' + Time.new.to_s if DEBUG
278
- print "Message: %-20s %-30s %-50s"%[client.id, message['action'], JSON.dump(message.select { |k,v| ['pattern','id','options'].include?(k) })] if INFO
279
- puts if DEBUG
280
- object = SeapigObject[message['id']] if message['id']
281
- case message['action']
282
- when 'object-producer-register'
283
- fail unless message['pattern']
284
- client.producer_register(message['pattern'])
285
- when 'object-producer-unregister'
286
- fail unless message['pattern']
287
- client.producer_unregister(message['pattern'])
288
- when 'object-patch'
289
- fail unless message['id'] and message['new_version'] and message['old_version']
290
- client.release(object)
291
- SeapigObject.gc
292
- object.patch(message['patch'], message['value'], message['old_version'], message['new_version']) if SeapigObject.all.include?(object) # ignoring objects nobody listens to
293
- SeapigObject.all.each { |object| Client.all.find { |client| client.assign(object) } }
294
- when 'object-consumer-register'
295
- fail unless message['id']
296
- client.consumer_register(object)
297
- when 'object-consumer-unregister'
298
- fail unless message['id']
299
- client.consumer_unregister(object)
300
- when 'client-options-set'
301
- fail unless message['options']
302
- client.options = message['options']
303
- else
304
- puts '***** WTF, got message with action: ' + message['action'].inspect
305
- end
306
- processing_times << (Time.new.to_f - started_at.to_f)
307
- processing_times_sum += processing_times[-1]
308
- if DEBUG
309
- puts "Clients:\n"+Client.all.map { |client| "        %-20s produces:%s consumes:%s"%[client.id,client.produces.inspect,client.consumes.map { |obj| obj.id }] }.join("\n")+"\n"
310
- puts "Objects:\n"+SeapigObject.all.map { |object| "        %s"%[object.inspect] }.join("\n")+"\n"
311
- puts "Processing:\n        time: %.3fs\n        count: %i\n        average: %.3fs\n        total: %.3fs"%[processing_times[-1], processing_times.size, processing_times_sum / processing_times.size, processing_times_sum]
312
- STDOUT.flush
656
+ begin
657
+ started_at = Time.new
658
+ client = Client[client_socket]
659
+ message = Oj.load message
660
+ puts "-"*80 + ' ' + Time.new.to_s if DEBUG
661
+ print "Message:\n        from: %-20s\n        action: %-30s\n        param: %-50s "%[client.id, message['action'], Oj.dump(message.select { |k,v| ['pattern','id','options'].include?(k) })] if DEBUG
662
+ puts if DEBUG
663
+ object_id = message['id'] if message['id']
664
+ case message['action']
665
+ when 'object-producer-register'
666
+ fail unless message['pattern']
667
+ client.producer_register(message['pattern'])
668
+ when 'object-producer-unregister'
669
+ fail unless message['pattern']
670
+ client.producer_unregister(message['pattern'])
671
+ when 'object-patch'
672
+ fail unless message['id']
673
+ client.object_patch(object_id,message['patch'], message['value'], message['old_version'], message['new_version'])
674
+ when 'object-consumer-register'
675
+ fail unless message['id']
676
+ client.consumer_register(object_id)
677
+ when 'object-consumer-unregister'
678
+ fail unless message['id']
679
+ client.consumer_unregister(object_id)
680
+ when 'client-options-set'
681
+ fail unless message['options']
682
+ client.options = message['options']
683
+ else
684
+ raise 'WTF, got message with action: ' + message['action'].inspect
685
+ end
686
+ processing_times << (Time.new.to_f - started_at.to_f)
687
+ processing_times_sum += processing_times[-1]
688
+ if DEBUG
689
+ puts Client.pp
690
+ puts SeapigObjectStore.pp
691
+ puts "Processing:\n        time: %.3fs\n        count: %i\n        average: %.3fs\n        total: %.3fs"%[processing_times[-1], processing_times.size, processing_times_sum / processing_times.size, processing_times_sum]
692
+ end
693
+ puts "message:%3i t:%.3fs Σt:%.3fs t̅:%.3fs"%[processing_times.size, processing_times[-1], processing_times_sum, processing_times_sum / processing_times.size,] if INFO and not DEBUG
694
+ rescue => e
695
+ puts "Message processing error:\n        "
696
+ p e
697
+ e.backtrace.each { |line| puts line }
698
+ raise
313
699
  end
314
- puts "ct:%3i t:%.3fs Σt:%.3fs t̅:%.3fs"%[processing_times.size, processing_times[-1], processing_times_sum, processing_times_sum / processing_times.size,] if INFO and not DEBUG
315
-
316
700
  }
317
701
 
318
-
702
+
319
703
  client_socket.onopen { Client.new(client_socket) }
320
704
  client_socket.onclose { Client[client_socket].destroy if Client[client_socket] }
321
705
  client_socket.onpong { Client[client_socket].pong }
322
706
  }
323
707
 
708
+ puts "Listening on %s:%s"%[HOST,PORT] if INFO or DEBUG
324
709
  Socket.open(:UNIX, :DGRAM) { |s| s.connect(Socket.pack_sockaddr_un(ENV['NOTIFY_SOCKET'])); s.sendmsg "READY=1" } if ENV['NOTIFY_SOCKET']
325
-
326
- EM.add_periodic_timer(10) { Client.all.each { |client| client.ping } }
327
- EM.add_periodic_timer(10) { Client.all.each { |client| client.check_ping_timeout } }
328
- EM.add_periodic_timer(10) { Client.all.each { |client| client.send_heartbeat } }
329
-
710
+
711
+ EM.add_periodic_timer(10) { Client.send_pings }
712
+ EM.add_periodic_timer(10) { Client.send_heartbeats }
713
+ EM.add_periodic_timer(10) { Client.check_ping_timeouts }
714
+
715
+ EM.add_periodic_timer(1) {
716
+ now = Time.new
717
+ puts "CPU time used: %7.3f%%"%[(processing_times_sum-$last_processing_times_sum)*100.0/(now - $last_cpu_time)] if $last_cpu_time and DEBUG
718
+ $last_cpu_time = now
719
+ $last_processing_times_sum = processing_times_sum
720
+ }
721
+
722
+
330
723
  }