-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Read replica support (#476)
* added variables for tracking time since last write * added variables to store reader and writer db connections * switched from time.utc to time.monotonic for tracking time since last write * Added methods to switch between connections. Granite::Connections now accept connections with writer/reader. See spec/spec_helper spec/adapters_spec Added env var to store url to sqlite replica db url * added instance variant of switch_to_writer_adapter for use with callback. Simplified callbacks * Fixed typo in def switch_to_writer_adapter automatically switch to the writer adapter before saving to database. * added logic to automatically switch to reader adapter * functions in querying now dynamically change adapter based on need * ensure all methods in query builder that need primary database switch connection * added better error message for invalid connections * groundwork for replica testing * fixed error where mysql tests don't run * Granite::Base.adapter class method now actually fetches current adapter if available instead of using first connection in Granite::Connections * fixed typo. Invalid adapter_type for pg_with_replica * fixed True to true. Added table name to ReplicatedChat. * update specs * spec updates * Update .gitignore * moved connection management logic to seperate module. Fixed bug where there was no switch to reader adapter. Added test for connection switching * fixed error where reader connection switch ignored specified wait period. Fixed invalid test. * moved default value for connection switch wait period to granite::connections to allow changing it globally * moved connection macro to connection management module * cleaned up code for fetching first connection * finalized syntax for adding new connections to granite::connections * optimization: when reader & writer database are the same do not duplicate connection pool --------- Co-authored-by: Holden Omans <[email protected]> Co-authored-by: Seth T <[email protected]>
- Loading branch information
1 parent
5afa787
commit 8b9b55b
Showing
15 changed files
with
221 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,3 +12,4 @@ shard.lock | |
|
||
# Ignore bin because they will be build with shards install | ||
bin | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export PG_DATABASE_URL=postgres://granite:password@localhost:5432/granite_db | ||
export PG_REPLICA_URL=postgres://granite:password@localhost:5432/granite__replica_db | ||
export MYSQL_DATABASE_URL=mysql://granite:password@localhost:3306/granite_db | ||
export MYSQL_REPLICA_URL=mysql://granite:password@localhost:3306/granite_replica_db | ||
export SQLITE_DATABASE_URL=sqlite3:./granite.db | ||
export SQLITE_REPLICA_URL=sqlite3:./granite_replica.db | ||
export PG_VERSION=15.2 | ||
export MYSQL_VERSION=5.7 | ||
export SQLITE_VERSION=3110000 | ||
export SQLITE_VERSION_YEAR=2016 | ||
export CURRENT_ADAPTER=sqlite |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
require "spec" | ||
|
||
describe "Granite::Base track time since last write" do | ||
it "should switch to reader db connection after connection_switch_wait_period after write operation" do | ||
ReplicatedChat.connection_switch_wait_period = 250 | ||
ReplicatedChat.new(content: "hello world!").save! | ||
sleep 500.milliseconds | ||
current_url = ReplicatedChat.adapter.url | ||
reader_url = Granite::Connections[ENV["CURRENT_ADAPTER"] + "_with_replica"].not_nil![:reader].url | ||
current_url.should eq reader_url | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
module Granite::ConnectionManagement | ||
macro included | ||
# Default value for the time a model waits before using a reader | ||
# database connection for read operations | ||
# all models use this value. Change it | ||
# to change it in all Granite::Base models. | ||
class_property connection_switch_wait_period : Int64 = Granite::Connections.connection_switch_wait_period | ||
@@last_write_time = Time.monotonic | ||
|
||
class_property current_adapter : Granite::Adapter::Base? | ||
class_property reader_adapter : Granite::Adapter::Base = Granite::Connections.first_reader | ||
class_property writer_adapter : Granite::Adapter::Base = Granite::Connections.first_writer | ||
|
||
def self.last_write_time | ||
@@last_write_time | ||
end | ||
|
||
# This is done this way because callbacks don't work on class mthods | ||
def self.update_last_write_time | ||
@@last_write_time = Time.monotonic | ||
end | ||
|
||
def update_last_write_time | ||
self.class.update_last_write_time | ||
end | ||
|
||
def self.time_since_last_write | ||
Time.monotonic - @@last_write_time | ||
end | ||
|
||
def time_since_last_write | ||
self.class.time_since_last_write | ||
end | ||
|
||
def self.switch_to_reader_adapter | ||
if time_since_last_write > @@connection_switch_wait_period.milliseconds | ||
@@current_adapter = @@reader_adapter | ||
end | ||
end | ||
|
||
def switch_to_reader_adapter | ||
self.class.switch_to_reader_adapter | ||
end | ||
|
||
def self.switch_to_writer_adapter | ||
@@current_adapter = @@writer_adapter | ||
end | ||
|
||
def switch_to_writer_adapter | ||
self.class.switch_to_writer_adapter | ||
end | ||
|
||
def self.schedule_adapter_switch | ||
spawn do | ||
sleep @@connection_switch_wait_period.milliseconds | ||
switch_to_reader_adapter | ||
end | ||
|
||
Fiber.yield | ||
end | ||
|
||
def schedule_adapter_switch | ||
self.class.schedule_adapter_switch | ||
end | ||
|
||
def self.adapter | ||
begin | ||
@@current_adapter.not_nil! | ||
rescue NilAssertionError | ||
Granite::Connections.registered_connections.first?.not_nil![:writer] | ||
end | ||
end | ||
end | ||
|
||
macro connection(name) | ||
{% name = name.id.stringify %} | ||
|
||
error_message = "Connection #{{{name}}} not found in Granite::Connections. | ||
Available connections are: | ||
#{Granite::Connections.registered_connections.map{ |conn| "#{conn[:writer].name}"}.join(", ")}" | ||
|
||
raise error_message if Granite::Connections[{{name}}].nil? | ||
|
||
self.writer_adapter = Granite::Connections[{{name}}].not_nil![:writer] | ||
self.reader_adapter = Granite::Connections[{{name}}].not_nil![:reader] | ||
self.current_adapter = @@writer_adapter | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,40 @@ | ||
module Granite | ||
class Connections | ||
class_getter registered_connections = [] of Granite::Adapter::Base | ||
class_property connection_switch_wait_period : Int64 = 2000 | ||
class_getter registered_connections = [] of {writer: Granite::Adapter::Base, reader: Granite::Adapter::Base} | ||
|
||
# Registers the given *adapter*. Raises if an adapter with the same name has already been registered. | ||
def self.<<(adapter : Granite::Adapter::Base) : Nil | ||
raise "Adapter with name '#{adapter.name}' has already been registered." if @@registered_connections.any? { |conn| conn.name == adapter.name } | ||
@@registered_connections << adapter | ||
raise "Adapter with name '#{adapter.name}' has already been registered." if @@registered_connections.any? { |conn| conn[:writer].name == adapter.name } | ||
@@registered_connections << {writer: adapter, reader: adapter} | ||
end | ||
|
||
def self.<<(data : NamedTuple(name: String, reader: String, writer: String, adapter_type: Granite::Adapter::Base.class)) : Nil | ||
raise "Adapter with name '#{data[:name]}' has already been registered." if @@registered_connections.any? { |conn| conn[:writer].name == data[:name] } | ||
|
||
writer_adapter = data[:adapter_type].new(name: data[:name], url: data[:writer]) | ||
|
||
# if reader/writer reference the same db. Make them point to the same granite adapter. | ||
# This avoids connection pool duplications on the same database. | ||
if (data[:reader] == data[:writer]) | ||
return @@registered_connections << {writer: writer_adapter, reader: writer_adapter} | ||
end | ||
|
||
reader_adapter = data[:adapter_type].new(name: data[:name], url: data[:reader]) | ||
@@registered_connections << {writer: writer_adapter, reader: reader_adapter} | ||
end | ||
|
||
# Returns a registered connection with the given *name*, otherwise `nil`. | ||
def self.[](name : String) : Granite::Adapter::Base? | ||
registered_connections.find { |conn| conn.name == name } | ||
def self.[](name : String) : {writer: Granite::Adapter::Base, reader: Granite::Adapter::Base}? | ||
registered_connections.find { |conn| conn[:writer].name == name } | ||
end | ||
|
||
def self.first_writer | ||
@@registered_connections.first?.not_nil![:writer] | ||
end | ||
|
||
def self.first_reader | ||
@@registered_connections.first?.not_nil![:reader] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.