seapig-server 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 867ee8657bbc0f1f9cdf2f8d56627179e10860f2
4
- data.tar.gz: e9102a3be76c639d1717d45c1366271cc9f97721
3
+ metadata.gz: 02ea92f8574a95efe7cbb84a4273a3f333fa0104
4
+ data.tar.gz: d7d0b375e2b6d801387c14420eebc938f15ad2ab
5
5
  SHA512:
6
- metadata.gz: eb28661eaa20bad43f3935c63b66a8ce0ba95febba2f8da5bd0742ab9d1b4e0acadd8eecdbcf6f59e31ebcefca57ed34c5558a9e74a60d88527d09bf7e82d3b8
7
- data.tar.gz: 18d635f7e0ef27f966a4d858e8ea8c4696942f4c780c27635a58ad96d754564f5bfb29c66714f560e3f1adc172971143563c184605c525608573dffe95212c80
6
+ metadata.gz: ff15f55ebfcce98672f6a820a54c254c1f4d6b5c3055fb4fd50975bec7021291d4fde0535edd1d6b84ee154725c3d315261243a543f9cc91b4c98ce4c118ff72
7
+ data.tar.gz: 17650ede8b9698155b93a1699ee61b9f597d805602c77eda65af2c0428d0e7eace0803c02b41e6af4f08292e92f3588fb9883093a1cfafa5a62c49eefe226dc9
@@ -1,4 +1,4 @@
1
- Copyright 2015 yunta
1
+ Copyright 2015-2017 yunta
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,36 +1,31 @@
1
1
  = Seapig
2
2
 
3
- Nothing here yet.
3
+ Seapig is a websocket-based master-slave json object replication/synchronisation system.
4
+
5
+ No docs exist yet.
4
6
 
5
7
  To cover:
6
8
 
7
- * what are seapigs and why they exist? (why not just having a db table?)
9
+ * what are seapigs and why they exist? (why not just having a stream?)
8
10
  * link to https://www.youtube.com/watch?v=_y4DbZivHCY
9
- * properties of seapig system
10
- * data is cached in memory
11
- * only diffs are sent
12
- * data is generated in separate processes
13
- * data gets regenerated on dependency change (immediately on db change)
11
+ * properties of a seapig system
12
+ * objects are cached in memory
13
+ * only diffs are sent over websocket when objects change
14
+ * seapig handles re-synchronization of objects on link loss
15
+ * objects are generated in separate processes
16
+ * objects are regenerated on dependency change (e.g. immediately on db change)
14
17
  * describe current limits
15
- * one level deps only
16
- * single object can have at most one generation process running at all times
17
- * cache is dropped when last client unlinks
18
- * no client-side timeout detection
19
- * postgres only (but is that really a problem? ;))
18
+ * there is no rate-limiting
19
+ * object is dropped from cache when last client un-listens
20
+ * seapig-client: no timeout detection
21
+ * seapig-rails: postgres only (but is that really a problem? ;))
20
22
  * graph of server/client side seapig object states
21
- * disclaimer on non-efficient code. it's all a draft, a test of idea.
23
+ * disclaimer on non-efficient code. it's all a draft, a test of an idea
22
24
  * rails not needed
23
25
  * stuff will change
24
- * seapig = firebase for apps that need real db
26
+ * seapig == firebase for apps that need real db
25
27
  * works nicely with mithril
26
28
  * how to use this shit
27
- * dummy app is a minimal demo
28
- * bundle exec rails s
29
- * bundle exec ruby ../../bin/seapig-server.rb
30
- * bundle exec ruby ../../bin/seapig-worker.rb ws://127.0.0.1:3001/seapig
31
- * bundle exec ruby ../../bin/seapig-notifier.rb ws://127.0.0.1:3001/seapig
32
- * application.js needs: require seapig/seapig
33
- * application.js needs: require json-patch
34
- * ActiveRecord models that are used for triggering regeneration of data need:
35
- * acts_as_seapig_dependency
36
- * and seapig_dependency_changed after commits
29
+ * write some docs
30
+ * link to example projects
31
+ * link to seapig-rails, seapig-client-ruby, seapig-router, etc.
@@ -112,23 +112,23 @@ module SeapigObjectStore
112
112
 
113
113
 
114
114
  def self.consumer_unregister(pattern_or_id, client)
115
- raise "Unregister without register" if not @@consumers[pattern_or_id].include?(client)
115
+ raise "Unregister without register" if (not @@consumers[pattern_or_id]) or (not @@consumers[pattern_or_id].include?(client))
116
116
  @@consumers[pattern_or_id].delete(client)
117
117
  @@consumers.delete(pattern_or_id) if @@consumers[pattern_or_id].size == 0
118
118
  self.matching(pattern_or_id,@@producers.merge(@@objects_by_id)).each { |matching_id|
119
119
  @@objects_by_id[matching_id].consumer_unregister(pattern_or_id, client) if @@objects_by_id[matching_id]
120
- 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])
120
+ self.despawn(@@objects_by_id[matching_id]) if @@objects_by_id[matching_id] and (not @@objects_by_id[matching_id].alive?) and (not @@dependents[matching_id])
121
121
  }
122
122
  end
123
123
 
124
124
 
125
125
  def self.producer_unregister(pattern_or_id,client)
126
- raise "Unregister without register" if not @@producers[pattern_or_id].include?(client)
126
+ raise "Unregister without register" if (not @@producers[pattern_or_id]) or (not @@producers[pattern_or_id].include?(client))
127
127
  @@producers[pattern_or_id].delete(client)
128
128
  @@producers.delete(pattern_or_id) if @@producers[pattern_or_id].size == 0
129
129
  self.matching(pattern_or_id,@@consumers.merge(@@dependents)).each { |matching_id|
130
130
  @@objects_by_id[matching_id].producer_unregister(pattern_or_id, client) if @@objects_by_id[matching_id]
131
- 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])
131
+ self.despawn(@@objects_by_id[matching_id]) if @@objects_by_id[matching_id] and (not @@objects_by_id[matching_id].alive?) and (not @@dependents[matching_id])
132
132
  }
133
133
  end
134
134
 
@@ -481,6 +481,11 @@ class Client
481
481
  end
482
482
 
483
483
 
484
+ def inspect
485
+ id
486
+ end
487
+
488
+
484
489
  def destroy
485
490
  puts "Client disconnected:\n        "+@index.to_s if DEBUG
486
491
  @@clients_by_socket.delete(@socket)
@@ -637,22 +642,6 @@ class Client
637
642
  end
638
643
 
639
644
 
640
- class InternalClient
641
-
642
- def self.produce
643
- end
644
-
645
- def initialize
646
- SeapigObjectStore.producer_register("SeapigServer::Objects", self)
647
- end
648
-
649
- def object_produce(object_id, object_version)
650
- objects =
651
- SeapigObjectStore.version_set(object_id,new_version,objects,object_version)
652
- end
653
-
654
- end
655
-
656
645
 
657
646
  #TODO:
658
647
  # * change protocol to use "pattern" instead of "id"
@@ -734,4 +723,19 @@ EM.run {
734
723
  }
735
724
 
736
725
 
726
+ close_reader, close_writer = IO.pipe
727
+
728
+ EM.watch(close_reader) { |connection|
729
+ connection.notify_readable = true
730
+ connection.define_singleton_method(:notify_readable) do
731
+ puts "Shutting down" if INFO or DEBUG
732
+ exit
733
+ end
734
+ }
735
+
736
+ Signal.trap("INT") {
737
+ puts "SIGINT received, scheduling exit." if DEBUG
738
+ close_writer.write('.')
739
+ }
740
+
737
741
  }
@@ -0,0 +1,757 @@
1
+ #!/bin/env ruby
2
+ # coding: utf-8
3
+ require 'websocket-eventmachine-server'
4
+ require 'narray'
5
+ require 'oj'
6
+ require 'jsondiff'
7
+ require 'hana'
8
+ require 'set'
9
+
10
+
11
+ DEBUG = (ARGV[0] == "debug")
12
+ INFO = (DEBUG or ARGV[0] == "info")
13
+ HOST = (ARGV[1] or "127.0.0.1").split(":")[0]
14
+ PORT = ((ARGV[1] or '').split(':')[1] or "3001").to_i
15
+
16
+ OBJECT_CACHE_SIZE = 1
17
+
18
+ $stdout.sync = true
19
+
20
+ Oj.default_options = { mode: :compat }
21
+
22
+
23
+ module WebSocket
24
+ module Frame
25
+ class Data < String
26
+ def getbytes(start_index, count)
27
+ data = self[start_index, count]
28
+ if @masking_key
29
+ payload_na = NArray.to_na(data,"byte")
30
+ mask_na = NArray.to_na((@masking_key.pack("C*")*((data.size/4) + 1))[0...data.size],"byte")
31
+ data = (mask_na ^ payload_na).to_s
32
+ end
33
+ data
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+
41
+ class String
42
+
43
+ def starexp
44
+ Regexp.new(Regexp.escape(self).gsub('\*','.*?'))
45
+ end
46
+
47
+
48
+ def starexp?
49
+ self.include?('*')
50
+ end
51
+
52
+ end
53
+
54
+
55
+ Signal.trap("USR1") {
56
+ t1 = Time.new; GC.start ;d = Time.new - t1
57
+ puts "Long GC run:\n        %.3fs"%(d) if DEBUG and d > 0.05
58
+ }
59
+
60
+
61
+ #
62
+ # Code is layered, with each layer only communicating with neighbouring layers (e.g. object store never directly talks to em or sockets).
63
+ #
64
+ # Object Store is a singleton responsible for:
65
+ # * managing objects' lifetime
66
+ # * dependency tracking and triggering object rebuilds (aka. production)
67
+ # * tracking of available producers and consumers
68
+ #
69
+ # Client class is responsible for:
70
+ # * keeping track of clients and their state
71
+ # * keeping network communication efficient (diffing)
72
+ #
73
+ # Eventmachine main loop is a:
74
+ # * router between physical world and Client class / instances
75
+ #
76
+
77
+
78
+ module SeapigObjectStore
79
+
80
+ @@objects_by_id = {} # {id => object}; stores all existing SeapigObjects
81
+
82
+ @@producers = {} # {pattern_or_id => {client}}; for assessing spawning possibility
83
+ @@consumers = {} # {pattern_or_id => {client}}; for assessing spawning need, for assessing holding need
84
+
85
+ @@dependents = {} # {id_depended_on => {id_depending}}; for assessing spawning need, for assessing holding need, for assessing reproduction need
86
+ @@dependencies = {} # {id_depending => {id_depended_on}}; for updating dependents
87
+
88
+ @@queue = [] # [object]; objects in need of production
89
+ @@producing = {} # {client => object}; for assessing client busy status
90
+ @@produced = {} # {id_being_produced => {version}}; for assessing enqueuing/dequeuing need
91
+
92
+
93
+ def self.consumer_register(pattern_or_id, client)
94
+ @@consumers[pattern_or_id] = Set.new if not @@consumers[pattern_or_id]
95
+ @@consumers[pattern_or_id].add(client)
96
+ self.matching(pattern_or_id, @@producers.merge(@@objects_by_id)).each { |matching_id|
97
+ @@objects_by_id[matching_id].consumer_register(pattern_or_id, client) if @@objects_by_id[matching_id]
98
+ self.spawn(matching_id) if not @@objects_by_id[matching_id]
99
+ }
100
+ end
101
+
102
+
103
+ def self.producer_register(pattern_or_id, client)
104
+ @@producers[pattern_or_id] = Set.new if not @@producers[pattern_or_id]
105
+ @@producers[pattern_or_id].add(client)
106
+ self.matching(pattern_or_id, @@consumers.merge(@@dependents)).each { |matching_id|
107
+ @@objects_by_id[matching_id].producer_register(pattern_or_id, client) if @@objects_by_id[matching_id]
108
+ self.spawn(matching_id) if not @@objects_by_id[matching_id]
109
+ }
110
+ self.dequeue(client,nil) if not @@producing[client]
111
+ end
112
+
113
+
114
+ def self.consumer_unregister(pattern_or_id, client)
115
+ raise "Unregister without register" if (not @@consumers[pattern_or_id]) or (not @@consumers[pattern_or_id].include?(client))
116
+ @@consumers[pattern_or_id].delete(client)
117
+ @@consumers.delete(pattern_or_id) if @@consumers[pattern_or_id].size == 0
118
+ self.matching(pattern_or_id,@@producers.merge(@@objects_by_id)).each { |matching_id|
119
+ @@objects_by_id[matching_id].consumer_unregister(pattern_or_id, client) if @@objects_by_id[matching_id]
120
+ self.despawn(@@objects_by_id[matching_id]) if @@objects_by_id[matching_id] and (not @@objects_by_id[matching_id].alive?) and (not @@dependents[matching_id])
121
+ }
122
+ end
123
+
124
+
125
+ def self.producer_unregister(pattern_or_id,client)
126
+ raise "Unregister without register" if (not @@producers[pattern_or_id]) or (not @@producers[pattern_or_id].include?(client))
127
+ @@producers[pattern_or_id].delete(client)
128
+ @@producers.delete(pattern_or_id) if @@producers[pattern_or_id].size == 0
129
+ self.matching(pattern_or_id,@@consumers.merge(@@dependents)).each { |matching_id|
130
+ @@objects_by_id[matching_id].producer_unregister(pattern_or_id, client) if @@objects_by_id[matching_id]
131
+ self.despawn(@@objects_by_id[matching_id]) if @@objects_by_id[matching_id] and (not @@objects_by_id[matching_id].alive?) and (not @@dependents[matching_id])
132
+ }
133
+ end
134
+
135
+
136
+ def self.version_get(client,id,version)
137
+ raise "version_get called on starexp, that doesn't make sense" if id.starexp?
138
+ return [0,{}] if not @@objects_by_id.has_key?(id)
139
+ @@objects_by_id[id].version_get(version)
140
+ end
141
+
142
+
143
+ # data can be one of:
144
+ # - Hash => given version corresponds to given data
145
+ # - false => given version has no data (aka. stall)
146
+ # - true => given version exists (data unknown)
147
+ # - nil => given version could not be generated (data unknown)
148
+ def self.version_set(client,id,version,data,requested_version)
149
+ raise "Update of pattern doesn't make sense" if id.starexp?
150
+
151
+ if requested_version != false
152
+ raise "client not in @@producing" if not @@producing[client]
153
+ raise "requested_version (%s) not in @@produced[id] (%s)"%[requested_version.inspect,@@produced[id].inspect] if not @@produced[id].include?(requested_version)
154
+ @@producing.delete(client)
155
+ @@produced[id].delete(requested_version) # also on disconnection / unproducer / test
156
+ @@produced.delete(id) if @@produced[id].size == 0
157
+ end
158
+
159
+ if @@objects_by_id.has_key?(id) or @@dependents[id] or @@consumers.keys.find { |pattern| id =~ pattern.starexp }
160
+ object = (@@objects_by_id[id] or self.spawn(id))
161
+ accepted = object.version_set(data, version, requested_version)
162
+ if accepted
163
+ puts "Version accepted" if DEBUG
164
+ (@@dependents[id] or Set.new).each { |dependent_id|
165
+ raise if not @@objects_by_id.has_key?(dependent_id)
166
+ next if not (dependent = @@objects_by_id[dependent_id])
167
+ 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
168
+ dependent.version_needed[id] = version
169
+ enqueue(dependent)
170
+ end
171
+ }
172
+ if version.kind_of? Hash
173
+ object.version_needed = {} if (not object.version_needed) or object.version_needed.kind_of?(Integer)
174
+ old_dependencies = (@@dependencies[id] or Set.new)
175
+ new_dependencies = (@@dependencies[id] = Set.new(version.keys))
176
+ (new_dependencies - old_dependencies).each { |added_dependency|
177
+ object.version_needed[added_dependency] = SeapigObject.version_newer((@@objects_by_id[added_dependency] ? @@objects_by_id[added_dependency].version_latest : 0), (version[added_dependency] or 0))
178
+ dependent_add(added_dependency, object.id)
179
+ }
180
+ (old_dependencies & new_dependencies).each { |kept_dependency|
181
+ object.version_needed[kept_dependency] = SeapigObject.version_newer((@@objects_by_id[kept_dependency] ? @@objects_by_id[kept_dependency].version_latest : 0), (version[kept_dependency] or 0))
182
+ }
183
+ (old_dependencies - new_dependencies).each { |removed_dependency|
184
+ object.version_needed.delete(removed_dependency)
185
+ dependent_remove(removed_dependency, object.id)
186
+ }
187
+ else
188
+ object.version_needed = version
189
+ end
190
+ end
191
+ enqueue(object)
192
+ end
193
+
194
+ dequeue(client,nil) if requested_version != false and not @@producing[client]
195
+ end
196
+
197
+
198
+ def self.cache_get(object_id, key)
199
+ return nil if not @@objects_by_id.has_key?(object_id)
200
+ @@objects_by_id[object_id].cache_get(key)
201
+ end
202
+
203
+
204
+ def self.cache_set(object_id, key, value)
205
+ return value if not @@objects_by_id.has_key?(object_id)
206
+ @@objects_by_id[object_id].cache_set(key, value)
207
+ value
208
+ end
209
+
210
+
211
+ private
212
+
213
+
214
+ class SeapigObject
215
+
216
+ attr_reader :id, :versions, :direct_producers, :wildcard_producers
217
+ attr_accessor :version_needed
218
+
219
+ def initialize(id)
220
+ @id = id
221
+ @versions = [ [0, {}] ]
222
+ @direct_consumers = Set.new
223
+ @wildcard_consumers = {}
224
+ @direct_producers = Set.new
225
+ @wildcard_producers = {}
226
+ @version_needed = nil
227
+ @cache = []
228
+ end
229
+
230
+
231
+ def destroy
232
+ @wildcard_consumers.keys.each { |client|
233
+ client.object_destroy(@id)
234
+ }
235
+ end
236
+
237
+
238
+ def version_get(object_version)
239
+ @versions.assoc(object_version) or [0,{}]
240
+ end
241
+
242
+
243
+ def version_set(data,version,requested_version)
244
+ return false if data == nil
245
+ return false if not SeapigObject.version_newer?(version_latest, version)
246
+ @version_needed = version if data == true and ((not @version_needed) or SeapigObject.version_newer?(@version_needed, version))
247
+ return false if data == true
248
+ @versions << [version,data]
249
+ (Set.new(@wildcard_consumers.keys)+@direct_consumers).each { |client| client.object_update(@id, version, data) } if data
250
+ versions_with_valid_data = 0
251
+ discard_below = @versions.size - 1
252
+ while discard_below > 0 and versions_with_valid_data < 1
253
+ versions_with_valid_data += 1 if @versions[discard_below][1]
254
+ discard_below -= 1
255
+ end
256
+ discard_below.times { @versions.shift }
257
+ true
258
+ end
259
+
260
+
261
+ def version_latest
262
+ return nil if not @versions[-1]
263
+ @versions[-1][0]
264
+ end
265
+
266
+
267
+ def self.version_newer?(latest,vb)
268
+ # return true if latest.nil? and (not vb.nil?)
269
+ # return false if (not latest.nil?) and vb.nil?
270
+ return latest < vb if (not latest.kind_of?(Hash)) and (not vb.kind_of?(Hash))
271
+ return true if (not latest.kind_of?(Hash)) and ( vb.kind_of?(Hash))
272
+ return false if ( latest.kind_of?(Hash)) and (not vb.kind_of?(Hash))
273
+ (latest.keys & vb.keys).each { |key|
274
+ return true if version_newer?(latest[key], vb[key])
275
+ }
276
+ return vb.size < latest.size #THINK: is this the right way to go...
277
+ end
278
+
279
+
280
+ def self.version_newer(va,vb)
281
+ version_newer?(va,vb) ? vb : va
282
+ end
283
+
284
+
285
+ def consumer_register(pattern,client)
286
+ return false if ((not pattern.starexp?) and @direct_consumers.include?(client)) or (pattern.starexp? and @wildcard_consumers[client] and @wildcard_consumers[client].include?(pattern))
287
+ if pattern.starexp?
288
+ (@wildcard_consumers[client] ||= Set.new).add(pattern)
289
+ else
290
+ @direct_consumers.add(client)
291
+ end
292
+ latest_known_version, latest_known_data = @versions.reverse.find { |version,data| data }
293
+ (Set.new(@wildcard_consumers.keys)+@direct_consumers).each { |client| client.object_update(@id, latest_known_version, latest_known_data) }
294
+ end
295
+
296
+
297
+ def producer_register(pattern,client)
298
+ return false if ((not pattern.starexp?) and @direct_producers.include?(client)) or (pattern.starexp? and @wildcard_producers[client] and @wildcard_producers[client].include?(pattern))
299
+ if pattern.starexp?
300
+ (@wildcard_producers[client] ||= Set.new).add(pattern)
301
+ else
302
+ @direct_producers.add(client)
303
+ end
304
+ end
305
+
306
+
307
+ def consumer_unregister(pattern,client)
308
+ raise "Unregister without register" if (not @direct_consumers.include?(client)) and ((not @wildcard_consumers.has_key?(client)) or (not @wildcard_consumers[client].include?(pattern)))
309
+ if pattern.starexp?
310
+ @wildcard_consumers[client].delete(pattern)
311
+ @wildcard_consumers.delete(client) if @wildcard_consumers[client].size == 0
312
+ else
313
+ @direct_consumers.delete(client)
314
+ end
315
+ end
316
+
317
+
318
+ def producer_unregister(pattern,client)
319
+ raise "Unregister without register" if (not @direct_producers.include?(client)) and ((not @wildcard_producers.has_key?(client)) or (not @wildcard_producers[client].include?(pattern)))
320
+ if pattern.starexp?
321
+ @wildcard_producers[client].delete(pattern)
322
+ @wildcard_producers.delete(client) if @wildcard_producers[client].size == 0
323
+ else
324
+ @direct_producers.delete(client)
325
+ end
326
+ end
327
+
328
+
329
+ def cache_get(key)
330
+ ret = @cache.assoc(key)
331
+ puts "Cache "+(ret ? "hit" : "miss") if DEBUG
332
+ ret and ret[1]
333
+ end
334
+
335
+
336
+ def cache_set(key, value)
337
+ @cache.delete(old_entry) if old_entry = @cache.assoc(key)
338
+ @cache << [key,value] if OBJECT_CACHE_SIZE > 0
339
+ @cache = @cache[-OBJECT_CACHE_SIZE..-1] if @cache.size > OBJECT_CACHE_SIZE
340
+ end
341
+
342
+
343
+ def alive?
344
+ (@direct_consumers.size > 0 or (@wildcard_consumers.size > 0 and @direct_producers.size > 0))
345
+ end
346
+
347
+
348
+ def inspect
349
+ '<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]
350
+ end
351
+
352
+ end
353
+
354
+
355
+ def self.matching(pattern,check_against)
356
+ if pattern.starexp?
357
+ check_against.each_key.map { |id|
358
+ id if (not id.starexp?) and (id =~ pattern.starexp)
359
+ }.compact
360
+ else
361
+ (check_against.each_key.find { |id|
362
+ (id.starexp? and pattern =~ id.starexp) or ((not id.starexp?) and pattern == id)
363
+ }) ? [pattern] : []
364
+ end
365
+ end
366
+
367
+
368
+ def self.spawn(id)
369
+ puts "Creating:\n        "+id if DEBUG
370
+ @@objects_by_id[id] = object = SeapigObject.new(id)
371
+ @@producers.each_pair.map { |pattern,clients| clients.each { |client| object.producer_register(pattern,client) if pattern.starexp? and (id =~ pattern.starexp) or (id == pattern) } }
372
+ @@consumers.each_pair.map { |pattern,clients| clients.each { |client| object.consumer_register(pattern,client) if pattern.starexp? and (id =~ pattern.starexp) or (id == pattern) } }
373
+ enqueue(object)
374
+ object
375
+ end
376
+
377
+
378
+ def self.despawn(object)
379
+ puts "Deleting:\n        "+object.id if DEBUG
380
+ raise "Despawning object that should stay alive" if object.alive? or @@dependents[object.id]
381
+ object.destroy
382
+ (@@dependencies.delete(object.id) or []).each { |dependency_id|
383
+ dependent_remove(dependency_id, object.id)
384
+ }
385
+ @@objects_by_id.delete(object.id)
386
+ end
387
+
388
+
389
+ def self.enqueue(object)
390
+ if object.version_needed and object.version_latest == object.version_needed
391
+ @@queue.delete(object)
392
+ else
393
+ return if @@queue.include?(object) or (@@produced[object.id] and @@produced[object.id].include?(object.version_needed))
394
+ @@queue << object
395
+ (Set.new(object.direct_producers) + object.wildcard_producers.keys).find { |client|
396
+ dequeue(client, object) if not @@producing[client]
397
+ }
398
+ end
399
+ end
400
+
401
+
402
+ def self.dequeue(client,object)
403
+ object = @@queue.find { |candidate_object| candidate_object.direct_producers.include?(client) or candidate_object.wildcard_producers.has_key?(client) } if not object
404
+ return false if not @@queue.include?(object)
405
+ version_snapshot = (object.version_needed == nil ? nil : (object.version_needed.kind_of?(Fixnum) ? object.version_needed : object.version_needed.clone))
406
+ client.object_produce(object.id, version_snapshot)
407
+ @@queue.delete(object)
408
+ @@producing[client] = object
409
+ (@@produced[object.id] ||= Set.new) << version_snapshot
410
+ end
411
+
412
+
413
+ def self.dependent_add(id, dependent)
414
+ @@dependents[id] = Set.new if not @@dependents[id]
415
+ @@dependents[id] << dependent
416
+ self.matching(id, @@producers).each { |matching_id|
417
+ self.spawn(matching_id) if not @@objects_by_id[matching_id]
418
+ }
419
+ end
420
+
421
+
422
+ def self.dependent_remove(id, dependent)
423
+ @@dependents[id].delete(dependent)
424
+ @@dependents.delete(id) if @@dependents[id].size == 0
425
+ self.despawn(@@objects_by_id[id]) if @@objects_by_id.include?(id) and (not @@objects_by_id[id].alive?) and (not @@dependents[id])
426
+ end
427
+
428
+
429
+ def self.pp
430
+ [
431
+ "Objects:", @@objects_by_id.values.map { |object| "        %s"%[object.inspect] }.join("\n"),
432
+ "Queue:", @@queue.map { |object| "        %s"%[object.inspect] }.join("\n"),
433
+ "Producing:", @@producing.map { |client,object| "        %s - %s"%[client.id,object.id] }.join("\n"),
434
+ "Produced:", @@produced.map { |object,versions| "        %s - %s"%[object,versions.inspect] }.join("\n")
435
+ ].select { |str| str.size > 0 }.join("\n")+"\n"
436
+ end
437
+
438
+
439
+
440
+
441
+
442
+
443
+ end
444
+
445
+
446
+
447
+ #TODO:
448
+ # * Refactor to have ClientSpace class/module with Clients inside
449
+
450
+
451
+ class Client
452
+
453
+ attr_reader :produces, :consumes, :socket, :producing, :index, :pong_time
454
+ attr_accessor :options
455
+
456
+ @@clients_by_socket = {}
457
+ @@count = 0
458
+
459
+
460
+ def self.[](socket)
461
+ @@clients_by_socket[socket]
462
+ end
463
+
464
+
465
+ def initialize(socket)
466
+ @index = @@count += 1
467
+ puts "Client connected:\n        "+@index.to_s if DEBUG
468
+ @socket = socket
469
+ @options = {}
470
+ @produces = Set.new
471
+ @consumes = Set.new
472
+ @versions = {}
473
+ @producing = nil
474
+ @@clients_by_socket[socket] = self
475
+ self.pong
476
+ end
477
+
478
+
479
+ def id
480
+ (@options['name'] or "") + ':' + @index.to_s
481
+ end
482
+
483
+
484
+ def inspect
485
+ id
486
+ end
487
+
488
+
489
+ def destroy
490
+ puts "Client disconnected:\n        "+@index.to_s if DEBUG
491
+ @@clients_by_socket.delete(@socket)
492
+ @produces.each { |pattern| SeapigObjectStore.producer_unregister(pattern,self) }
493
+ @consumes.each { |pattern| SeapigObjectStore.consumer_unregister(pattern,self) }
494
+ producing = @producing
495
+ @producing = nil
496
+ SeapigObjectStore.version_set(self,producing[0],nil,nil,producing[1]) if producing
497
+ end
498
+
499
+
500
+ def producer_register(pattern, known_version)
501
+ @produces.add(pattern)
502
+ SeapigObjectStore.producer_register(pattern, self)
503
+ SeapigObjectStore.version_set(self, pattern, known_version, true, false) if known_version and not pattern.starexp?
504
+ end
505
+
506
+
507
+ def producer_unregister(pattern)
508
+ @produces.delete(pattern)
509
+ SeapigObjectStore.producer_unregister(pattern, self)
510
+ if @producing and (pattern.starexp? ? (@producing[0] =~ pattern.starexp) : (@producing[0] == pattern)) #NOTE: overlaping production patterns are not supported
511
+ producing = @producing
512
+ @producing = nil
513
+ SeapigObjectStore.version_set(self,producing[0],nil,nil,producing[1])
514
+ end
515
+ end
516
+
517
+
518
+ def consumer_register(pattern, known_version)
519
+ @consumes.add(pattern)
520
+ @versions[pattern] = known_version if not pattern.starexp?
521
+ SeapigObjectStore.consumer_register(pattern, self)
522
+ gc_versions
523
+ end
524
+
525
+
526
+ def consumer_unregister(pattern)
527
+ @consumes.delete(pattern)
528
+ SeapigObjectStore.consumer_unregister(pattern, self)
529
+ gc_versions
530
+ end
531
+
532
+
533
+ def gc_versions
534
+ @versions.keys.each { |object_id|
535
+ @versions.delete(object_id) if not (@consumes).find { |pattern|
536
+ pattern.starexp? and (object_id =~ pattern.starexp) or (pattern == object_id)
537
+ }
538
+ }
539
+ end
540
+
541
+
542
+ def object_update(object_id, object_version, object_data)
543
+ #THINK: should we propagate stalls to clients?
544
+ return if object_version == 0 or object_version == @versions[object_id]
545
+ old_version, old_data = SeapigObjectStore.version_get(self,object_id,(@versions[object_id] or 0))
546
+ data = if old_version == 0
547
+ { "value" => object_data }
548
+ else
549
+ diff = SeapigObjectStore.cache_get(object_id,[:diff,old_version,object_version])
550
+ diff = SeapigObjectStore.cache_set(object_id,[:diff,old_version,object_version],JsonDiff.generate(old_data, object_data)) if not diff
551
+ { "patch" => diff }
552
+ end
553
+
554
+ json = Oj.dump({
555
+ "action" => 'object-update',
556
+ "id" => object_id,
557
+ "old_version" => old_version,
558
+ "new_version" => object_version,
559
+ }.merge(data))
560
+ puts "Sending:\n        %8iB %s to %s"%[json.size, object_id, id] if DEBUG
561
+ @versions[object_id] = object_version
562
+ @socket.send json
563
+ end
564
+
565
+
566
+ def object_destroy(object_id)
567
+ @socket.send Oj.dump("action" => 'object-destroy', "id" => object_id)
568
+ end
569
+
570
+
571
+ def object_patch(object_id, patch, value, from_version, to_version)
572
+ raise "patching wildcard object. no." if object_id.starexp?
573
+ requested_object_id, requested_version = @producing
574
+ if requested_object_id == object_id
575
+ @producing = nil
576
+ else
577
+ requested_version = false
578
+ end
579
+ new_version = to_version
580
+
581
+ new_data = if patch
582
+ object_version, object_data = SeapigObjectStore.version_get(self,object_id,from_version)
583
+ 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
584
+ if from_version == object_version
585
+ puts 'clean' if DEBUG
586
+ new_data = Oj.load(Oj.dump(object_data))
587
+ begin
588
+ Hana::Patch.new(patch).apply(new_data) if patch
589
+ rescue Exception => e
590
+ puts "Patching failed!\n        Old object: "+object_data.inspect+"\n        Patch: "+patch.inspect if DEBUG
591
+ raise e
592
+ end
593
+ new_data
594
+ else
595
+ puts "can't update object, couldn't find base version" if DEBUG
596
+ nil
597
+ end
598
+ elsif value != nil
599
+ 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
600
+ value
601
+ else
602
+ nil
603
+ end
604
+
605
+ SeapigObjectStore.version_set(self,object_id,new_version,new_data,requested_version)
606
+ end
607
+
608
+
609
+ def object_produce(object_id, object_version)
610
+ raise "Can't produce a wildcard object" if object_id.starexp?
611
+ raise "Client already producing something (producing: %s, trying to assign: %s)"%[@producing.inspect, [object_id,object_version].inspect] if @producing
612
+ raise "Can't produce that pattern: "+@produces.inspect+" "+object_id.inspect if not @produces.find { |pattern| object_id =~ pattern.starexp }
613
+ puts "Assigning:\n        "+object_id+':'+object_version.inspect+' to: '+self.id if DEBUG
614
+ @socket.send Oj.dump("action" => 'object-produce', "id" => object_id, "version"=>object_version)
615
+ @producing = [object_id, object_version]
616
+ end
617
+
618
+
619
+ def pong
620
+ @pong_time = Time.new
621
+ end
622
+
623
+
624
+ def self.send_pings
625
+ @@clients_by_socket.keys.each { |socket| socket.ping }
626
+ end
627
+
628
+
629
+ def self.send_heartbeats
630
+ @@clients_by_socket.each_pair { |socket,client| socket.send Oj.dump('action' => 'heartbeat') if client.options['heartbeat'] }
631
+ end
632
+
633
+
634
+ def self.check_ping_timeouts
635
+ @@clients_by_socket.each_pair { |socket,client| socket.close if Time.new - client.pong_time > 60 }
636
+ end
637
+
638
+
639
+ def self.pp
640
+ "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"
641
+ end
642
+ end
643
+
644
+
645
+ class InternalClient
646
+
647
+ def self.produce
648
+ end
649
+
650
+ def initialize
651
+ SeapigObjectStore.producer_register("SeapigServer::Objects", self)
652
+ end
653
+
654
+ def object_produce(object_id, object_version)
655
+ objects =
656
+ SeapigObjectStore.version_set(object_id,new_version,objects,object_version)
657
+ end
658
+
659
+ end
660
+
661
+
662
+ #TODO:
663
+ # * change protocol to use "pattern" instead of "id"
664
+ # * change "object-patch" to something nicer
665
+
666
+ processing_times = []
667
+ processing_times_sum = 0
668
+
669
+ EM.run {
670
+
671
+
672
+ WebSocket::EventMachine::Server.start(host: HOST, port: PORT) { |client_socket|
673
+
674
+ client_socket.onmessage { |message|
675
+ begin
676
+ started_at = Time.new
677
+ client = Client[client_socket]
678
+ message = Oj.load message
679
+ puts "-"*80 + ' ' + Time.new.to_s if DEBUG
680
+ 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
681
+ puts if DEBUG
682
+ object_id = message['id'] if message['id']
683
+ case message['action']
684
+ when 'object-producer-register'
685
+ fail unless message['pattern']
686
+ client.producer_register(message['pattern'],message['known-version'])
687
+ when 'object-producer-unregister'
688
+ fail unless message['pattern']
689
+ client.producer_unregister(message['pattern'])
690
+ when 'object-patch'
691
+ fail unless message['id']
692
+ client.object_patch(object_id,message['patch'], message['value'], message['old_version'], message['new_version'])
693
+ when 'object-consumer-register'
694
+ fail unless message['id']
695
+ client.consumer_register(object_id,message['known-version'])
696
+ when 'object-consumer-unregister'
697
+ fail unless message['id']
698
+ client.consumer_unregister(object_id)
699
+ when 'client-options-set'
700
+ fail unless message['options']
701
+ client.options = message['options']
702
+ else
703
+ raise 'WTF, got message with action: ' + message['action'].inspect
704
+ end
705
+ processing_times << (Time.new.to_f - started_at.to_f)
706
+ processing_times_sum += processing_times[-1]
707
+ if DEBUG
708
+ puts Client.pp
709
+ puts SeapigObjectStore.pp
710
+ 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]
711
+ end
712
+ 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
713
+ rescue => e
714
+ puts "Message processing error:\n        "
715
+ p e
716
+ e.backtrace.each { |line| puts line }
717
+ raise
718
+ end
719
+ }
720
+
721
+
722
+ client_socket.onopen { Client.new(client_socket) }
723
+ client_socket.onclose { Client[client_socket].destroy if Client[client_socket] }
724
+ client_socket.onpong { Client[client_socket].pong }
725
+ }
726
+
727
+ puts "Listening on %s:%s"%[HOST,PORT] if INFO or DEBUG
728
+ Socket.open(:UNIX, :DGRAM) { |s| s.connect(Socket.pack_sockaddr_un(ENV['NOTIFY_SOCKET'])); s.sendmsg "READY=1" } if ENV['NOTIFY_SOCKET']
729
+
730
+ EM.add_periodic_timer(10) { Client.send_pings }
731
+ EM.add_periodic_timer(10) { Client.send_heartbeats }
732
+ EM.add_periodic_timer(10) { Client.check_ping_timeouts }
733
+
734
+ EM.add_periodic_timer(1) {
735
+ now = Time.new
736
+ 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
737
+ $last_cpu_time = now
738
+ $last_processing_times_sum = processing_times_sum
739
+ }
740
+
741
+
742
+ close_reader, close_writer = IO.pipe
743
+
744
+ EM.watch(close_reader) { |connection|
745
+ connection.notify_readable = true
746
+ connection.define_singleton_method(:notify_readable) do
747
+ puts "Shutting down" if INFO or DEBUG
748
+ exit
749
+ end
750
+ }
751
+
752
+ Signal.trap("INT") {
753
+ puts "SIGINT received, scheduling exit." if DEBUG
754
+ close_writer.write('.')
755
+ }
756
+
757
+ }
@@ -1,3 +1,3 @@
1
1
  module Seapig
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seapig-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - yunta
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-06 00:00:00.000000000 Z
11
+ date: 2017-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket-eventmachine-server
@@ -92,6 +92,7 @@ files:
92
92
  - README.rdoc
93
93
  - Rakefile
94
94
  - bin/seapig-server
95
+ - bin/seapig-server-intro
95
96
  - lib/seapig-server.rb
96
97
  - lib/seapig/version.rb
97
98
  - test/dummy/log/development.log