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.
- checksums.yaml +7 -0
- data/DOCS.md +469 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +22 -0
- data/exe/qwe +118 -0
- data/lib/guerilla.rb +83 -0
- data/lib/puma/plugin/qwe.rb +80 -0
- data/lib/qwe/attribute.rb +193 -0
- data/lib/qwe/db/commits_file.rb +71 -0
- data/lib/qwe/db/record.rb +179 -0
- data/lib/qwe/db/server.rb +211 -0
- data/lib/qwe/db/worker.rb +121 -0
- data/lib/qwe/db.rb +61 -0
- data/lib/qwe/function.rb +98 -0
- data/lib/qwe/mixins/process.rb +14 -0
- data/lib/qwe/mixins/root.rb +45 -0
- data/lib/qwe/mixins/thing.rb +36 -0
- data/lib/qwe/mixins.rb +17 -0
- data/lib/qwe/version.rb +5 -0
- data/lib/qwe.rb +49 -0
- data/lib/spawn_worker.rb +5 -0
- data/qwe.gemspec +32 -0
- metadata +98 -0
data/README.md
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# Qwe - pure ruby framework for stuff
|
2
|
+
|
3
|
+
The stuff is mostly complex ruby objects living in RAM for a long time, accessed over DRb. Qwe provides database-like interface for managing that with a few extras, while attempting to impose as little bounds
|
4
|
+
as possible on the way you write code.
|
5
|
+
|
6
|
+
[Full Documentation](DOCS.md)
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
### Zero-config startup
|
11
|
+
|
12
|
+
```
|
13
|
+
bundle exec qwe
|
14
|
+
```
|
15
|
+
And you are ready to go.
|
16
|
+
[Puma plugin](DOCS.md#Puma-plugin) is also available.
|
17
|
+
|
18
|
+
### Object persistence
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
id = Qwe::DB.create(Hash)
|
22
|
+
```
|
23
|
+
The hash is now accessible from any ruby process with `Qwe::DB[]`,
|
24
|
+
considering you know that id.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
Qwe::DB[id][:qwe] = 123
|
28
|
+
```
|
29
|
+
|
30
|
+
After a period of inactivity, or manually if configured,
|
31
|
+
hash will get dumped on disk with `Marshal` and RAM freed.
|
32
|
+
Once you call `Qwe::DB[id]` again, object gets un-marshalled and lives in RAM the same way.
|
33
|
+
|
34
|
+
### Attributes API
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class Person
|
38
|
+
include Qwe::Mixins::Thing
|
39
|
+
|
40
|
+
attribute :age, min: 0, init: 18, convert: :to_i
|
41
|
+
end
|
42
|
+
```
|
43
|
+
is equivalent to
|
44
|
+
```ruby
|
45
|
+
class Person
|
46
|
+
def initialize
|
47
|
+
@age = 18
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :age
|
51
|
+
|
52
|
+
def age=(val)
|
53
|
+
val = val.to_i
|
54
|
+
val = 0 if val < 0
|
55
|
+
@age = val
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
Despite the fact that `attribute` method accepts many arguments and can be extended,
|
61
|
+
performance of both variants is the same.
|
62
|
+
|
63
|
+
### Transactions
|
64
|
+
|
65
|
+
Every attribute change can be tracked, so you can scroll back and forth your object state
|
66
|
+
throughout it's lifecycle. Changes are stored as ruby code in plaintext file.
|
67
|
+
|
68
|
+
requirements.rb
|
69
|
+
```ruby
|
70
|
+
class Person
|
71
|
+
include Qwe::Mixins::Root
|
72
|
+
|
73
|
+
attribute :age, init: 0, convert: :to_i
|
74
|
+
end
|
75
|
+
```
|
76
|
+
Server startup
|
77
|
+
```
|
78
|
+
qwe -r requirements.rb
|
79
|
+
```
|
80
|
+
ruby console
|
81
|
+
```ruby
|
82
|
+
id = Qwe::DB.create(:Person)
|
83
|
+
Qwe::DB[id].age = 1
|
84
|
+
Qwe::DB[id].age += 2
|
85
|
+
Qwe::DB[id].age += 3
|
86
|
+
puts Qwe::DB[id].record.commits
|
87
|
+
```
|
88
|
+
Outputs:
|
89
|
+
```ruby
|
90
|
+
self.instance_variable_set(:@age, 1)
|
91
|
+
self.instance_variable_set(:@age, 3)
|
92
|
+
self.instance_variable_set(:@age, 6)
|
93
|
+
```
|
94
|
+
|
95
|
+
While commit "logs" are not handcrafted ruby code, they are still readable.
|
96
|
+
Most standard types are supported and subject to extension.
|
97
|
+
|
98
|
+
# Requirements
|
99
|
+
|
100
|
+
ruby 3+ is required, but since long-running workers greatly benefit from YJIT compiler, and compiler is greatly improved in subsequent releases, it is better to use latest ruby whenever possible.
|
101
|
+
|
102
|
+
# Notes
|
103
|
+
|
104
|
+
## Is it production ready?
|
105
|
+
|
106
|
+
It is extracted from production and backported there, but your case may be different. Feedback is welcome.
|
107
|
+
|
108
|
+
## Does it scale?
|
109
|
+
|
110
|
+
It depends, but mostly yes.
|
111
|
+
|
112
|
+
On MRI one object lives in one thread. Qwe can work with any amount of worker threads,
|
113
|
+
but it's for increasing amount of simultaneously served objects.
|
114
|
+
Other ruby implementations are not tested.
|
115
|
+
|
116
|
+
The primary limitation and at the same time strength is the fact that "root" objects
|
117
|
+
(the ones created with Qwe::DB.create) are fully loaded into RAM upon access.
|
118
|
+
It makes working with them faster after a short startup delay, but does not allow to effectively perform SQL-like
|
119
|
+
requests across multiple root objects.
|
120
|
+
|
121
|
+
If root object are independent from each other, system scales easily
|
122
|
+
by running independent Qwe servers.
|
123
|
+
|
124
|
+
## Note on security
|
125
|
+
|
126
|
+
`Qwe::DB[]` provides access to object from another thread with **DRb**, ruby gem from standard library.
|
127
|
+
DRb does not provide any access control, and opens separate port for each object it serves.
|
128
|
+
It is safe to run Qwe in docker container or other isolated network, and unsafe to run on virtual or physical device with all ports open.
|
129
|
+
|
130
|
+
See [DRb docs](https://docs.ruby-lang.org/en/3.3/DRb.html#module-DRb-label-Security) for a more detailed explanation.
|
131
|
+
|
132
|
+
## License
|
133
|
+
|
134
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "etc"
|
6
|
+
|
7
|
+
task :server do
|
8
|
+
pid = spawn("#{__dir__}/exe/qwe", "-p", "3229", "-d", Etc.systmpdir + "/qwe_db", "-r", "#{__dir__}/test/requirements.rb", "-s", "30", "--gc", "5", "-w", "#{__dir__}/lib")
|
9
|
+
Process.wait(pid)
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << "test"
|
14
|
+
t.libs << "lib"
|
15
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
16
|
+
end
|
17
|
+
|
18
|
+
require "rubocop/rake_task"
|
19
|
+
|
20
|
+
RuboCop::RakeTask.new
|
21
|
+
|
22
|
+
task default: %i[test rubocop]
|
data/exe/qwe
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
5
|
+
|
6
|
+
require "qwe"
|
7
|
+
require "optparse"
|
8
|
+
|
9
|
+
args = {}
|
10
|
+
opt_parser = OptionParser.new do |parser|
|
11
|
+
parser.banner = "Usage: qwe [options] - start server\n"
|
12
|
+
|
13
|
+
parser.on("-dDIR", "--dir=DIR", "Directory with persistent data") do |a|
|
14
|
+
args[:dir] = a
|
15
|
+
end
|
16
|
+
|
17
|
+
parser.on("-pPORT", "--port=PORT", "Server port") do |a|
|
18
|
+
args[:port] = a
|
19
|
+
end
|
20
|
+
|
21
|
+
parser.on("-wDIR", "--watch=DIR", "Restart server on changes in that directory. Can be passed multiple times") do |a|
|
22
|
+
args[:watch] ||= []
|
23
|
+
args[:watch].push a
|
24
|
+
end
|
25
|
+
|
26
|
+
parser.on("-rFILE", "--require=FILE", "--require_file=FILE", "This file will be required in worker threads") do |a|
|
27
|
+
args[:require_file] = File.expand_path(a)
|
28
|
+
end
|
29
|
+
|
30
|
+
parser.on("-iID", "--interactive=ID", "Launch interactive console bound to record #ID") do |a|
|
31
|
+
args[:interactive] = a.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
parser.on("-cCLASS", "--create=CLASS", "Connect to running server, create new record of class CLASS, and print its id") do |a|
|
35
|
+
args[:create] = a
|
36
|
+
end
|
37
|
+
|
38
|
+
parser.on("-tTHREADS", "--threads=THREADS", "Worker threads count. Defaults to the number of CPU cores") do |a|
|
39
|
+
args[:threads] = a.to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
parser.on("--gc=SECONDS", "--gc_interval=SECONDS", "Call garbage collector (GC.start) every x seconds. Off by default") do |a|
|
43
|
+
args[:gc_interval] = a.to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
parser.on("-sSECONDS", "--detach=SECONDS", "--detach_timeout=SECONDS", "Detach record after x seconds since creation or loading. 0 - disable, default 300.") do |a|
|
47
|
+
args[:detach_timeout] = a.to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
parser.on("--no-jit", "--no_jit", "Disable JIT. It's on by default") do |a|
|
51
|
+
args[:no_jit] = true
|
52
|
+
end
|
53
|
+
|
54
|
+
parser.on("-h", "--help", "Print this help") do
|
55
|
+
puts parser
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
opt_parser.parse!
|
60
|
+
|
61
|
+
def spawn_server(args)
|
62
|
+
spawn(__FILE__, *((args.to_a.filter { |a| a[0] != :watch }).map { |a| "--#{a[0]}=#{a[1]}" }))
|
63
|
+
end
|
64
|
+
|
65
|
+
if args[:watch]
|
66
|
+
begin
|
67
|
+
require "listen"
|
68
|
+
rescue LoadError
|
69
|
+
puts "Can't load 'listen' gem"
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
pid = spawn_server(args)
|
73
|
+
Process.detach(pid)
|
74
|
+
|
75
|
+
puts "Watching changes in #{args[:watch]}..."
|
76
|
+
Listen.to(*args[:watch]) do
|
77
|
+
Process.kill("INT", pid)
|
78
|
+
pid = spawn_server(args)
|
79
|
+
Process.detach(pid)
|
80
|
+
puts "\n\n"
|
81
|
+
end.start
|
82
|
+
|
83
|
+
trap :INT do
|
84
|
+
Process.kill("INT", pid)
|
85
|
+
Process.exit
|
86
|
+
end
|
87
|
+
sleep
|
88
|
+
elsif args[:interactive]
|
89
|
+
require "irb"
|
90
|
+
|
91
|
+
Qwe::DB.connect("druby://localhost:#{args[:port]}") if args[:port]
|
92
|
+
|
93
|
+
if args[:interactive] > 0
|
94
|
+
obj = Qwe::DB[args[:interactive]]
|
95
|
+
puts "\e[0;32mQwe interactive prompt\e[0m, record \e[1;32m##{args[:interactive]}\e[0m, class \e[1;36m#{obj.record.obj_eval("self.class.to_s")}\e[0m"
|
96
|
+
else
|
97
|
+
obj = Qwe::DB.server.pick_worker
|
98
|
+
puts "\e[0;32mQwe interactive prompt\e[0m, worker \e[1;36m#{obj.uri}\e[0m, require #{obj.requirements}"
|
99
|
+
|
100
|
+
require obj.requirements
|
101
|
+
end
|
102
|
+
|
103
|
+
(obj.instance_exec { binding }).instance_exec do
|
104
|
+
irb(show_code: false)
|
105
|
+
rescue
|
106
|
+
# Older irb versions always print source code around call point
|
107
|
+
IRB.setup(source_location[0], argv: [])
|
108
|
+
workspace = IRB::WorkSpace.new(self)
|
109
|
+
binding_irb = IRB::Irb.new(workspace)
|
110
|
+
binding_irb.context.irb_path = File.expand_path(source_location[0])
|
111
|
+
binding_irb.run(IRB.conf)
|
112
|
+
end
|
113
|
+
elsif args[:create]
|
114
|
+
Qwe::DB.connect("druby://localhost:#{args[:port]}") if args[:port]
|
115
|
+
puts Qwe::DB.create(args[:create].to_sym)
|
116
|
+
else
|
117
|
+
Qwe::DB.serve(**args)
|
118
|
+
end
|
data/lib/guerilla.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class BasicObject
|
4
|
+
def self.to_rb
|
5
|
+
name
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class String
|
10
|
+
def to_rb
|
11
|
+
"'" + gsub("'", "\\\\'") + "'"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Symbol
|
16
|
+
def to_rb
|
17
|
+
":\"#{self}\""
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Hash
|
22
|
+
def to_rb
|
23
|
+
"{" + (to_a.map { |v| "#{v[0].to_rb} => #{v[1].to_rb}" }).join(", ") + "}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Array
|
28
|
+
def to_rb
|
29
|
+
"[" + (map { |v| v.to_rb }).join(", ") + "]"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Time
|
34
|
+
def to_rb
|
35
|
+
"Time.at(#{to_r}r)"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Float
|
40
|
+
def to_rb
|
41
|
+
if nan?
|
42
|
+
"Float::NAN"
|
43
|
+
elsif infinite?
|
44
|
+
"Float::INFINITY"
|
45
|
+
else
|
46
|
+
to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Integer
|
52
|
+
alias_method :to_rb, :to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
class Rational
|
56
|
+
def to_rb
|
57
|
+
to_s + "r"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Vector
|
62
|
+
def to_rb
|
63
|
+
"Vector#{to_a.to_rb}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class TrueClass
|
68
|
+
def to_rb
|
69
|
+
"true"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class FalseClass
|
74
|
+
def to_rb
|
75
|
+
"false"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class NilClass
|
80
|
+
def to_rb
|
81
|
+
"nil"
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "puma/plugin"
|
2
|
+
|
3
|
+
Puma::Plugin.create do
|
4
|
+
attr_reader :puma_pid, :qwe_pid, :log_writer
|
5
|
+
|
6
|
+
def start(launcher)
|
7
|
+
@log_writer = launcher.log_writer
|
8
|
+
@puma_pid = $$
|
9
|
+
|
10
|
+
in_background do
|
11
|
+
monitor_qwe
|
12
|
+
end
|
13
|
+
|
14
|
+
launcher.events.on_booted do
|
15
|
+
@qwe_pid = fork do
|
16
|
+
Thread.new { monitor_puma }
|
17
|
+
conf = Rails.root.join("config", "qwe.yml")
|
18
|
+
if File.exist?(conf)
|
19
|
+
log "Loading config #{conf}"
|
20
|
+
Qwe::DB.serve(**YAML.parse_file(conf).to_ruby(symbolize_names: true))
|
21
|
+
else
|
22
|
+
Qwe::DB.serve
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
launcher.events.on_stopped { stop_qwe }
|
28
|
+
launcher.events.on_restart { stop_qwe }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def stop_qwe
|
34
|
+
Process.waitpid(qwe_pid, Process::WNOHANG)
|
35
|
+
log "Stopping Qwe..."
|
36
|
+
Process.kill(:INT, qwe_pid) if qwe_pid
|
37
|
+
Process.wait(qwe_pid)
|
38
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
39
|
+
end
|
40
|
+
|
41
|
+
def monitor_puma
|
42
|
+
monitor(:puma_dead?, "Detected Puma has gone away, stopping Qwe...")
|
43
|
+
end
|
44
|
+
|
45
|
+
def monitor_qwe
|
46
|
+
monitor(:qwe_dead?, "Detected Qwe has gone away, stopping Puma...")
|
47
|
+
end
|
48
|
+
|
49
|
+
def monitor(process_dead, message)
|
50
|
+
loop do
|
51
|
+
if send(process_dead)
|
52
|
+
log message
|
53
|
+
Process.kill(:INT, $$)
|
54
|
+
break
|
55
|
+
end
|
56
|
+
sleep 2
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def qwe_dead?
|
61
|
+
if qwe_started?
|
62
|
+
Process.waitpid(qwe_pid, Process::WNOHANG)
|
63
|
+
end
|
64
|
+
false
|
65
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def qwe_started?
|
70
|
+
qwe_pid.present?
|
71
|
+
end
|
72
|
+
|
73
|
+
def puma_dead?
|
74
|
+
Process.ppid != puma_pid
|
75
|
+
end
|
76
|
+
|
77
|
+
def log(...)
|
78
|
+
log_writer.log(...)
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qwe
|
4
|
+
class Attribute
|
5
|
+
@@possible_params = {}
|
6
|
+
|
7
|
+
[:name, :reader, :writer].each do |k|
|
8
|
+
@@possible_params[k] = true
|
9
|
+
attr_accessor k
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :klass, :params
|
13
|
+
|
14
|
+
def initialize(klass, name, *booleans, writer: nil, reader: nil, **params)
|
15
|
+
@klass = klass
|
16
|
+
@name = name
|
17
|
+
@params = params
|
18
|
+
raise "Parameter 'name' is reserved" if @params[:name]
|
19
|
+
@params[:name] = name
|
20
|
+
booleans.each { |b| @params[b] = true }
|
21
|
+
|
22
|
+
@params.each_key do |p|
|
23
|
+
raise "Unknown parameter #{p}" unless @@possible_params.include?(p)
|
24
|
+
end
|
25
|
+
|
26
|
+
@reader = if reader
|
27
|
+
if reader.is_a?(Proc)
|
28
|
+
reader
|
29
|
+
else
|
30
|
+
raise "Reader parameter should be a proc, #{reader.class} given"
|
31
|
+
end
|
32
|
+
else
|
33
|
+
Qwe::Function.new(klass, name)
|
34
|
+
end
|
35
|
+
|
36
|
+
@writer = Qwe::Function.new(klass, :"#{name}=")
|
37
|
+
if writer
|
38
|
+
if writer.is_a?(Proc)
|
39
|
+
klass.define_method(:"#{name}_writer", &writer)
|
40
|
+
@writer.stage("value = #{name}_writer(value)")
|
41
|
+
else
|
42
|
+
raise "writer parameter should be a Proc, #{writer.class} given"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@@extensions = []
|
48
|
+
def self.add(p = nil, &block)
|
49
|
+
if p
|
50
|
+
@@possible_params[p] = true
|
51
|
+
if block
|
52
|
+
@@extensions.push(proc { instance_exec(&block) if self[p] })
|
53
|
+
end
|
54
|
+
elsif block
|
55
|
+
@@extensions.push(block)
|
56
|
+
else
|
57
|
+
raise ArgumentError.new("Attribute.add - param or block required")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def compile
|
62
|
+
writer.arg(:value)
|
63
|
+
|
64
|
+
@@extensions.each do |block|
|
65
|
+
instance_exec(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
if reader.is_a?(Function)
|
69
|
+
reader.stage "@#{name}"
|
70
|
+
reader.compile
|
71
|
+
else
|
72
|
+
klass.define_method(name, &reader)
|
73
|
+
end
|
74
|
+
|
75
|
+
writer.stage("@#{name} = value")
|
76
|
+
writer.stage("root&.record&.commit(\"\#{to_rb}.instance_variable_set(:@#{name}, \#{value.to_rb})\\n\")")
|
77
|
+
writer.stage("value")
|
78
|
+
writer.compile
|
79
|
+
end
|
80
|
+
|
81
|
+
def method_missing(symbol, *args)
|
82
|
+
if @@possible_params.has_key?(symbol)
|
83
|
+
@params[symbol]
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def respond_to_missing?(symbol, *args)
|
90
|
+
@@possible_params[symbol] || super
|
91
|
+
end
|
92
|
+
|
93
|
+
def [](key)
|
94
|
+
@params[key]
|
95
|
+
end
|
96
|
+
|
97
|
+
def inspect
|
98
|
+
"Attribute #{klass}.#{params}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def init_or_default_stage(p)
|
102
|
+
if self[p].is_a?(Class)
|
103
|
+
"#{self[p]}.new"
|
104
|
+
else
|
105
|
+
klass.class_variable_set(:"@@#{name}_#{p}", self[p])
|
106
|
+
if self[p].is_a?(Proc)
|
107
|
+
"instance_exec(&@@#{name}_#{p})"
|
108
|
+
else
|
109
|
+
"@@#{name}_#{p}.clone"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
add(:init) do
|
115
|
+
klass.init.stage "self.#{name} = " + init_or_default_stage(:init)
|
116
|
+
end
|
117
|
+
|
118
|
+
add(:default) do
|
119
|
+
reader.stage "self.#{name} = " + init_or_default_stage(:default) + " unless @#{name}"
|
120
|
+
end
|
121
|
+
|
122
|
+
add(:convert) do
|
123
|
+
writer.stage "value = value.#{convert}"
|
124
|
+
end
|
125
|
+
|
126
|
+
add(:min) do
|
127
|
+
klass.class_variable_set(:"@@#{name}_min", convert && min.send(convert) || min)
|
128
|
+
writer.stage "value = @@#{name}_min.clone if value < @@#{name}_min"
|
129
|
+
end
|
130
|
+
|
131
|
+
add(:max) do
|
132
|
+
klass.class_variable_set(:"@@#{name}_max", convert && max.send(convert) || max)
|
133
|
+
writer.stage "value = @@#{name}_max.clone if value > @@#{name}_max"
|
134
|
+
end
|
135
|
+
|
136
|
+
add(:array) do
|
137
|
+
writer.stage "value = Qwe::Attribute::ArrayProxy.from_array(self, :#{name}, value)"
|
138
|
+
klass.init.stage("self.#{name} = []") unless init
|
139
|
+
end
|
140
|
+
|
141
|
+
add(:enum) do
|
142
|
+
raise "#{klass}.#{name} - enum #{enum} doesn't respond to include?" unless enum.respond_to?(:include?)
|
143
|
+
klass.class_variable_set(:"@@#{name}_enum", enum)
|
144
|
+
writer.stage "raise \"Enum - unknown value \#{self}.#{name} = \#{value}\" unless @@#{name}_enum.include?(value)"
|
145
|
+
end
|
146
|
+
|
147
|
+
class ArrayProxy < Array
|
148
|
+
attr_accessor :__qwe_thing
|
149
|
+
attr_accessor :__qwe_name
|
150
|
+
|
151
|
+
include DRb::DRbUndumped
|
152
|
+
|
153
|
+
undef _dump
|
154
|
+
|
155
|
+
def __qwe_commit(m, *a, **k)
|
156
|
+
__qwe_thing&.root&.record&.commit("#{__qwe_thing.to_rb}.#{__qwe_name}.method(:#{m}).super_method.call(*#{a.to_rb}, **#{k.to_rb})\n")
|
157
|
+
end
|
158
|
+
|
159
|
+
TRANSACTION_METHODS = [:<<, :[]=, :clear, :compact!, :concat, :delete_at, :flatten!, :insert, :pop, :push, :replace, :reverse!, :rotate!, :shift, :shuffle!, :slice!, :unshift]
|
160
|
+
REPLACE_METHODS = [:delete, :delete_if, :fill, :filter!, :select!, :keep_if, :map!, :reject!, :sort!, :sort_by!, :uniq!]
|
161
|
+
|
162
|
+
TRANSACTION_METHODS.each do |m|
|
163
|
+
define_method(m) do |*a, **k|
|
164
|
+
__qwe_commit(m, *a, **k)
|
165
|
+
super(*a, **k)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
REPLACE_METHODS.each do |m|
|
170
|
+
define_method(m) do |*a, **k, &block|
|
171
|
+
r = super(*a, **k, &block)
|
172
|
+
if block
|
173
|
+
__qwe_thing&.root&.record&.commit "#{__qwe_thing.to_rb}.instance_variable_set(:@#{__qwe_name}, #{to_rb})\n"
|
174
|
+
else
|
175
|
+
__qwe_commit(m, *a, **k)
|
176
|
+
end
|
177
|
+
r
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.from_array(thing, name, arr = [])
|
182
|
+
a = new(arr)
|
183
|
+
a.__qwe_thing = thing
|
184
|
+
a.__qwe_name = name
|
185
|
+
a
|
186
|
+
end
|
187
|
+
|
188
|
+
def to_rb
|
189
|
+
"Qwe::Attribute::ArrayProxy.from_array(#{__qwe_thing.to_rb}, :#{__qwe_name}, #{super})"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zstds"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Qwe::DB
|
7
|
+
class CommitsFile
|
8
|
+
include DRb::DRbUndumped
|
9
|
+
|
10
|
+
attr_reader :path, :archive_path, :file
|
11
|
+
|
12
|
+
def initialize(dir)
|
13
|
+
@path = File.join(dir, "commits")
|
14
|
+
@archive_path = File.join(dir, "commits.zst")
|
15
|
+
unless File.exist?(@archive_path)
|
16
|
+
@file = File.open(@path, "a+")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(str)
|
21
|
+
unless file
|
22
|
+
log "Write #{str} to archived commits file #{path}, unpacking"
|
23
|
+
ZSTDS::File.decompress archive_path, path
|
24
|
+
FileUtils.rm_f(archive_path)
|
25
|
+
@file = File.open(@path, "a+")
|
26
|
+
end
|
27
|
+
file.write(str)
|
28
|
+
rescue => e
|
29
|
+
log "Couldn't write #{str} to #{path}: #{e.full_message}"
|
30
|
+
0
|
31
|
+
end
|
32
|
+
|
33
|
+
def read(line = nil)
|
34
|
+
if file
|
35
|
+
file.flush
|
36
|
+
file.rewind
|
37
|
+
if line
|
38
|
+
lines = file.readlines
|
39
|
+
lines[0, line].join("\n")
|
40
|
+
else
|
41
|
+
file.read
|
42
|
+
end
|
43
|
+
else
|
44
|
+
cs = ZSTDS::Stream::Reader.open(archive_path) do |r|
|
45
|
+
r.set_encoding("UTF-8")
|
46
|
+
r.read
|
47
|
+
end
|
48
|
+
if line
|
49
|
+
cs.lines[0, line].join("")
|
50
|
+
else
|
51
|
+
cs
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def archive!
|
57
|
+
file&.close
|
58
|
+
ZSTDS::File.compress path, archive_path
|
59
|
+
FileUtils.rm_f(path)
|
60
|
+
@file = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def flush
|
64
|
+
file&.flush
|
65
|
+
end
|
66
|
+
|
67
|
+
def close
|
68
|
+
file&.close
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|