syntropy 0.40.0 → 0.42.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/CHANGELOG.md +14 -0
- data/TODO.md +334 -31
- data/cmd/new/template/app/_lib/storage.rb +2 -2
- data/cmd/new/template/config/test.rb +1 -1
- data/examples/agent/config/test.rb +1 -1
- data/examples/blog/app/_lib/posts.rb +10 -6
- data/examples/blog/app/_lib/storage.rb +2 -2
- data/examples/blog/app/posts/[id]/index.rb +2 -2
- data/examples/blog/config/test.rb +1 -1
- data/lib/syntropy/collection.rb +206 -0
- data/lib/syntropy/connection_pool.rb +118 -0
- data/lib/syntropy/controller_extensions.rb +12 -0
- data/lib/syntropy/http/server_connection.rb +18 -30
- data/lib/syntropy/json_api.rb +3 -12
- data/lib/syntropy/kv_store.rb +57 -0
- data/lib/syntropy/logger.rb +24 -47
- data/lib/syntropy/markdown.rb +1 -1
- data/lib/syntropy/module_loader.rb +53 -14
- data/lib/syntropy/schema.rb +95 -0
- data/lib/syntropy/storage.rb +42 -4
- data/lib/syntropy/test.rb +1 -1
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +2 -1
- data/syntropy.gemspec +2 -2
- data/test/fixtures/app/_lib/dep_invalidate.rb +5 -0
- data/test/fixtures/collection/basic/_articles/2026-06-30-foo.md +6 -0
- data/test/fixtures/collection/basic/_articles/2026-07-13-bar.md +6 -0
- data/test/fixtures/collection/basic/_articles/2026-08-07-baz.md +6 -0
- data/test/fixtures/collection/basic/index+.rb +23 -0
- data/test/fixtures/collection/docs/_pages/01-foo/01-foo.md +4 -0
- data/test/fixtures/collection/docs/_pages/01-foo/02-bar.md +4 -0
- data/test/fixtures/collection/docs/_pages/01-foo/_index.yml +1 -0
- data/test/fixtures/collection/docs/_pages/02-bar/01-bar.md +4 -0
- data/test/fixtures/collection/docs/_pages/02-bar/02-baz.md +4 -0
- data/test/fixtures/collection/docs/_pages/02-bar/_index.yml +1 -0
- data/test/fixtures/collection/docs/index+.rb +16 -0
- data/test/test_app.rb +7 -7
- data/test/test_cmd_new.rb +38 -0
- data/test/test_collection.rb +164 -0
- data/test/test_connection_pool.rb +79 -3
- data/test/test_http_server_connection.rb +6 -6
- data/test/test_json_api.rb +4 -4
- data/test/test_kv_store.rb +6 -31
- data/test/test_markdown.rb +3 -3
- data/test/test_module_loader.rb +15 -0
- data/test/test_schema.rb +4 -4
- data/test/test_storage.rb +103 -0
- metadata +24 -11
- data/lib/syntropy/storage/connection_pool.rb +0 -106
- data/lib/syntropy/storage/kv_store.rb +0 -68
- data/lib/syntropy/storage/prepared_query.rb +0 -36
- data/lib/syntropy/storage/schema.rb +0 -92
- data/lib/syntropy/storage/store.rb +0 -33
- data/test/test_store.rb +0 -24
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'syntropy/markdown'
|
|
6
|
+
|
|
7
|
+
module Syntropy
|
|
8
|
+
# A collection represents a collection of data entities represented in files.
|
|
9
|
+
class Collection
|
|
10
|
+
attr_reader :root, :root_dir, :url_base
|
|
11
|
+
|
|
12
|
+
# Initializes a collection.
|
|
13
|
+
#
|
|
14
|
+
# @param machine [UringMachine]
|
|
15
|
+
# @param root [String] collection root
|
|
16
|
+
# @param url_base [String] URL base
|
|
17
|
+
# @return [void]
|
|
18
|
+
def initialize(machine:, root:, url_base:)
|
|
19
|
+
@machine = machine
|
|
20
|
+
@root_dir = File.expand_path(root)
|
|
21
|
+
@url_base = url_base
|
|
22
|
+
@list = {} # list of non-directory items
|
|
23
|
+
@root = make_tree_root
|
|
24
|
+
@dirs = { @root_dir => @root } # mapping directory paths to tree items
|
|
25
|
+
|
|
26
|
+
calc_collection_tree
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns a list of items in the collection.
|
|
30
|
+
#
|
|
31
|
+
# @return [Array] items
|
|
32
|
+
def list
|
|
33
|
+
@list.values
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the item corresponding to the given URL.
|
|
37
|
+
#
|
|
38
|
+
# @param url [String] URL
|
|
39
|
+
# @return [Hash] item
|
|
40
|
+
def get(url)
|
|
41
|
+
@list[url]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def make_tree_root
|
|
47
|
+
{
|
|
48
|
+
fn: find_directory_data_file(@root_dir),
|
|
49
|
+
ref: '',
|
|
50
|
+
url: url_base,
|
|
51
|
+
type: :directory,
|
|
52
|
+
items: []
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Calculates the collection tree.
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
59
|
+
def calc_collection_tree
|
|
60
|
+
queue = UM::Queue.new
|
|
61
|
+
Dir[File.join(@root_dir, '**/*')].each do |fn|
|
|
62
|
+
next if File.basename(fn) =~ /^_/
|
|
63
|
+
|
|
64
|
+
ref = fn_to_ref(fn)
|
|
65
|
+
url = File.join(@url_base, ref)
|
|
66
|
+
parent_fn = File.expand_path(File.join(fn, '..'))
|
|
67
|
+
parent = @dirs[parent_fn]
|
|
68
|
+
raise Error, "Parent not found: #{parent_fn}" if !parent
|
|
69
|
+
|
|
70
|
+
if File.directory?(fn)
|
|
71
|
+
dir = {
|
|
72
|
+
fn: find_directory_data_file(fn),
|
|
73
|
+
ref:,
|
|
74
|
+
url:,
|
|
75
|
+
type: :directory,
|
|
76
|
+
items: []
|
|
77
|
+
}
|
|
78
|
+
@dirs[fn] = dir
|
|
79
|
+
parent[:items] << dir
|
|
80
|
+
@machine.push(queue, dir) if dir[:fn]
|
|
81
|
+
else
|
|
82
|
+
item = {
|
|
83
|
+
fn:,
|
|
84
|
+
ref:,
|
|
85
|
+
url:
|
|
86
|
+
}
|
|
87
|
+
@list[url] = item
|
|
88
|
+
parent[:items] << item
|
|
89
|
+
@machine.push(queue, item)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
4.times { @machine.push(queue, :stop) }
|
|
93
|
+
|
|
94
|
+
fibers = 4.times.map {
|
|
95
|
+
@machine.spin {
|
|
96
|
+
loop {
|
|
97
|
+
item = @machine.shift(queue)
|
|
98
|
+
break if item == :stop
|
|
99
|
+
|
|
100
|
+
load_item(item)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
@machine.join(fibers)
|
|
105
|
+
|
|
106
|
+
add_prev_next_links
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def scan_files(path, queue)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Converts a filename to a ref.
|
|
113
|
+
#
|
|
114
|
+
# @param fn [String]
|
|
115
|
+
# @return [String] ref
|
|
116
|
+
def fn_to_ref(fn)
|
|
117
|
+
@ref_rel_regexp ||= /^#{@root_dir}\/(.+)/
|
|
118
|
+
rel = fn.match(@ref_rel_regexp)[1]
|
|
119
|
+
|
|
120
|
+
(m = rel.match(/^(.+)\.(md|json|yml|yaml)$/)) ? m[1] : rel
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns the data file for the given directory.
|
|
124
|
+
#
|
|
125
|
+
# @param dir [String]
|
|
126
|
+
# @return [String, nil] directory data file
|
|
127
|
+
def find_directory_data_file(dir)
|
|
128
|
+
(
|
|
129
|
+
detect_file(File.join(dir, '_index.yml')) ||
|
|
130
|
+
detect_file(File.join(dir, '_index.json'))
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Detects if the given file exists.
|
|
135
|
+
#
|
|
136
|
+
# @param fn [String]
|
|
137
|
+
# @return [String, nil] filename if exists, nil otherwise
|
|
138
|
+
def detect_file(fn)
|
|
139
|
+
File.file?(fn) ? fn : nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Loads the given item.
|
|
143
|
+
#
|
|
144
|
+
# @param item [Hash]
|
|
145
|
+
# @return [void]
|
|
146
|
+
def load_item(item)
|
|
147
|
+
basename = File.basename(item[:fn])
|
|
148
|
+
case (ext = File.extname(basename))
|
|
149
|
+
when '.md'
|
|
150
|
+
item[:type] ||= :markdown
|
|
151
|
+
load_item_markdown(item)
|
|
152
|
+
when '.json'
|
|
153
|
+
item[:type] ||= :json
|
|
154
|
+
load_item_json(item)
|
|
155
|
+
when '.yml', '.yaml'
|
|
156
|
+
item[:type] ||= :yaml
|
|
157
|
+
load_item_yaml(item)
|
|
158
|
+
else
|
|
159
|
+
raise Syntropy::Error, "Unkown file type #{ext}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Loads ands parses the file for the given markdown item.
|
|
164
|
+
#
|
|
165
|
+
# @param item [Hash]
|
|
166
|
+
# @return [void]
|
|
167
|
+
def load_item_markdown(item)
|
|
168
|
+
attributes, body = Markdown.parse_file(item[:fn], {})
|
|
169
|
+
item.merge!(attributes:, body:)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Loads ands parses the file for the given JSON item.
|
|
173
|
+
#
|
|
174
|
+
# @param item [Hash]
|
|
175
|
+
# @return [void]
|
|
176
|
+
def load_item_json(item)
|
|
177
|
+
data = @machine.file_read(item[:fn])
|
|
178
|
+
item[:data] = JSON.parse(data, symbolize_names: true)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
YAML_OPTS = {
|
|
182
|
+
permitted_classes: [Date],
|
|
183
|
+
symbolize_names: true
|
|
184
|
+
}.freeze
|
|
185
|
+
|
|
186
|
+
# Loads ands parses the file for the given YAML item.
|
|
187
|
+
#
|
|
188
|
+
# @param item [Hash]
|
|
189
|
+
# @return [void]
|
|
190
|
+
def load_item_yaml(item)
|
|
191
|
+
data = @machine.file_read(item[:fn])
|
|
192
|
+
item[:data] = YAML.safe_load(data, **YAML_OPTS)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def add_prev_next_links
|
|
196
|
+
last = nil
|
|
197
|
+
@list.each_value { |item|
|
|
198
|
+
if last
|
|
199
|
+
item[:prev] = last
|
|
200
|
+
last[:next] = item
|
|
201
|
+
end
|
|
202
|
+
last = item
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'extralite'
|
|
4
|
+
|
|
5
|
+
module Syntropy
|
|
6
|
+
# ConnectionPool implements concurrent access to an SQLite database.
|
|
7
|
+
class ConnectionPool
|
|
8
|
+
attr_reader :count
|
|
9
|
+
|
|
10
|
+
# Initializes the connection pool.
|
|
11
|
+
#
|
|
12
|
+
# @param machine [UringMachine] machine instance
|
|
13
|
+
# @param fn [String] database filename
|
|
14
|
+
# @param max_conn [Integer] maximum number of connections
|
|
15
|
+
# @return [void]
|
|
16
|
+
def initialize(machine, fn, max_conn)
|
|
17
|
+
@machine = machine
|
|
18
|
+
@fn = fn
|
|
19
|
+
@count = 0
|
|
20
|
+
@max_conn = max_conn
|
|
21
|
+
@queue = UM::Queue.new
|
|
22
|
+
@key = :"connection_pool_#{object_id}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Checks out a database connection, passing it to the given block. This
|
|
26
|
+
# method is reentrant - called from the same fiber, it will yield the same
|
|
27
|
+
# database connection.
|
|
28
|
+
#
|
|
29
|
+
# @return [Extralite::Database] database connection
|
|
30
|
+
def with_db
|
|
31
|
+
if (db = Thread.current[@key])
|
|
32
|
+
@machine.snooze
|
|
33
|
+
return yield(db)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
db = checkout
|
|
37
|
+
begin
|
|
38
|
+
Thread.current[@key] = db
|
|
39
|
+
yield(db)
|
|
40
|
+
ensure
|
|
41
|
+
Thread.current[@key] = nil
|
|
42
|
+
checkin(db)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Performs the given query by acquiring a database connection and running
|
|
47
|
+
# the query.
|
|
48
|
+
#
|
|
49
|
+
# @return [Array<Hash>] result rows
|
|
50
|
+
def query(*, **, &)
|
|
51
|
+
with_db { it.query(*, **, &) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Performs the given query by acquiring a database connection and running
|
|
55
|
+
# the query.
|
|
56
|
+
#
|
|
57
|
+
# @return [<Hash>, nil] result row
|
|
58
|
+
def query_single_row(*, **)
|
|
59
|
+
with_db { it.query_single(*, **) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Performs the given query by acquiring a database connection and running
|
|
63
|
+
# the query.
|
|
64
|
+
#
|
|
65
|
+
# @return [any] result value
|
|
66
|
+
def query_single_value(*, **)
|
|
67
|
+
with_db { it.query_single_splat(*, **) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Executes the given query by acquiring a database connection and running
|
|
71
|
+
# the query.
|
|
72
|
+
#
|
|
73
|
+
# @return [Integer] number of changed rows
|
|
74
|
+
def execute(*, **)
|
|
75
|
+
with_db { it.execute(*, **) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Closes the connection pool by removing all connections.
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
81
|
+
def close
|
|
82
|
+
while @queue.count > 0
|
|
83
|
+
db = @machine.shift(@queue)
|
|
84
|
+
db.close
|
|
85
|
+
@count -= 1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Checks out a connection from the pool.
|
|
92
|
+
#
|
|
93
|
+
# @return [Extralite::Database] database connection
|
|
94
|
+
def checkout
|
|
95
|
+
return make_db_instance if @queue.count == 0 && @count < @max_conn
|
|
96
|
+
|
|
97
|
+
@machine.shift(@queue)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Checks in a connection to the pool.
|
|
101
|
+
#
|
|
102
|
+
# @param db [Extralite::Database] database connection
|
|
103
|
+
# @return [void]
|
|
104
|
+
def checkin(db)
|
|
105
|
+
@machine.push(@queue, db)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Creates a database connection.
|
|
109
|
+
#
|
|
110
|
+
# @return [Extralite::Database] database connection
|
|
111
|
+
def make_db_instance
|
|
112
|
+
Extralite::Database.new(@fn, wal: true).tap do
|
|
113
|
+
@count += 1
|
|
114
|
+
it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require 'syntropy/collection'
|
|
4
5
|
|
|
5
6
|
module Syntropy
|
|
6
7
|
# Utilities for use in modules
|
|
@@ -113,5 +114,16 @@ module Syntropy
|
|
|
113
114
|
|
|
114
115
|
send(sym, req)
|
|
115
116
|
end
|
|
117
|
+
|
|
118
|
+
# Opens a collection with the given arguments.
|
|
119
|
+
#
|
|
120
|
+
# @param rel_root [String] relative collection root
|
|
121
|
+
# @param url_base [String] URL base
|
|
122
|
+
# @return [Syntropy::Collection] collection
|
|
123
|
+
def collection(rel_root, url_base:)
|
|
124
|
+
invalidate_on_file_change(File.join(rel_root, '**'))
|
|
125
|
+
path = normalize_import_ref(rel_root)
|
|
126
|
+
Collection.new(machine: @machine, root: File.join(@env[:app_root], path), url_base:)
|
|
127
|
+
end
|
|
116
128
|
end
|
|
117
129
|
end
|
|
@@ -91,9 +91,10 @@ module Syntropy
|
|
|
91
91
|
# @return [void]
|
|
92
92
|
def handle_error(request, err)
|
|
93
93
|
case err
|
|
94
|
+
when Errno::ECONNRESET
|
|
95
|
+
# ignore
|
|
94
96
|
when SystemCallError
|
|
95
|
-
log_error(err, '
|
|
96
|
-
false
|
|
97
|
+
log_error(err, 'Syscall error')
|
|
97
98
|
when ProtocolError
|
|
98
99
|
log_error(err, err.message)
|
|
99
100
|
respond(request, err.message, ':status' => err.http_status)
|
|
@@ -218,16 +219,16 @@ module Syntropy
|
|
|
218
219
|
# @param done [boolean] whether the response is completed
|
|
219
220
|
# @return [void]
|
|
220
221
|
def send_chunk(request, chunk, done: false)
|
|
221
|
-
|
|
222
|
-
data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
|
|
223
|
-
data << EMPTY_CHUNK if done
|
|
224
|
-
return if data.empty?
|
|
225
|
-
|
|
226
|
-
@machine.send(@fd, data, data.bytesize, SEND_FLAGS)
|
|
227
|
-
return if @done || !done
|
|
222
|
+
return if @done
|
|
228
223
|
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
len = chunk.is_a?(IO::Buffer) ? chunk.size : chunk.bytesize
|
|
225
|
+
if !done
|
|
226
|
+
@io.write(len.to_s(16),"\r\n", chunk, "\r\n")
|
|
227
|
+
else
|
|
228
|
+
@io.write(len.to_s(16),"\r\n", chunk, "\r\n0\r\n\r\n")
|
|
229
|
+
@logger&.info(request: request, response_headers: @response_headers)
|
|
230
|
+
@done = true
|
|
231
|
+
end
|
|
231
232
|
end
|
|
232
233
|
|
|
233
234
|
# Finishes the response to the current request. If no headers were sent,
|
|
@@ -257,25 +258,12 @@ module Syntropy
|
|
|
257
258
|
env[:headers] = cache_headers
|
|
258
259
|
end
|
|
259
260
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return respond(req, buf, env[:headers])
|
|
267
|
-
elsif res == 0
|
|
268
|
-
return finish(req)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
if !headers_sent
|
|
272
|
-
send_headers(req, env[:headers])
|
|
273
|
-
headers_sent = true
|
|
274
|
-
end
|
|
275
|
-
done = res < maxlen
|
|
276
|
-
send_chunk(req, buf, done: done)
|
|
277
|
-
return if done
|
|
278
|
-
end
|
|
261
|
+
io = @machine.io(fd, :file)
|
|
262
|
+
send_headers(req, env[:headers])
|
|
263
|
+
io.read_each { send_chunk(req, it) }
|
|
264
|
+
finish(req)
|
|
265
|
+
ensure
|
|
266
|
+
@machine.close_async(fd) if fd
|
|
279
267
|
end
|
|
280
268
|
|
|
281
269
|
# Closes the connection.
|
data/lib/syntropy/json_api.rb
CHANGED
|
@@ -25,11 +25,6 @@ module Syntropy
|
|
|
25
25
|
':status' => status,
|
|
26
26
|
'Content-Type' => 'application/json'
|
|
27
27
|
)
|
|
28
|
-
rescue => e
|
|
29
|
-
puts '*' * 40
|
|
30
|
-
p e
|
|
31
|
-
p e.backtrace.join
|
|
32
|
-
puts
|
|
33
28
|
end
|
|
34
29
|
|
|
35
30
|
private
|
|
@@ -39,20 +34,16 @@ module Syntropy
|
|
|
39
34
|
# @param req [Syntropy::Request]
|
|
40
35
|
def __invoke__(req)
|
|
41
36
|
q = req.validate_param(:q, String).to_sym
|
|
42
|
-
|
|
37
|
+
case req.method
|
|
43
38
|
when 'get'
|
|
44
|
-
__invoke_get__(q, req)
|
|
39
|
+
response = __invoke_get__(q, req)
|
|
45
40
|
when 'post'
|
|
46
|
-
__invoke_post__(q, req)
|
|
41
|
+
response = __invoke_post__(q, req)
|
|
47
42
|
else
|
|
48
43
|
raise Syntropy::Error.method_not_allowed
|
|
49
44
|
end
|
|
50
45
|
[{ status: 'OK', response: response }, HTTP::OK]
|
|
51
46
|
rescue StandardError => e
|
|
52
|
-
if !e.is_a?(Syntropy::Error)
|
|
53
|
-
p e
|
|
54
|
-
p e.backtrace
|
|
55
|
-
end
|
|
56
47
|
__error_response__(e)
|
|
57
48
|
end
|
|
58
49
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Syntropy
|
|
4
|
+
# The KVStore class implements an SQLite-backed key-value store
|
|
5
|
+
class KVStore
|
|
6
|
+
attr_reader :table_name
|
|
7
|
+
|
|
8
|
+
def initialize(db, table_name)
|
|
9
|
+
@table_name = table_name
|
|
10
|
+
setup_queries
|
|
11
|
+
apply_schema(db)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(db, key)
|
|
15
|
+
db.query_single_splat(@sql_get, key)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set(db, key, value)
|
|
19
|
+
db.execute(@sql_set, key, value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def setex(db, key, value, ttl)
|
|
23
|
+
db.execute(@sql_setex, key, value, ttl ? Time.now.to_f + ttl : nil)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sweep(db)
|
|
27
|
+
db.execute(@sql_sweep, Time.now.to_f)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def apply_schema(db)
|
|
33
|
+
db.execute <<~SQL
|
|
34
|
+
create table if not exists #{@table_name} (key text primary key, value, expires float);
|
|
35
|
+
create index if not exists idx_#{@table_name}_expires on #{@table_name} (expires) where expires is not null;
|
|
36
|
+
SQL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def setup_queries
|
|
40
|
+
@sql_get = "select value from #{@table_name} where key = ?"
|
|
41
|
+
|
|
42
|
+
@sql_set = <<~SQL
|
|
43
|
+
insert into #{@table_name} (key, value)
|
|
44
|
+
values($1, $2)
|
|
45
|
+
on conflict (key) do update set value = $2, expires = null
|
|
46
|
+
SQL
|
|
47
|
+
|
|
48
|
+
@sql_setex = <<~SQL
|
|
49
|
+
insert into #{@table_name} (key, value, expires)
|
|
50
|
+
values($1, $2, $3)
|
|
51
|
+
on conflict (key) do update set value = $2, expires = $3
|
|
52
|
+
SQL
|
|
53
|
+
|
|
54
|
+
@sql_sweep = "delete from #{@table_name} where expires < ?"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/syntropy/logger.rb
CHANGED
|
@@ -80,49 +80,40 @@ module Syntropy
|
|
|
80
80
|
# @param o [Hash] raw entry
|
|
81
81
|
# @return [Hash] log entry
|
|
82
82
|
def make_entry(level, o)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
t = Time.now
|
|
84
|
+
entry = o.merge(
|
|
85
|
+
level: level.to_s,
|
|
86
|
+
ts: t.to_i,
|
|
87
|
+
ts_s: t.iso8601,
|
|
88
|
+
message: o[:message]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
entry.merge!(error_info(error)) if (error = o.delete(:error))
|
|
92
|
+
entry.merge!(request_info(request)) if (request = o.delete(:request))
|
|
93
|
+
|
|
94
|
+
entry
|
|
90
95
|
end
|
|
91
96
|
|
|
92
|
-
#
|
|
97
|
+
# Extracts error info from given error object
|
|
93
98
|
#
|
|
94
|
-
# @param
|
|
95
|
-
# @
|
|
96
|
-
|
|
97
|
-
def make_error_entry(level, o)
|
|
98
|
-
err = o[:error]
|
|
99
|
-
t = Time.now
|
|
99
|
+
# @param error [Exception] exception
|
|
100
|
+
# @return [Hash] error info
|
|
101
|
+
def error_info(error)
|
|
100
102
|
{
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}.merge(o).merge(
|
|
105
|
-
error: "#{err.class}: #{err.message}",
|
|
106
|
-
backtrace: err.backtrace
|
|
107
|
-
)
|
|
103
|
+
error: "#{error.class}: #{error.message}",
|
|
104
|
+
backtrace: error.backtrace
|
|
105
|
+
}
|
|
108
106
|
end
|
|
109
107
|
|
|
110
|
-
#
|
|
108
|
+
# Extracts request info from given request object
|
|
111
109
|
#
|
|
112
|
-
# @param
|
|
113
|
-
# @
|
|
114
|
-
|
|
115
|
-
def make_request_entry(level, o)
|
|
116
|
-
request = o[:request]
|
|
110
|
+
# @param request [Syntropy::Request] request
|
|
111
|
+
# @return [Hash] request info
|
|
112
|
+
def request_info(request)
|
|
117
113
|
request_headers = request.headers
|
|
118
|
-
response_headers =
|
|
114
|
+
response_headers = request.response_headers
|
|
119
115
|
elapsed = monotonic_clock - request.start_stamp
|
|
120
|
-
t = Time.now
|
|
121
116
|
{
|
|
122
|
-
level: level.to_s,
|
|
123
|
-
ts: t.to_i,
|
|
124
|
-
ts_s: t.iso8601,
|
|
125
|
-
message: o[:message] || 'HTTP request done',
|
|
126
117
|
client_ip: request.forwarded_for || '?',
|
|
127
118
|
http_method: request_headers[':method'].upcase,
|
|
128
119
|
user_agent: request_headers['user-agent'],
|
|
@@ -132,20 +123,6 @@ module Syntropy
|
|
|
132
123
|
}
|
|
133
124
|
end
|
|
134
125
|
|
|
135
|
-
# Makes a request log entry.
|
|
136
|
-
#
|
|
137
|
-
# @param level [Symbol] log level
|
|
138
|
-
# @param o [Hash] input entry
|
|
139
|
-
# @return [Hash] output entry
|
|
140
|
-
def make_hash_entry(level, hash)
|
|
141
|
-
t = Time.now
|
|
142
|
-
{
|
|
143
|
-
level: level.to_s,
|
|
144
|
-
ts: t.to_i,
|
|
145
|
-
ts_s: t.iso8601
|
|
146
|
-
}.merge(hash)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
126
|
# Returns the monotonic clock.
|
|
150
127
|
#
|
|
151
128
|
# @return [Float]
|
data/lib/syntropy/markdown.rb
CHANGED
|
@@ -137,7 +137,7 @@ module Syntropy
|
|
|
137
137
|
# @return [Array<atts, md>] Array containing attributes and clean markdown
|
|
138
138
|
def parse_md(atts, md)
|
|
139
139
|
md = parse_content(atts, md)
|
|
140
|
-
[atts, md]
|
|
140
|
+
[atts, md.chomp]
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
# Creates a Markdown file contoroller for the given environment,
|