Skip to content

Commit 7f80ffe

Browse files
committed
feat: added updated_at tracking for tiles and incremental reconstruction
- added migration 004_add_updated_at_to_tiles.rb with updated_at field and trigger for automatic updates - updated tiles table schema in database_manager.rb with updated_at field and idx_tiles_zoom_updated index - update updated_at on tile insert/update in config.ru (fetch_with_lock) and background_tile_loader.rb (fetch_tile) - implemented incremental processing in tile_reconstructor.rb based on updated_at for gap filling optimization - added save_last_run_timestamp and get_last_run_timestamp methods to track last reconstruction run - filter tiles by updated_at > last_run_time or generated = -5 to process only changed tiles - update updated_at when generating parent tiles and marking grandparent for regeneration
1 parent e692c40 commit 7f80ffe

File tree

7 files changed

+92
-23
lines changed

7 files changed

+92
-23
lines changed

docker/ruby/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,6 @@ ENV RUBY_YJIT_ENABLE=1 \
6262
WORKDIR /app
6363

6464
# --start_period=5s (Unknown flag: start_period)
65-
HEALTHCHECK --interval=15s --timeout=2s --retries=3 CMD curl --fail http://127.0.0.1:$PORT/healthcheck || exit 1
65+
# HEALTHCHECK --interval=15s --timeout=2s --retries=3 CMD curl --fail http://127.0.0.1:$PORT/healthcheck || exit 1
6666
CMD bundle exec rackup -o 0.0.0.0 -p $PORT -s falcon
6767
# docker-compose build --progress plain

src/background_tile_loader.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,16 @@ def fetch_tile(x, y, z)
279279

280280
@route[:db][:tiles].insert_conflict(
281281
target: [:zoom_level, :tile_column, :tile_row],
282-
update: { tile_data: Sequel[:excluded][:tile_data] }
282+
update: {
283+
tile_data: Sequel[:excluded][:tile_data],
284+
updated_at: Sequel.lit("datetime('now', 'utc')")
285+
}
283286
).insert(
284287
zoom_level: z,
285288
tile_column: x,
286289
tile_row: tms_y(z, y),
287-
tile_data: Sequel.blob(data)
290+
tile_data: Sequel.blob(data),
291+
updated_at: Sequel.lit("datetime('now', 'utc')")
288292
)
289293

290294
if @route.dig(:gap_filling, :enabled)

src/config.ru

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,13 @@ helpers do
306306
end
307307

308308
route[:db][:tiles].insert_conflict(target: [:zoom_level, :tile_column, :tile_row],
309-
update: { tile_data: Sequel[:excluded][:tile_data] })
309+
update: {
310+
tile_data: Sequel[:excluded][:tile_data],
311+
updated_at: Sequel.lit("datetime('now', 'utc')")
312+
})
310313
.insert(zoom_level: z, tile_column: x, tile_row: tms,
311-
tile_data: Sequel.blob(result[:data]))
312-
313-
# if route.dig(:gap_filling, :enabled) && z >= route.dig(:gap_filling, :source_real_minzoom)
314-
# route[:reconstructor]&.mark_parent_for_new_child(route[:db], z, x, tms, route[:minzoom])
315-
# end
314+
tile_data: Sequel.blob(result[:data]),
315+
updated_at: Sequel.lit("datetime('now', 'utc')"))
316316

317317
result[:data]
318318
end

src/database_manager.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,12 @@ def create_tables(db)
9898
Integer :tile_row, null:false
9999
File :tile_data, null:false
100100
Integer :generated, default: 0 # 0=original, 1=generated, 2=needs_regeneration
101+
DateTime :updated_at, default: Sequel.lit("datetime('now', 'utc')")
101102
unique [:zoom_level,:tile_column,:tile_row], name: :tile_index
102103
index :zoom_level, name: :idx_tiles_zoom_level
103104
index [:zoom_level, Sequel.function(:length, :tile_data)], name: :idx_tiles_zoom_size
104105
index [:zoom_level, :generated], name: :idx_tiles_zoom_generated
106+
index [:zoom_level, :updated_at], name: :idx_tiles_zoom_updated
105107
}
106108
db.create_table?(:misses){
107109
Integer :zoom_level, null: false
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Sequel.migration do
2+
up do
3+
next unless table_exists?(:tiles)
4+
5+
columns = schema(:tiles).map(&:first)
6+
unless columns.include?(:updated_at)
7+
alter_table(:tiles) do
8+
add_column :updated_at, DateTime
9+
end
10+
end
11+
12+
run "UPDATE tiles SET updated_at = datetime('now', 'utc') WHERE updated_at IS NULL"
13+
14+
begin
15+
add_index :tiles, [:zoom_level, :updated_at], name: :idx_tiles_zoom_updated
16+
rescue Sequel::DatabaseError => e
17+
raise unless e.message.include?('already exists')
18+
end
19+
end
20+
21+
down do
22+
next unless table_exists?(:tiles)
23+
24+
raise Sequel::Error, 'Irreversible migration: dropping updated_at column would require table recreation'
25+
end
26+
end
27+

src/tile_reconstructor.rb

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,28 @@ def status
7878

7979
private
8080

81+
def save_last_run_timestamp(db)
82+
db[:metadata].insert_conflict(
83+
target: :name,
84+
update: { value: Sequel[:excluded][:value] }
85+
).insert(
86+
name: 'reconstruction_last_run',
87+
value: Time.now.utc.iso8601
88+
)
89+
rescue => e
90+
LOGGER.warn("TileReconstructor: failed to save last run timestamp: #{e.message}")
91+
end
92+
93+
def get_last_run_timestamp(db)
94+
timestamp_str = db[:metadata].where(name: 'reconstruction_last_run').get(:value)
95+
return nil unless timestamp_str
96+
97+
Time.parse(timestamp_str)
98+
rescue => e
99+
LOGGER.warn("TileReconstructor: failed to get last run timestamp: #{e.message}")
100+
nil
101+
end
102+
81103
# Parses schedule time from config, returns {hour:, minute:} or nil
82104
def parse_schedule_time
83105
time_str = @route.dig(:gap_filling, :schedule, :time) || @route.dig(:gap_filling, :schedule, 'time')
@@ -111,34 +133,38 @@ def run_reconstruction
111133

112134
downsample_opts = build_downsample_opts(@route)
113135

114-
LOGGER.info("TileReconstructor: starting gap filling for #{@source_name} from zoom #{start_zoom} to #{minzoom}")
136+
last_run_time = get_last_run_timestamp(db)
137+
LOGGER.info("TileReconstructor: starting #{last_run_time ? "incremental (last run: #{last_run_time.iso8601})" : "full (first run)"} gap filling for #{@source_name} from zoom #{start_zoom} to #{minzoom}")
115138

116139
start_zoom.downto(minzoom) do |z|
117140
break unless @running
118141

119142
begin
120-
process_zoom_level(z, db, downsample_opts, minzoom, maxzoom)
143+
process_zoom_level(z, db, downsample_opts, minzoom, maxzoom, last_run_time)
121144
rescue => e
122145
LOGGER.error("TileReconstructor: failed to process zoom #{z} for #{@source_name}: #{e.message}")
123146
LOGGER.debug("TileReconstructor: backtrace: #{e.backtrace.join("\n")}")
124147
end
125148
end
126149

150+
save_last_run_timestamp(db)
151+
127152
LOGGER.info("TileReconstructor: gap filling completed for #{@source_name}")
128153
end
129154

130155
otl_def :run_reconstruction
131156

132-
def process_zoom_level(z, db, downsample_opts, minzoom, maxzoom)
157+
def process_zoom_level(z, db, downsample_opts, minzoom, maxzoom, last_run_time = nil)
133158
parent_z = z - 1
134159
return if parent_z < minzoom
135160

136161
LOGGER.info("TileReconstructor: processing zoom #{z} -> #{parent_z}")
137162

138-
all_tiles_z = load_tiles_for_zoom(z, db)
163+
all_tiles_z = load_tiles_for_zoom(z, db, last_run_time)
139164
return if all_tiles_z.empty?
140165

141-
LOGGER.info("TileReconstructor: loaded #{all_tiles_z.size} tiles for zoom #{z}")
166+
log_msg = last_run_time ? "loaded #{all_tiles_z.size} tiles for zoom #{z} (filtered by timestamp)" : "loaded #{all_tiles_z.size} tiles for zoom #{z}"
167+
LOGGER.info("TileReconstructor: #{log_msg}")
142168

143169
parent_coords_set = calculate_parent_coords(all_tiles_z)
144170
LOGGER.info("TileReconstructor: calculated #{parent_coords_set.size} unique parents for zoom #{parent_z}")
@@ -182,11 +208,19 @@ def process_parent(parent_coords, z, parent_z, db, downsample_opts, minzoom)
182208
generate_and_save_parent(px, py, parent_z, children_data_array, used_count, downsample_opts, db, grandparent_tile, parent_tile, parent_validation)
183209
end
184210

185-
# Loads all tiles for a zoom level (coordinates only, no blob)
186-
def load_tiles_for_zoom(z, db)
187-
db[:tiles].where(zoom_level: z)
188-
.select(:tile_column, :tile_row, :generated)
189-
.to_a
211+
def load_tiles_for_zoom(z, db, last_run_time = nil)
212+
query = db[:tiles].where(zoom_level: z)
213+
214+
if last_run_time
215+
last_run_utc = last_run_time.utc
216+
conditions = [
217+
Sequel[:updated_at] > last_run_utc,
218+
Sequel[:generated] => -5
219+
]
220+
query = query.where { Sequel.|(*conditions) }
221+
end
222+
223+
query.select(:tile_column, :tile_row, :generated).to_a
190224
end
191225

192226
def calculate_parent_coords(all_tiles_z)
@@ -323,14 +357,16 @@ def generate_and_save_parent(px, py, parent_z, children_data_array, used_count,
323357
target: [:zoom_level, :tile_column, :tile_row],
324358
update: {
325359
tile_data: Sequel[:excluded][:tile_data],
326-
generated: Sequel[:excluded][:generated]
360+
generated: Sequel[:excluded][:generated],
361+
updated_at: Sequel.lit("datetime('now', 'utc')")
327362
}
328363
).insert(
329364
zoom_level: parent_z,
330365
tile_column: px,
331366
tile_row: py,
332367
tile_data: Sequel.blob(new_data),
333-
generated: used_count
368+
generated: used_count,
369+
updated_at: Sequel.lit("datetime('now', 'utc')")
334370
)
335371

336372
mark_grandparent_for_regeneration(grandparent_tile, db) if grandparent_tile && grandparent_tile[:generated] != 0
@@ -347,7 +383,7 @@ def mark_grandparent_for_regeneration(grandparent_tile, db)
347383
zoom_level: grandparent_tile[:zoom_level],
348384
tile_column: grandparent_tile[:tile_column],
349385
tile_row: grandparent_tile[:tile_row]
350-
).update(generated: -5)
386+
).update(generated: -5, updated_at: Sequel.lit("datetime('now', 'utc')"))
351387
end
352388

353389
# Validates tile and returns its status

src/views/database.slim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ html
100100
.content
101101
- page, per_page = (params[:page] || 1).to_i, 50
102102
- offset = (page - 1) * per_page
103-
- table_data = { 'tiles' => { title: 'Tiles Table', cols: ['Zoom Level', 'Tile Column', 'Tile Row', 'Data Size', 'Generated'], data: @route[:db][:tiles].limit(per_page).offset(offset), format: ->(r) { [r[:zoom_level], r[:tile_column], r[:tile_row], "#{(r[:tile_data].bytesize / 1024.0).round(1)} KB", r[:generated]] } }, 'misses' => { title: 'Misses Table', cols: ['Zoom Level', 'Tile Column', 'Tile Row', 'Timestamp', 'Status', 'Reason', 'Details', 'Response Size'], data: @route[:db][:misses].order(Sequel.desc(:ts)).limit(per_page).offset(offset), format: ->(r) { [r[:zoom_level], r[:tile_column], r[:tile_row], Time.at(r[:ts]).strftime("%Y-%m-%d %H:%M:%S"), r[:status] || 'N/A', r[:reason] || 'N/A', r[:details] || 'N/A', r[:response_body] ? "#{(r[:response_body].bytesize / 1024.0).round(1)} KB" : 'N/A'] } }, 'metadata' => { title: 'Metadata Table', cols: ['Name', 'Value'], data: @route[:db][:metadata].limit(per_page).offset(offset), format: ->(r) { [r[:name], r[:value]] } }, 'tile_scan_progress' => { title: 'Autoscan Progress Table', cols: ['Source', 'Zoom Level', 'Last X', 'Last Y', 'Tiles Today', 'Last Scan Date', 'Status'], data: (@route[:db].table_exists?(:tile_scan_progress) ? @route[:db][:tile_scan_progress].order(:zoom_level).limit(per_page).offset(offset) : []), format: ->(r) { [r[:source], r[:zoom_level], r[:last_x], r[:last_y], r[:tiles_today], r[:last_scan_date], r[:status]] } } }
103+
- table_data = { 'tiles' => { title: 'Tiles Table', cols: ['Zoom Level', 'Tile Column', 'Tile Row', 'Data Size', 'Generated', 'Updated At'], data: @route[:db][:tiles].limit(per_page).offset(offset), format: ->(r) { [r[:zoom_level], r[:tile_column], r[:tile_row], "#{(r[:tile_data].bytesize / 1024.0).round(1)} KB", r[:generated], r[:updated_at] ? Time.parse(r[:updated_at].to_s).strftime("%Y-%m-%d %H:%M:%S") : 'N/A'] } }, 'misses' => { title: 'Misses Table', cols: ['Zoom Level', 'Tile Column', 'Tile Row', 'Timestamp', 'Status', 'Reason', 'Details', 'Response Size'], data: @route[:db][:misses].order(Sequel.desc(:ts)).limit(per_page).offset(offset), format: ->(r) { [r[:zoom_level], r[:tile_column], r[:tile_row], Time.at(r[:ts]).strftime("%Y-%m-%d %H:%M:%S"), r[:status] || 'N/A', r[:reason] || 'N/A', r[:details] || 'N/A', r[:response_body] ? "#{(r[:response_body].bytesize / 1024.0).round(1)} KB" : 'N/A'] } }, 'metadata' => { title: 'Metadata Table', cols: ['Name', 'Value'], data: @route[:db][:metadata].limit(per_page).offset(offset), format: ->(r) { [r[:name], r[:value]] } }, 'tile_scan_progress' => { title: 'Autoscan Progress Table', cols: ['Source', 'Zoom Level', 'Last X', 'Last Y', 'Tiles Today', 'Last Scan Date', 'Status'], data: (@route[:db].table_exists?(:tile_scan_progress) ? @route[:db][:tile_scan_progress].order(:zoom_level).limit(per_page).offset(offset) : []), format: ->(r) { [r[:source], r[:zoom_level], r[:last_x], r[:last_y], r[:tiles_today], r[:last_scan_date], r[:status]] } } }
104104
- current_data = table_data[current_table]
105105
- total_count = current_table == 'tile_scan_progress' && !@route[:db].table_exists?(:tile_scan_progress) ? 0 : @route[:db][current_table.to_sym].count
106106
- total_pages = (total_count.to_f / per_page).ceil

0 commit comments

Comments
 (0)