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.
- checksums.yaml +4 -4
- data/bin/seapig-server +582 -189
- data/lib/seapig/version.rb +1 -1
- metadata +71 -123
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -15
- data/test/dummy/app/assets/javascripts/json-patch.js +0 -392
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -9
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/application/index.html.slim +0 -10
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -29
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -26
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/database.yml +0 -85
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -79
- data/test/dummy/config/environments/test.rb +0 -42
- data/test/dummy/config/initializers/assets.rb +0 -11
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/config/secrets.yml +0 -22
- data/test/dummy/lib/seapigs/random.rb +0 -14
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/navigation_test.rb +0 -8
- data/test/seapig_test.rb +0 -7
- data/test/test_helper.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c92d464df0fe01cbc76a752b14954adb3382f2c7
|
4
|
+
data.tar.gz: 5e3523365c94ab376bbe71f45eddff0dccafd519
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d61241a434eea5aa514014603e9ee54fecb35486e0eeae099479fdd0924fdfbd2875d2b878a0d77c40355c34663cabe0cbd27301ca1d77e3347d1319958b1ea2
|
7
|
+
data.tar.gz: a270181bcef678e4742cb03074dc629fe6509e98602b93b7dac0c6ca5201d2c22e5b1c90da3683dc1f5a88c6a48db67da3081e6e66aa19eb4db75eb013c5120a
|
data/bin/seapig-server
CHANGED
@@ -2,13 +2,41 @@
|
|
2
2
|
# coding: utf-8
|
3
3
|
|
4
4
|
require 'websocket-eventmachine-server'
|
5
|
-
require '
|
6
|
-
require '
|
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
|
-
|
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
|
-
|
41
|
-
|
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.
|
46
|
-
@@
|
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.
|
51
|
-
|
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
|
56
|
-
|
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
|
61
|
-
|
62
|
-
|
63
|
-
|
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.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@@objects_by_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
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
126
|
-
|
127
|
-
return false if not
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
149
|
-
|
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, :
|
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.
|
171
|
-
@@clients_by_socket
|
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
|
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
|
473
|
+
puts "Client disconnected:\n "+@index.to_s if DEBUG
|
195
474
|
@@clients_by_socket.delete(@socket)
|
196
|
-
|
197
|
-
|
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.
|
203
|
-
|
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
|
208
|
-
@consumes.
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
218
|
-
|
219
|
-
@versions
|
220
|
-
|
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
|
225
|
-
|
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
|
238
|
-
|
239
|
-
|
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
|
244
|
-
|
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
|
254
|
-
|
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
|
-
|
259
|
-
|
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:
|
272
|
-
|
653
|
+
WebSocket::EventMachine::Server.start(host: HOST, port: PORT) { |client_socket|
|
654
|
+
|
273
655
|
client_socket.onmessage { |message|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
puts "
|
312
|
-
|
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.
|
327
|
-
EM.add_periodic_timer(10) { Client.
|
328
|
-
EM.add_periodic_timer(10) { Client.
|
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
|
}
|