Module: Vehicles::Refresher
- Defined in:
- lib/vehicles/refresher.rb
Overview
Pulls the latest published dataset from the VehiclesDB data repo (config.data_url) into a local file cache (config.cache_path). This is how data fixes and new makes reach an app WITHOUT a gem upgrade — schedule ‘Vehicles.refresh!` (e.g. a daily job) and loads will prefer the cached data over the bundled snapshot.
Error-isolated by design: a failed/partial download never corrupts the cache and never raises — the gem just keeps serving whatever it already had (cached, or the bundled snapshot). Atomic write so a reader never sees a half-written file.
Constant Summary collapse
- MAX_REDIRECTS =
3
Class Method Summary collapse
-
.cached? ⇒ Boolean
True when a refreshed dataset is cached on disk.
- .cached_path ⇒ Object
-
.clear_cache! ⇒ Object
Remove the cached dataset (the gem falls back to the bundled snapshot).
- .fetch(url, timeout, redirects_left = MAX_REDIRECTS) ⇒ Object
-
.refresh! ⇒ Object
Download + cache the latest dataset.
-
.valid_payload?(body) ⇒ Boolean
A payload is only accepted if it parses and looks like our dataset, so a CDN error page / truncated download can never replace good data.
- .write_atomic(path, body) ⇒ Object
Class Method Details
.cached? ⇒ Boolean
True when a refreshed dataset is cached on disk.
36 37 38 39 |
# File 'lib/vehicles/refresher.rb', line 36 def cached? path = Vehicles.configuration.cache_path !path.to_s.empty? && File.exist?(path) end |
.cached_path ⇒ Object
41 42 43 |
# File 'lib/vehicles/refresher.rb', line 41 def cached_path cached? ? Vehicles.configuration.cache_path : nil end |
.clear_cache! ⇒ Object
Remove the cached dataset (the gem falls back to the bundled snapshot).
46 47 48 49 50 |
# File 'lib/vehicles/refresher.rb', line 46 def clear_cache! path = Vehicles.configuration.cache_path File.delete(path) if path && File.exist?(path) Vehicles.reload! end |
.fetch(url, timeout, redirects_left = MAX_REDIRECTS) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/vehicles/refresher.rb', line 65 def fetch(url, timeout, redirects_left = MAX_REDIRECTS) require "net/http" require "uri" uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" http.open_timeout = timeout http.read_timeout = timeout response = http.get(uri.request_uri, "Accept" => "application/json") case response when Net::HTTPSuccess # Net::HTTP hands back an ASCII-8BIT body (e.g. after gzip decompression); # our datasets are UTF-8 JSON, so label it correctly. Otherwise downstream # writes/parses can transcode-raise under Encoding.default_internal = UTF-8. response.body&.dup&.force_encoding(Encoding::UTF_8) when Net::HTTPRedirection return nil if redirects_left <= 0 fetch(response["location"], timeout, redirects_left - 1) end end |
.refresh! ⇒ Object
Download + cache the latest dataset. Returns true on success, false on any failure (network, non-200, unparseable/empty payload). Never raises.
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/vehicles/refresher.rb', line 22 def refresh! config = Vehicles.configuration body = fetch(config.data_url, config.refresh_timeout) return false unless valid_payload?(body) write_atomic(config.cache_path, body) Vehicles.reload! # drop the in-memory dataset so the next access uses fresh data true rescue StandardError => e Vehicles.logger&.warn("[vehicles] refresh failed: #{e.class}: #{e.}") false end |
.valid_payload?(body) ⇒ Boolean
A payload is only accepted if it parses and looks like our dataset, so a CDN error page / truncated download can never replace good data.
56 57 58 59 60 61 62 63 |
# File 'lib/vehicles/refresher.rb', line 56 def valid_payload?(body) return false if body.to_s.empty? data = JSON.parse(body) data.is_a?(Hash) && data["makes"].is_a?(Array) && !data["makes"].empty? rescue JSON::ParserError false end |
.write_atomic(path, body) ⇒ Object
89 90 91 92 93 94 95 96 97 |
# File 'lib/vehicles/refresher.rb', line 89 def write_atomic(path, body) require "fileutils" FileUtils.mkdir_p(File.dirname(path)) tmp = "#{path}.#{Process.pid}.tmp" # Binary write: persist the exact bytes, never transcode. Robust regardless # of the body's encoding or the app's Encoding.default_internal. File.binwrite(tmp, body) File.rename(tmp, path) # atomic on the same filesystem end |