seapig-server 0.0.8 → 0.1.0

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