Class: OpenASN::Snapshot

Inherits:
Object
  • Object
show all
Defined in:
lib/openasn/snapshot.rb

Overview

An immutable, fully-loaded dataset: canonical artifacts (both address families) + org names + Tier B overlays, plus the metadata to explain where it all came from. Snapshots are built off the hot path and swapped in with a single ivar assignment (see Dataset) — readers never see partial state, and there are no locks anywhere near lookups.

Defined Under Namespace

Classes: Family

Constant Summary collapse

SEED_DIR =
File.expand_path("data/seed", __dir__)
EMPTY_OVERLAYS =
[].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(v4_parsed, v6_parsed, orgs, overlays, manifest, origin, store) ⇒ Snapshot

Returns a new instance of Snapshot.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/openasn/snapshot.rb', line 106

def initialize(v4_parsed, v6_parsed, orgs, overlays, manifest, origin, store)
  @v4 = Family.new(base: v4_parsed[:base], vpn: v4_parsed[:vpn], dc: v4_parsed[:dc], relay: v4_parsed[:relay])
  @v6 = Family.new(base: v6_parsed[:base], vpn: v6_parsed[:vpn], dc: v6_parsed[:dc], relay: v6_parsed[:relay])
  @orgs = orgs
  @overlays = overlays.freeze
  @build_id = manifest["build_id"]
  @build_ts = v4_parsed[:build_ts]
  @loaded_at = Time.now.utc
  @origin = origin
  @record_counts = {
    base_ipv4: @v4.base.count, vpn_ipv4: @v4.vpn.count, dc_ipv4: @v4.dc.count,
    base_ipv6: @v6.base.count
  }.freeze
  @tier_b_status = build_tier_b_status(store)
  # Precomputed (family, maps_to) -> [[entry, layer], ...] index. The old
  # per-lookup filter_map was fine at 8 overlays but allocation-heavy at
  # 18+ (measured: 42us/lookup vs 15us overlay-less with 11 overlays -
  # most of the gap was these throwaway arrays, not the binary searches).
  # The snapshot is immutable, so build the index exactly once.
  @overlay_index = {}
  @overlays.each do |o|
    { ipv4: o.v4, ipv6: o.v6 }.each do |fam, layer|
      next unless layer

      (@overlay_index[[fam, o.maps_to]] ||= []) << [o, layer].freeze
    end
  end
  @overlay_index.each_value(&:freeze)
  @overlay_index.freeze
  freeze
end

Instance Attribute Details

#build_idObject (readonly)

Returns the value of attribute build_id.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def build_id
  @build_id
end

#build_tsObject (readonly)

Returns the value of attribute build_ts.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def build_ts
  @build_ts
end

#loaded_atObject (readonly)

Returns the value of attribute loaded_at.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def loaded_at
  @loaded_at
end

#orgsObject (readonly)

Returns the value of attribute orgs.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def orgs
  @orgs
end

#originObject (readonly)

Returns the value of attribute origin.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def origin
  @origin
end

#overlaysObject (readonly)

Returns the value of attribute overlays.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def overlays
  @overlays
end

#record_countsObject (readonly)

Returns the value of attribute record_counts.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def record_counts
  @record_counts
end

#tier_b_statusObject (readonly)

Returns the value of attribute tier_b_status.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def tier_b_status
  @tier_b_status
end

#v4Object (readonly)

Returns the value of attribute v4.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def v4
  @v4
end

#v6Object (readonly)

Returns the value of attribute v6.



17
18
19
# File 'lib/openasn/snapshot.rb', line 17

def v6
  @v6
end

Class Method Details

.build(config) ⇒ Object

Build from the best available data:

1. data_dir artifacts (downloaded by the updater) when present+valid
2. the gem's bundled seed otherwise (first boot, corrupted dir, …)

A corrupt downloaded artifact NEVER crashes boot — parsing happens INSIDE the fallback boundary (a truncated download that still reads fine must not take the app down); we log and fall back to the seed, and the next update cycle re-downloads. If the bundled seed itself fails to parse, the gem is broken and raising IS correct.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/openasn/snapshot.rb', line 28

def self.build(config)
  data_dir = config.data_dir
  mode = config.memory_mode

  origin = :data_dir
  begin
    v4_bytes, v6_bytes, manifest = read_artifacts(data_dir)
    raise Errno::ENOENT, "no artifacts in data_dir" unless v4_bytes

    v4_parsed, v6_parsed = parse_pair(v4_bytes, v6_bytes, mode)
  rescue StandardError => e
    unless e.is_a?(Errno::ENOENT)
      config.logger.warn("openasn: data_dir artifacts unusable (#{e.message}); falling back to bundled seed")
    end
    origin = :seed
    v4_bytes = File.binread(File.join(SEED_DIR, "openasn-ipv4.bin"))
    v6_bytes = File.binread(File.join(SEED_DIR, "openasn-ipv6.bin"))
    manifest = JSON.parse(File.read(File.join(SEED_DIR, "manifest.json")))
    v4_parsed, v6_parsed = parse_pair(v4_bytes, v6_bytes, mode)
  end

  orgs = load_orgs(data_dir, config)
  store = OverlayStore.new(data_dir)
  overlays = store.load(config.enabled_tier_b_source_ids, mode)

  new(v4_parsed, v6_parsed, orgs, overlays, manifest, origin, store)
end

.load_orgs(data_dir, config) ⇒ Object



96
97
98
99
100
101
102
103
104
# File 'lib/openasn/snapshot.rb', line 96

def self.load_orgs(data_dir, config)
  path = File.join(data_dir, "openasn-orgs.bin")
  return nil unless File.exist?(path)

  BinaryFormat::OrgIndex.load(path)
rescue StandardError => e
  config.logger.warn("openasn: openasn-orgs.bin unusable (#{e.message}); as_org will be nil until next update")
  nil
end

.parse_pair(v4_bytes, v6_bytes, mode) ⇒ Object

Raises:



56
57
58
59
60
61
62
63
# File 'lib/openasn/snapshot.rb', line 56

def self.parse_pair(v4_bytes, v6_bytes, mode)
  v4_parsed = BinaryFormat.parse_artifact(v4_bytes, mode: mode)
  v6_parsed = BinaryFormat.parse_artifact(v6_bytes, mode: mode)
  raise FormatError, "openasn-ipv4.bin is not an IPv4 artifact" unless v4_parsed[:family] == :ipv4
  raise FormatError, "openasn-ipv6.bin is not an IPv6 artifact" unless v6_parsed[:family] == :ipv6

  [v4_parsed, v6_parsed]
end

.read_artifacts(data_dir) ⇒ Object



65
66
67
68
69
70
71
72
73
74
# File 'lib/openasn/snapshot.rb', line 65

def self.read_artifacts(data_dir)
  v4 = File.join(data_dir, "openasn-ipv4.bin")
  v6 = File.join(data_dir, "openasn-ipv6.bin")
  mf = File.join(data_dir, "manifest.json")
  return nil unless File.exist?(v4) && File.exist?(v6)

  manifest = File.exist?(mf) ? JSON.parse(File.read(mf)) : {}
  verify_manifest_hashes!(manifest, "openasn-ipv4.bin" => v4, "openasn-ipv6.bin" => v6)
  [File.binread(v4), File.binread(v6), manifest]
end

.verify_manifest_hashes!(manifest, files) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/openasn/snapshot.rb', line 76

def self.verify_manifest_hashes!(manifest, files)
  by_name = (manifest["files"] || []).to_h { |f| [f["name"], f] }
  return if by_name.empty? # legacy/manual installs without checksums still parse through FORMAT.md.

  files.each do |name, path|
    expected = by_name.dig(name, "sha256")
    raise IntegrityError, "manifest missing checksum for #{name}" if expected.to_s.empty?

    actual = Digest::SHA256.file(path).hexdigest
    next if actual == expected

    # Updater writes manifest.json last as the commit marker. If a
    # process dies during the final rename loop, a fresh boot can see
    # new artifact bytes next to the old manifest. Reject that mixed
    # set here and fall back to the bundled seed instead of serving a
    # cross-build IPv4/IPv6 pair.
    raise IntegrityError, "#{name} checksum does not match manifest"
  end
end

Instance Method Details

#age_secondsObject



153
154
155
# File 'lib/openasn/snapshot.rb', line 153

def age_seconds
  Time.now.to_i - @build_ts
end

#family(fam) ⇒ Object



138
# File 'lib/openasn/snapshot.rb', line 138

def family(fam) = fam == :ipv4 ? @v4 : @v6

#org_name(asn) ⇒ Object



149
150
151
# File 'lib/openasn/snapshot.rb', line 149

def org_name(asn)
  asn && @orgs ? @orgs.name(asn) : nil
end

#overlays_for(fam, maps_to) ⇒ Object

Overlays for one family in a stable order (executor wrote them; order among same-precedence overlays doesn't affect verdicts, only which provider gets attribution on exotic multi-overlay hits).



145
146
147
# File 'lib/openasn/snapshot.rb', line 145

def overlays_for(fam, maps_to)
  @overlay_index.fetch([fam, maps_to], EMPTY_OVERLAYS)
end