legion-data 1.10.9 → 1.10.28
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 +101 -0
- data/README.md +76 -4
- data/lib/legion/data/archival.rb +48 -49
- data/lib/legion/data/archiver.rb +28 -26
- data/lib/legion/data/audit_log_hash_chain.rb +3 -1
- data/lib/legion/data/audit_record.rb +12 -9
- data/lib/legion/data/connection.rb +113 -98
- data/lib/legion/data/connection_pool.rb +83 -0
- data/lib/legion/data/database_lock.rb +22 -0
- data/lib/legion/data/encryption/key_provider.rb +1 -2
- data/lib/legion/data/event_store.rb +6 -5
- data/lib/legion/data/local.rb +16 -9
- data/lib/legion/data/migrations/137_preserve_task_archive_fields.rb +37 -0
- data/lib/legion/data/migrations/138_add_append_claims.rb +11 -0
- data/lib/legion/data/model.rb +19 -0
- data/lib/legion/data/models/audit_log.rb +34 -2
- data/lib/legion/data/retention.rb +42 -11
- data/lib/legion/data/rls.rb +11 -5
- data/lib/legion/data/spool.rb +36 -16
- data/lib/legion/data/version.rb +1 -1
- data/lib/legion/data.rb +59 -24
- metadata +5 -1
|
@@ -12,24 +12,36 @@ module Legion
|
|
|
12
12
|
class << self
|
|
13
13
|
include Legion::Logging::Helper
|
|
14
14
|
|
|
15
|
-
def archive_old_records(table:, date_column: nil, archive_after_days: DEFAULT_ARCHIVE_AFTER_DAYS)
|
|
15
|
+
def archive_old_records(table:, date_column: nil, archive_after_days: DEFAULT_ARCHIVE_AFTER_DAYS, batch_size: 1000)
|
|
16
16
|
db = Legion::Data.connection
|
|
17
17
|
return { archived: 0, table: table } unless db
|
|
18
|
+
raise ArgumentError, 'batch_size must be a positive Integer' unless batch_size.is_a?(Integer) && batch_size.positive?
|
|
18
19
|
|
|
19
20
|
date_column = resolve_date_column(table, date_column)
|
|
20
21
|
cutoff = Time.now - (archive_after_days * 86_400)
|
|
21
22
|
archive_table = archive_table_name(table)
|
|
23
|
+
primary_keys = db.schema(table).filter_map { |name, info| name if info[:primary_key] }
|
|
24
|
+
raise Sequel::Error, "#{table} needs a primary key for bounded retention" if primary_keys.empty?
|
|
22
25
|
|
|
23
26
|
ensure_archive_table!(db, table, archive_table)
|
|
24
27
|
|
|
25
28
|
count = 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
loop do
|
|
30
|
+
archived = db.transaction(savepoint: true, **(db.database_type == :sqlite ? { mode: :immediate } : {})) do
|
|
31
|
+
records = db[table].where(Sequel.identifier(date_column) < cutoff).order(*primary_keys).limit(batch_size)
|
|
32
|
+
records = records.for_update unless db.database_type == :sqlite
|
|
33
|
+
rows = records.all
|
|
34
|
+
next 0 if rows.empty?
|
|
35
|
+
|
|
36
|
+
db[archive_table].multi_insert(rows)
|
|
37
|
+
deleted = delete_archived_rows(db: db, table: table, rows: rows, primary_keys: primary_keys)
|
|
38
|
+
raise Sequel::Error, "Retention copied #{rows.length} rows but deleted #{deleted} from #{table}" unless deleted == rows.length
|
|
39
|
+
|
|
40
|
+
deleted
|
|
32
41
|
end
|
|
42
|
+
break if archived.zero?
|
|
43
|
+
|
|
44
|
+
count += archived
|
|
33
45
|
end
|
|
34
46
|
|
|
35
47
|
log.info "Archived #{count} row(s) from #{table}" if count.positive?
|
|
@@ -113,18 +125,37 @@ module Legion
|
|
|
113
125
|
end
|
|
114
126
|
end
|
|
115
127
|
|
|
128
|
+
def delete_archived_rows(db:, table:, rows:, primary_keys:)
|
|
129
|
+
if primary_keys.one?
|
|
130
|
+
key = primary_keys.first
|
|
131
|
+
return db[table].where(key => rows.map { |row| row.fetch(key) }).delete
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# SQLite expands composite-key predicates into OR expressions with a depth limit.
|
|
135
|
+
identity_batch_size = db.database_type == :sqlite ? 100 : rows.length
|
|
136
|
+
rows.each_slice(identity_batch_size).sum do |batch|
|
|
137
|
+
db[table].where(primary_keys => batch.map { |row| row.values_at(*primary_keys) }).delete
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
116
141
|
def ensure_archive_table!(db, source_table, archive_table)
|
|
117
142
|
return if db.table_exists?(archive_table)
|
|
118
143
|
|
|
119
144
|
source_schema = db.schema(source_table).to_h
|
|
120
145
|
|
|
121
146
|
log.info "Creating archive table #{archive_table} from #{source_table}"
|
|
122
|
-
db.
|
|
123
|
-
|
|
124
|
-
|
|
147
|
+
db.transaction(savepoint: true) do
|
|
148
|
+
db.create_table?(archive_table) do
|
|
149
|
+
source_schema.each do |col_name, col_info|
|
|
150
|
+
column col_name, col_info[:db_type]
|
|
151
|
+
end
|
|
152
|
+
DateTime :archived_at, default: Sequel::CURRENT_TIMESTAMP
|
|
125
153
|
end
|
|
126
|
-
DateTime :archived_at, default: Sequel::CURRENT_TIMESTAMP
|
|
127
154
|
end
|
|
155
|
+
rescue Sequel::UniqueConstraintViolation => e
|
|
156
|
+
raise unless db.table_exists?(archive_table)
|
|
157
|
+
|
|
158
|
+
handle_exception(e, level: :debug, handled: true, operation: :concurrent_archive_creation, table: archive_table)
|
|
128
159
|
end
|
|
129
160
|
end
|
|
130
161
|
end
|
data/lib/legion/data/rls.rb
CHANGED
|
@@ -47,11 +47,17 @@ module Legion
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def with_tenant(tenant_id)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
return yield unless rls_enabled?
|
|
51
|
+
|
|
52
|
+
Legion::Data.connection.synchronize do
|
|
53
|
+
previous = current_tenant
|
|
54
|
+
begin
|
|
55
|
+
assign_tenant(tenant_id)
|
|
56
|
+
yield
|
|
57
|
+
ensure
|
|
58
|
+
previous ? assign_tenant(previous) : reset_tenant
|
|
59
|
+
end
|
|
60
|
+
end
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
end
|
data/lib/legion/data/spool.rb
CHANGED
|
@@ -61,8 +61,10 @@ module Legion
|
|
|
61
61
|
|
|
62
62
|
def read(sub_namespace)
|
|
63
63
|
sorted_files(sub_namespace).each_with_object([]) do |path, events|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
with_locked_event(path) do |file|
|
|
65
|
+
event = load_event_file(file, path, sub_namespace)
|
|
66
|
+
events << event if event
|
|
67
|
+
end
|
|
66
68
|
end
|
|
67
69
|
rescue StandardError => e
|
|
68
70
|
handle_exception(e, level: :error, handled: false, operation: :spool_read, sub_namespace: sub_namespace)
|
|
@@ -72,13 +74,16 @@ module Legion
|
|
|
72
74
|
def flush(sub_namespace)
|
|
73
75
|
count = 0
|
|
74
76
|
path = nil
|
|
75
|
-
sorted_files(sub_namespace).each do |
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
sorted_files(sub_namespace).each do |entry_path|
|
|
78
|
+
path = entry_path
|
|
79
|
+
with_locked_event(path) do |file|
|
|
80
|
+
event = load_event_file(file, path, sub_namespace)
|
|
81
|
+
next unless event
|
|
82
|
+
|
|
83
|
+
yield event
|
|
84
|
+
File.delete(path)
|
|
85
|
+
count += 1
|
|
86
|
+
end
|
|
82
87
|
end
|
|
83
88
|
log.info "Spool drained #{count} item(s) from #{sub_namespace}" if count.positive?
|
|
84
89
|
count
|
|
@@ -95,8 +100,10 @@ module Legion
|
|
|
95
100
|
dir = sub_dir(sub_namespace)
|
|
96
101
|
return unless Dir.exist?(dir)
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
sorted_files(sub_namespace).each do |path|
|
|
104
|
+
with_locked_event(path) { |_file| File.delete(path) }
|
|
105
|
+
end
|
|
106
|
+
log.info "Spool cleared unclaimed events from #{sub_namespace}"
|
|
100
107
|
rescue StandardError => e
|
|
101
108
|
handle_exception(e, level: :error, handled: false, operation: :spool_clear, sub_namespace: sub_namespace)
|
|
102
109
|
raise
|
|
@@ -115,11 +122,24 @@ module Legion
|
|
|
115
122
|
Dir.glob(File.join(dir, '*.json'), sort: true)
|
|
116
123
|
end
|
|
117
124
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
def with_locked_event(path)
|
|
126
|
+
file = begin
|
|
127
|
+
File.open(path, 'rb')
|
|
128
|
+
rescue Errno::ENOENT => e
|
|
129
|
+
log.debug("spool event file not found: #{path}: #{e.message}")
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
return unless file
|
|
133
|
+
return unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
134
|
+
return unless File.identical?(file, path)
|
|
135
|
+
|
|
136
|
+
yield file
|
|
137
|
+
ensure
|
|
138
|
+
file&.close
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def load_event_file(file, path, sub_namespace)
|
|
142
|
+
::JSON.parse(file.read, symbolize_names: true)
|
|
123
143
|
rescue ::JSON::ParserError, EOFError, ArgumentError => e
|
|
124
144
|
quarantine_corrupt_file(path, sub_namespace, e)
|
|
125
145
|
nil
|
data/lib/legion/data/version.rb
CHANGED
data/lib/legion/data.rb
CHANGED
|
@@ -48,6 +48,11 @@ module Legion
|
|
|
48
48
|
setup_cache
|
|
49
49
|
setup_local
|
|
50
50
|
log.info 'Legion::Data setup complete'
|
|
51
|
+
rescue StandardError, LoadError => e
|
|
52
|
+
shutdown
|
|
53
|
+
Legion::Data::Connection.sequel = nil
|
|
54
|
+
handle_exception(e, level: :fatal, handled: false, operation: :setup)
|
|
55
|
+
raise
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def setup_local
|
|
@@ -62,10 +67,19 @@ module Legion
|
|
|
62
67
|
|
|
63
68
|
def setup_global
|
|
64
69
|
connection_setup
|
|
70
|
+
Legion::Settings[:data][:connected] = false
|
|
71
|
+
Legion::Settings[:data][:migrations][:ran] = false
|
|
72
|
+
Legion::Settings[:data][:models][:loaded] = false
|
|
65
73
|
migrate
|
|
66
74
|
load_models
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
Legion::Settings[:data][:connected] = true
|
|
76
|
+
rescue StandardError, LoadError => e
|
|
77
|
+
Legion::Data::Connection.shutdown
|
|
78
|
+
Legion::Data::Connection.sequel = nil
|
|
79
|
+
Legion::Settings[:data][:migrations][:ran] = false
|
|
80
|
+
Legion::Settings[:data][:models][:loaded] = false
|
|
81
|
+
handle_exception(e, level: :fatal, handled: false, operation: :setup_global)
|
|
82
|
+
raise
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def setup_cache
|
|
@@ -119,36 +133,56 @@ module Legion
|
|
|
119
133
|
def can_write?(table_name)
|
|
120
134
|
return false unless connected?
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
database = connection
|
|
137
|
+
return false unless database
|
|
138
|
+
return true if database.database_type == :sqlite
|
|
139
|
+
|
|
140
|
+
cache = @write_privileges ||= ObjectSpace::WeakKeyMap.new
|
|
141
|
+
database.synchronize do |session|
|
|
142
|
+
privileges = cache[session] ||= {}
|
|
143
|
+
return privileges[table_name] if privileges.key?(table_name)
|
|
144
|
+
|
|
145
|
+
privileges[table_name] = case database.database_type
|
|
146
|
+
when :postgres
|
|
147
|
+
database.fetch("SELECT has_table_privilege(current_user, ?, 'INSERT') AS can", table_name.to_s)
|
|
148
|
+
.server(:default).first[:can] == true
|
|
149
|
+
when :mysql
|
|
150
|
+
database.fetch('EXPLAIN INSERT INTO ? () VALUES ()', Sequel.identifier(table_name)).server(:default).all
|
|
151
|
+
true
|
|
152
|
+
else
|
|
153
|
+
raise Sequel::Error, "Unsupported privilege adapter: #{database.database_type}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
131
156
|
rescue StandardError => e
|
|
132
157
|
handle_exception(e, level: :warn, handled: true, operation: :can_write?, table: table_name)
|
|
133
|
-
@write_privileges[table_name] = false if @write_privileges
|
|
134
158
|
false
|
|
135
159
|
end
|
|
136
160
|
|
|
137
161
|
def can_read?(table_name)
|
|
138
162
|
return false unless connected?
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
database = connection
|
|
165
|
+
return false unless database
|
|
166
|
+
return true if database.database_type == :sqlite
|
|
167
|
+
|
|
168
|
+
cache = @read_privileges ||= ObjectSpace::WeakKeyMap.new
|
|
169
|
+
database.synchronize do |session|
|
|
170
|
+
privileges = cache[session] ||= {}
|
|
171
|
+
return privileges[table_name] if privileges.key?(table_name)
|
|
172
|
+
|
|
173
|
+
privileges[table_name] = case database.database_type
|
|
174
|
+
when :postgres
|
|
175
|
+
database.fetch("SELECT has_table_privilege(current_user, ?, 'SELECT') AS can", table_name.to_s)
|
|
176
|
+
.server(:default).first[:can] == true
|
|
177
|
+
when :mysql
|
|
178
|
+
database.fetch('SELECT * FROM ? LIMIT 0', Sequel.identifier(table_name)).server(:default).all
|
|
179
|
+
true
|
|
180
|
+
else
|
|
181
|
+
raise Sequel::Error, "Unsupported privilege adapter: #{database.database_type}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
149
184
|
rescue StandardError => e
|
|
150
185
|
handle_exception(e, level: :warn, handled: true, operation: :can_read?, table: table_name)
|
|
151
|
-
@read_privileges[table_name] = false if @read_privileges
|
|
152
186
|
false
|
|
153
187
|
end
|
|
154
188
|
|
|
@@ -191,8 +225,9 @@ module Legion
|
|
|
191
225
|
|
|
192
226
|
def shutdown
|
|
193
227
|
Legion::Data::Local.shutdown if defined?(Legion::Data::Local) && Legion::Data::Local.connected?
|
|
194
|
-
Legion::Data::Connection.shutdown
|
|
195
|
-
log.info 'Legion::Data shutdown complete'
|
|
228
|
+
closed = Legion::Data::Connection.shutdown
|
|
229
|
+
log.info 'Legion::Data shutdown complete' if closed
|
|
230
|
+
closed
|
|
196
231
|
end
|
|
197
232
|
|
|
198
233
|
private
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-data
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.10.
|
|
4
|
+
version: 1.10.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -126,6 +126,8 @@ files:
|
|
|
126
126
|
- lib/legion/data/audit_record.rb
|
|
127
127
|
- lib/legion/data/auth_failure_handler.rb
|
|
128
128
|
- lib/legion/data/connection.rb
|
|
129
|
+
- lib/legion/data/connection_pool.rb
|
|
130
|
+
- lib/legion/data/database_lock.rb
|
|
129
131
|
- lib/legion/data/encryption/cipher.rb
|
|
130
132
|
- lib/legion/data/encryption/key_provider.rb
|
|
131
133
|
- lib/legion/data/encryption/sequel_plugin.rb
|
|
@@ -284,6 +286,8 @@ files:
|
|
|
284
286
|
- lib/legion/data/migrations/134_add_route_attempt_columns.rb
|
|
285
287
|
- lib/legion/data/migrations/135_add_llm_context_token_accounting.rb
|
|
286
288
|
- lib/legion/data/migrations/136_drop_llm_messages_seq_unique.rb
|
|
289
|
+
- lib/legion/data/migrations/137_preserve_task_archive_fields.rb
|
|
290
|
+
- lib/legion/data/migrations/138_add_append_claims.rb
|
|
287
291
|
- lib/legion/data/model.rb
|
|
288
292
|
- lib/legion/data/models/apollo/access_log.rb
|
|
289
293
|
- lib/legion/data/models/apollo/entries.rb
|