Class: Neo4j::Driver::Routing::RoutingTable

Inherits:
Object
  • Object
show all
Defined in:
lib/neo4j/driver/routing/routing_table.rb

Overview

Snapshot of the cluster's routing roles, plus mutators used by the error→action handlers (forget on connection failure, forget_writer on NotALeader / ForbiddenOnReadOnlyDatabase). Mirrors the design of Java's ClusterRoutingTable + Python's _routing.RoutingTable.

Threading: callers (LoadBalancer) hold @refresh_lock while mutating or reading. Methods here are not internally synchronised.

Constant Summary collapse

ROLE_TO_KEY =
{ 'READ' => :readers, 'WRITE' => :writers, 'ROUTE' => :routers }.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(database:, routers: [], readers: [], writers: [], ttl: 0, clock: Internal::Clock.new) ⇒ RoutingTable

Returns a new instance of RoutingTable.



43
44
45
46
47
48
49
50
51
52
# File 'lib/neo4j/driver/routing/routing_table.rb', line 43

def initialize(database:, routers: [], readers: [], writers: [], ttl: 0, clock: Internal::Clock.new)
  @database = database
  @routers = routers.to_set
  @readers = readers.to_set
  @writers = writers.to_set
  @ttl = ttl
  @clock = clock
  @last_updated = @clock.realtime
  @initialized_without_writers = @writers.empty?
end

Instance Attribute Details

#databaseObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def database
  @database
end

#initialized_without_writersObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def initialized_without_writers
  @initialized_without_writers
end

#last_updatedObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def last_updated
  @last_updated
end

#readersObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def readers
  @readers
end

#routersObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def routers
  @routers
end

#ttlObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def ttl
  @ttl
end

#writersObject (readonly)

Servers don't always return a writer right away (e.g. cluster mid-leader-election). The table is still usable for reads in that window, but the next routing refresh should prefer the initial address (the seed router) over the existing routers list since the latter may have been populated by a leaderless reply.



19
20
21
# File 'lib/neo4j/driver/routing/routing_table.rb', line 19

def writers
  @writers
end

Class Method Details

.from_response(rt, requested_database, clock: Internal::Clock.new) ⇒ Object

Build a table from the rt map returned by the BOLT ROUTE message:

{ttl: 1000, db: "homedb", servers: [{addresses: [...], role: "READ"}, ...]}


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/neo4j/driver/routing/routing_table.rb', line 26

def self.from_response(rt, requested_database, clock: Internal::Clock.new)
  buckets = { readers: [], writers: [], routers: [] }
  (rt[:servers] || []).each do |server|
    key = ROLE_TO_KEY[server[:role]] or next
    (server[:addresses] || []).each { |addr| buckets[key] << ServerAddress.parse(addr) }
  end

  new(
    database: requested_database || rt[:db],
    routers: buckets[:routers].uniq,
    readers: buckets[:readers].uniq,
    writers: buckets[:writers].uniq,
    ttl: (rt[:ttl] || 0).to_f,
    clock: clock
  )
end

Instance Method Details

#expiration_timestampObject

Absolute expiry in epoch millis (last_updated + ttl). testkit's backend derives the relative ttl from this — uniformly with JRuby, whose Java RoutingTable exposes only this absolute timestamp.



72
73
74
# File 'lib/neo4j/driver/routing/routing_table.rb', line 72

def expiration_timestamp
  ((@last_updated + @ttl).to_f * 1000).round
end

#expired?(now = @clock.realtime) ⇒ Boolean

Returns:

  • (Boolean)


65
66
67
# File 'lib/neo4j/driver/routing/routing_table.rb', line 65

def expired?(now = @clock.realtime)
  now >= @last_updated + @ttl
end

#forget(address) ⇒ Object

Remove an address from every role bucket. Called on connection failure: the server is gone, drop it everywhere.



96
97
98
99
100
# File 'lib/neo4j/driver/routing/routing_table.rb', line 96

def forget(address)
  @routers.delete(address)
  @readers.delete(address)
  @writers.delete(address)
end

#forget_writer(address) ⇒ Object

Remove an address only from the writers bucket. Called when a WRITE op fails with NotALeader / ForbiddenOnReadOnlyDatabase: the server is alive but no longer the leader, so it can still serve reads.



106
107
108
# File 'lib/neo4j/driver/routing/routing_table.rb', line 106

def forget_writer(address)
  @writers.delete(address)
end

#fresh?(readonly:, now: @clock.realtime) ⇒ Boolean

A table is fresh when it isn't expired AND has the servers needed for the requested access mode. An empty writers list is acceptable for read-only acquires (initialized_without_writers state), but a write-mode acquire needs a writer.

Returns:

  • (Boolean)


58
59
60
61
62
63
# File 'lib/neo4j/driver/routing/routing_table.rb', line 58

def fresh?(readonly:, now: @clock.realtime)
  return false if expired?(now)
  return false if @routers.empty?

  readonly ? @readers.any? : @writers.any?
end

#purge?(grace:, now: @clock.realtime) ⇒ Boolean

Past the cache grace period? Used by LoadBalancer to drop tables for databases nobody is touching anymore.

Returns:

  • (Boolean)


78
79
80
# File 'lib/neo4j/driver/routing/routing_table.rb', line 78

def purge?(grace:, now: @clock.realtime)
  now >= @last_updated + @ttl + grace
end

#serversObject

Union of all role buckets — used when shutting down per-server pools that no current routing table references.



121
122
123
# File 'lib/neo4j/driver/routing/routing_table.rb', line 121

def servers
  @routers | @readers | @writers
end

#servers_for(access_mode) ⇒ Object



110
111
112
113
114
115
116
117
# File 'lib/neo4j/driver/routing/routing_table.rb', line 110

def servers_for(access_mode)
  case access_mode
  when :read  then @readers
  when :write then @writers
  when :route then @routers
  else raise ArgumentError, "Unknown access mode: #{access_mode.inspect}"
  end
end

#update(other) ⇒ Object

Replace the contents in place with another table's roles. The existing table identity is preserved so anyone holding a reference sees the new state.



85
86
87
88
89
90
91
92
# File 'lib/neo4j/driver/routing/routing_table.rb', line 85

def update(other)
  @routers = other.routers.dup
  @readers = other.readers.dup
  @writers = other.writers.dup
  @ttl = other.ttl
  @last_updated = @clock.realtime
  @initialized_without_writers = @writers.empty?
end