qwe 0.0.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.
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qwe::DB
4
+ class Record
5
+ include DRb::DRbUndumped
6
+
7
+ def _dump(...)
8
+ ""
9
+ end
10
+
11
+ def self._load(...)
12
+ nil
13
+ end
14
+
15
+ def detach_at
16
+ @detach_at || keep
17
+ end
18
+
19
+ def keep
20
+ @detach_at = Time.now + Worker.instance.detach_timeout
21
+ end
22
+
23
+ def should_detach?
24
+ detach_at < Time.now
25
+ end
26
+
27
+ def object=(obj)
28
+ raise "Object for record #{id} is already set" if object
29
+ @object = obj
30
+ end
31
+
32
+ def id=(id)
33
+ raise "Object for record #{id} is already set" if self.id
34
+ @id = id
35
+ end
36
+
37
+ def commit(str)
38
+ @meta["commits_length"] += str.count("\n")
39
+ commits_file.write(str)
40
+ unless str[-1] == "\n"
41
+ commits_file.write("\n")
42
+ @meta["commits_length"] += 1
43
+ end
44
+ self
45
+ rescue => e
46
+ log "Can't write commit #{str}: #{e.full_message}"
47
+ self
48
+ end
49
+
50
+ def commits_length=(v)
51
+ @meta["commits_length"] = v
52
+ end
53
+
54
+ def commits_length
55
+ @meta["commits_length"]
56
+ end
57
+
58
+ def meta_file
59
+ File.join(@dir, "meta")
60
+ end
61
+
62
+ attr_reader :dir, :object, :id, :meta
63
+
64
+ def commits_file
65
+ @commits_file ||= CommitsFile.new(@dir)
66
+ end
67
+
68
+ def dump
69
+ Marshal.dump(object)
70
+ end
71
+
72
+ def dump_object
73
+ File.binwrite(File.join(dir, commits_length.to_s), dump)
74
+ end
75
+
76
+ def create_object(klass)
77
+ self.object = klass.new
78
+ object.init if klass.include?(Qwe::Mixins::Root)
79
+ dump_object
80
+ end
81
+
82
+ def initialize(id, server_dir, create: nil, **args)
83
+ @dir = File.join(server_dir, "records", id.to_s)
84
+ self.id = id
85
+
86
+ if create
87
+ Qwe::DB::Server.mkdir(dir)
88
+ @meta = args
89
+ @meta["commits_length"] = 0
90
+ File.write(meta_file, JSON.generate(args))
91
+ if create.is_a?(Class)
92
+ create_object(create)
93
+ elsif create.is_a?(Symbol)
94
+ create_object(Object.module_eval(create.to_s))
95
+ elsif create.is_a?(String)
96
+ self.object = Marshal.load(create)
97
+ File.binwrite(File.join(dir, "0"), create)
98
+ else
99
+ self.object = create
100
+ dump_object
101
+ end
102
+ else
103
+ @meta = JSON.parse(File.read(meta_file))
104
+ self.object = Marshal.load(File.binread(File.join(dir, "dump")))
105
+ end
106
+
107
+ if object.respond_to?(:record=)
108
+ object.record = self
109
+ end
110
+ end
111
+
112
+ def save
113
+ save!
114
+ rescue => e
115
+ log "Can't save record #{id}: #{e.full_message} #{e.backtrace}"
116
+ end
117
+
118
+ def save!
119
+ commits_file.close
120
+ File.binwrite(File.join(dir, "dump"), dump)
121
+ File.write(meta_file, JSON.generate(@meta))
122
+ end
123
+
124
+ def commits(commit_id = nil)
125
+ commits_file.read(commit_id)
126
+ end
127
+
128
+ def object_at(commit_id)
129
+ o = Marshal.load File.read(File.join(dir, "0"))
130
+ o.instance_eval commits(commit_id)
131
+ o
132
+ end
133
+
134
+ def detach
135
+ Worker.instance.detach(id, self)
136
+ end
137
+
138
+ def fork(commit_id = nil)
139
+ log "Fork at commit id #{commit_id}"
140
+ commit_id ||= commits_length
141
+
142
+ zero = File.binread(File.join(dir, "0"))
143
+ rec = Worker.instance.allocate_record(zero)
144
+
145
+ commits = commits(commit_id)
146
+ rec.commits_length = commit_id + 1
147
+
148
+ begin
149
+ rec.object.instance_eval(commits)
150
+ rescue ScriptError, StandardError => e
151
+ log e.full_message
152
+ log "Commits are:\n\n#{commits}\n\n"
153
+ end
154
+
155
+ if rec.commits.length > 0
156
+ log "Commits should not produce another commits:", "\n" + rec.commits
157
+ raise "Commits loop detected"
158
+ end
159
+
160
+ rec.commits_file.write(commits)
161
+ rec
162
+ end
163
+
164
+ def obj_eval(rb)
165
+ object.instance_eval(rb)
166
+ end
167
+
168
+ def archive
169
+ archive!
170
+ rescue => e
171
+ log "Error archiving #{id}: #{e.full_message}"
172
+ end
173
+
174
+ def archive!
175
+ log "Archive record #{id}"
176
+ commits_file.archive!
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Qwe::DB
6
+ class Server
7
+ include DRb::DRbUndumped
8
+ include Qwe::Mixins::Process
9
+
10
+ DEFAULT_PORT = 3228
11
+ DEFAULT_HOST = "druby://localhost"
12
+
13
+ attr_reader :uri, :dir, :host, :port, :records_length, :threads_count
14
+
15
+ def self.default_uri
16
+ "#{DEFAULT_HOST}:#{DEFAULT_PORT}"
17
+ end
18
+
19
+ def initialize(
20
+ dir: "#{Dir.pwd}/qwe_db",
21
+ host: DEFAULT_HOST,
22
+ port: DEFAULT_PORT,
23
+ require_file: nil,
24
+ no_jit: false,
25
+ threads: Etc.nprocessors,
26
+ gc_interval: 0,
27
+ detach_timeout: 300
28
+ )
29
+ @port = port
30
+ @host = host
31
+ @dir = dir
32
+ @uri = "#{@host}:#{@port}"
33
+ @threads_count = threads
34
+
35
+ @worker_args = [RbConfig.ruby]
36
+ @worker_args.push "--jit" unless no_jit
37
+ @worker_args += ["#{__dir__}/../../spawn_worker.rb", uri, dir, gc_interval.to_s, detach_timeout.to_s]
38
+ @worker_args.push require_file if require_file
39
+
40
+ @workers = {}
41
+ @records = {}
42
+
43
+ log "Initialized server in #{dir}"
44
+ end
45
+
46
+ def start
47
+ self.class.mkdir(dir)
48
+ d = File.join(dir, "records")
49
+ self.class.mkdir(d)
50
+ @records_length = (Dir.new(d).children.map(&:to_i).max || 0) + 1
51
+ @drb = DRb.start_service(uri, self)
52
+
53
+ trap_stop_signals
54
+
55
+ log "Started server at #{@drb.uri}, records_length = #{records_length}, pid = #{Process.pid}"
56
+
57
+ @threads_count.times do
58
+ spawn_worker(wait: false)
59
+ end
60
+
61
+ @ready = true
62
+
63
+ @drb.thread.join
64
+ end
65
+
66
+ def ready?
67
+ @ready || false
68
+ end
69
+
70
+ def [](id)
71
+ id = id.to_i
72
+ w = @records[id]
73
+ if w
74
+ if w.is_a?(Thread)
75
+ w.value
76
+ else
77
+ w[id]
78
+ end
79
+ else
80
+ load(id)
81
+ end
82
+ end
83
+
84
+ def workers_length
85
+ @workers.length
86
+ end
87
+
88
+ def self.mkdir(d)
89
+ Dir.mkdir(d) unless Dir.exist?(d)
90
+ end
91
+
92
+ def records_length=(len)
93
+ @records_length = len
94
+ File.write(rl_file, len)
95
+ end
96
+
97
+ def stop
98
+ return if @stopping
99
+ @stopping = true
100
+ log "Stopping"
101
+ @workers.each do |pid, worker|
102
+ log "Terminate worker #{pid}"
103
+ Process.kill("TERM", pid)
104
+ rescue Errno::ESRCH
105
+ log "Worker #{pid} is already dead"
106
+ rescue => e
107
+ log "Error terminating worker #{pid}: #{e.full_message}"
108
+ end
109
+ exit
110
+ end
111
+
112
+ SPAWN_WORKER_TIMEOUT = 50
113
+
114
+ def spawn_worker(wait: true)
115
+ pid = spawn(*@worker_args)
116
+ Process.detach(pid)
117
+
118
+ @workers[pid] = Thread.new do
119
+ sleep SPAWN_WORKER_TIMEOUT
120
+ raise "Worker did not start after #{SPAWN_WORKER_TIMEOUT}s timeout"
121
+ end
122
+ @workers[pid].join if wait
123
+ pid
124
+ end
125
+
126
+ def pick_worker(attempt = 0)
127
+ pid = @workers.keys.sample
128
+ if @workers[pid].is_a? Thread
129
+ @workers[pid].join
130
+ end
131
+ @workers[pid]
132
+ rescue => e
133
+ log "Error picking worker: #{e.full_message}, attempt #{attempt}"
134
+ raise "Give up" if attempt > 2
135
+ del_worker(pid)
136
+ spawn_worker
137
+ pick_worker(attempt + 1)
138
+ end
139
+
140
+ def set_worker_uri(uri, pid)
141
+ t = @workers[pid]
142
+ @workers[pid] = DRbObject.new_with_uri(uri)
143
+ log "Worker #{pid} is up"
144
+ t.exit
145
+ end
146
+
147
+ def crash_worker(pid, message)
148
+ @workers[pid].raise message
149
+ @workers.delete(pid)
150
+ end
151
+
152
+ def del_worker(pid)
153
+ if @workers[pid].is_a?(Thread)
154
+ @workers[pid].exit
155
+ end
156
+ @workers.delete(pid)
157
+ end
158
+
159
+ def create(klass)
160
+ id = records_length
161
+ w = pick_worker
162
+ w.create(id, klass)
163
+ log "Create record #{id} in worker #{w.uri}"
164
+ @records[id] = w
165
+ @records_length += 1
166
+ id
167
+ end
168
+
169
+ def detach(id)
170
+ @records[id].detach(id)
171
+ nil
172
+ end
173
+
174
+ def detach_record(id)
175
+ @records.delete(id)
176
+ end
177
+
178
+ def load(id)
179
+ @records[id] = Thread.new do
180
+ w = pick_worker
181
+ log "Load record #{id} in worker #{w.uri}"
182
+ obj = w.load(id)
183
+ @records[id] = w
184
+ obj
185
+ end
186
+ @records[id].value
187
+ end
188
+
189
+ def allocate_record(pid)
190
+ id = records_length
191
+ @records_length += 1
192
+ @records[id] = @workers[pid]
193
+ log "Allocate record #{id}"
194
+ id
195
+ end
196
+
197
+ def destroy(id)
198
+ destroy!(id)
199
+ rescue => e
200
+ log "Can't destroy - #{e.full_message}"
201
+ nil
202
+ end
203
+
204
+ def destroy!(id)
205
+ throw "Record #{id} is in use" if @records[id]
206
+ FileUtils.rm_r(File.join(dir, "records", id.to_s))
207
+ log "Destroyed #{id}"
208
+ nil
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qwe::DB
4
+ class Worker
5
+ include DRb::DRbUndumped
6
+ include Qwe::Mixins::Process
7
+
8
+ attr_reader :server_dir, :server, :records, :gc_interval, :detach_timeout, :requirements
9
+
10
+ POLL_INTERVAL = 20
11
+
12
+ def initialize(server_url, server_dir, gc_interval = 0, detach_timeout = 300, requirements = nil)
13
+ @server_dir = server_dir
14
+ @server = DRbObject.new_with_uri(server_url)
15
+ @gc_interval = gc_interval.to_i
16
+ @detach_timeout = detach_timeout.to_i
17
+
18
+ @requirements = requirements
19
+ begin
20
+ require requirements if requirements
21
+ rescue ScriptError, StandardError => e
22
+ server.crash_worker(Process.pid, "Error requiring '#{requirements}': #{e.full_message}")
23
+ exit
24
+ end
25
+
26
+ Qwe::Function.compile_all
27
+
28
+ @records = {}
29
+ @object_refs = {}
30
+ end
31
+
32
+ def start_poll
33
+ @poll_thread = Thread.new do
34
+ loop do
35
+ sleep POLL_INTERVAL
36
+ @records.keys.each do |id|
37
+ r = @records[id]
38
+ detach(id, r) if r.should_detach?
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def detach(id, record = nil)
45
+ record ||= @records[id]
46
+ @server.detach_record(id)
47
+ @object_refs.delete(id)
48
+ @records.delete(id)
49
+ record.save
50
+ log "Detach record #{id} from worker #{uri}"
51
+ nil
52
+ end
53
+
54
+ def start_gc
55
+ @gc_thread = Thread.new do
56
+ loop do
57
+ sleep @gc_interval
58
+ next if @records.empty?
59
+ GC.start
60
+ end
61
+ end
62
+ end
63
+
64
+ def uri
65
+ @drb&.uri
66
+ end
67
+
68
+ def work
69
+ @drb = DRb.start_service("druby://localhost:0", self)
70
+ log "Started worker at #{@drb.uri}, pid = #{Process.pid}, requirements = #{@requirements}"
71
+ @server.set_worker_uri(uri, Process.pid)
72
+
73
+ start_poll
74
+ start_gc if @gc_interval > 0
75
+
76
+ trap_stop_signals
77
+
78
+ @drb.thread.join
79
+ end
80
+
81
+ def create(id, klass)
82
+ @records[id] = Qwe::DB::Record.new(id, server_dir, create: klass)
83
+ end
84
+
85
+ def load(id)
86
+ @records[id] = Qwe::DB::Record.new(id, server_dir)
87
+ DRbObject.new(@records[id].object)
88
+ end
89
+
90
+ def [](i)
91
+ @object_refs[i] ||= DRbObject.new(@records[i].object)
92
+ end
93
+
94
+ def stop
95
+ return if @stopping
96
+ @stopping = true
97
+ log "Stopping"
98
+ @records.keys.each { |id| @records[id].save }
99
+ exit
100
+ end
101
+
102
+ def allocate_record(zero)
103
+ log "Allocate record for #{zero.class}"
104
+ id = server.allocate_record(Process.pid)
105
+ records[id] = Qwe::DB::Record.new(id, server_dir, create: zero)
106
+ end
107
+
108
+ def self.instance
109
+ @@instance
110
+ end
111
+
112
+ def self.spawn(*)
113
+ @@instance = Qwe::DB::Worker.new(*)
114
+ @@instance.work
115
+ end
116
+
117
+ def e(str)
118
+ instance_eval str
119
+ end
120
+ end
121
+ end
data/lib/qwe/db.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "db/commits_file"
4
+ require_relative "db/server"
5
+ require_relative "db/worker"
6
+ require_relative "db/record"
7
+
8
+ module Qwe
9
+ module DB
10
+ # Server #
11
+
12
+ def self.serve(*a1, **a2)
13
+ @@srv = Server.new(*a1, **a2)
14
+ @@srv.start
15
+ end
16
+
17
+ def self.stop
18
+ @@srv.stop
19
+ end
20
+
21
+ # Client #
22
+
23
+ def self.connect(uri = Server.default_uri)
24
+ @@server = DRbObject.new_with_uri(uri)
25
+ 300.times do
26
+ if server.ready?
27
+ return server
28
+ end
29
+ sleep 0.1
30
+ rescue
31
+ sleep 0.1
32
+ end
33
+ raise "Couldn't connect to server in more than 30s"
34
+ end
35
+
36
+ def self.server
37
+ @@server ||= DRbObject.new_with_uri(Server.default_uri)
38
+ end
39
+
40
+ def self.[](id)
41
+ server[id]
42
+ end
43
+
44
+ def self.create(...)
45
+ server.create(...)
46
+ end
47
+
48
+ def self.destroy(...)
49
+ server.destroy(...)
50
+ end
51
+
52
+ def self.destroy!(...)
53
+ server.destroy!(...)
54
+ end
55
+
56
+ def self.detach(id)
57
+ server.detach(id)
58
+ nil
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qwe
4
+ class Function
5
+ attr_accessor :klass
6
+ attr_reader :name
7
+
8
+ def name=(n)
9
+ @name = n.to_sym
10
+ end
11
+
12
+ def self.instances
13
+ @@instances ||= []
14
+ end
15
+
16
+ def self.compile_all
17
+ instances.each(&:compile)
18
+ end
19
+
20
+ def initialize(klass, name)
21
+ self.klass = klass
22
+ self.name = name
23
+ self.class.instances.push(self)
24
+ Qwe[klass, :functions, name] = self
25
+
26
+ unless klass.respond_to?(:compile)
27
+ klass.define_singleton_method(:compile) do
28
+ Qwe[self, :functions].each do |n, f|
29
+ f.compile
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def args
36
+ @args ||= {}
37
+ end
38
+
39
+ def arg(name, default = nil)
40
+ args[name] = default
41
+ end
42
+
43
+ def keywords
44
+ @keywords ||= {}
45
+ end
46
+
47
+ def keyword(name, default = nil)
48
+ keywords[name] = default
49
+ end
50
+
51
+ def stages
52
+ @stages ||= []
53
+ end
54
+
55
+ def stage(code)
56
+ @compiled = false
57
+ stages.push(code) if code
58
+ end
59
+
60
+ def code
61
+ rb = "def #{name}(" + \
62
+ ((args.to_a.map { |a| "#{a[0]} = #{a[1].to_rb}" }) + \
63
+ (keywords.to_a.map { |a| "#{a[0]}: #{a[1].to_rb}" })).join(", ") + \
64
+ ")\n"
65
+ stages.each do |s|
66
+ rb += " " + s + "\n"
67
+ end
68
+ rb += "end"
69
+ end
70
+
71
+ def compile
72
+ return if @compiled
73
+
74
+ begin
75
+ klass.module_eval(code)
76
+ rescue ScriptError, StandardError => e
77
+ log "Error compiling #{klass}.#{name}, code is: \n#{code}\n"
78
+ raise e
79
+ end
80
+
81
+ @compiled = true
82
+ end
83
+
84
+ def compiled?
85
+ @compiled
86
+ end
87
+
88
+ def inspect
89
+ "\nFunction #{klass}.#{name}\n#{code}\n"
90
+ end
91
+
92
+ def self.compile(klass, name, &block)
93
+ f = Qwe::Function.new(klass, name)
94
+ yield f
95
+ f.compile
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qwe::Mixins::Process
4
+ def trap_stop_signals
5
+ Signal.trap("INT") do
6
+ log "SIGINT, stop"
7
+ stop
8
+ end
9
+ Signal.trap("TERM") do
10
+ log "SIGTERM, stop"
11
+ stop
12
+ end
13
+ end
14
+ end