Skip to content

Commit bbacb9c

Browse files
authored
Explicit shard selection; Rails tests (#24)
* Explicit shard selection; Rails tests * try running ruby tests * try without lockfile * aha * ok
1 parent aa79628 commit bbacb9c

File tree

7 files changed

+170
-14
lines changed

7 files changed

+170
-14
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
key: cargo-lock-2-{{ checksum "Cargo.lock" }}
2626
- run:
2727
name: "Install dependencies"
28-
command: "sudo apt-get update && sudo apt-get install -y psmisc postgresql-contrib-12 postgresql-client-12"
28+
command: "sudo apt-get update && sudo apt-get install -y psmisc postgresql-contrib-12 postgresql-client-12 ruby ruby-dev libpq-dev"
2929
- run:
3030
name: "Build"
3131
command: "cargo build"

.circleci/run_tests.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ psql -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_select.sql > /
3434
# Replica/primary selection & more sharding tests
3535
psql -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_primary_replica.sql > /dev/null
3636

37+
#
38+
# ActiveRecord tests!
39+
#
40+
cd tests/ruby
41+
sudo gem install bundler
42+
bundle install
43+
ruby tests.rb
44+
3745
# Attempt clean shut down
3846
killall pgcat -s SIGINT
3947

src/query_router.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ use sqlparser::parser::Parser;
1010
use crate::config::Role;
1111
use crate::sharding::Sharder;
1212

13-
const SHARDING_REGEX: &str = r"SET SHARDING KEY TO '[0-9]+';";
14-
const ROLE_REGEX: &str = r"SET SERVER ROLE TO '(PRIMARY|REPLICA)';";
13+
const SHARDING_REGEX: &str = r"SET SHARDING KEY TO '[0-9]+'";
14+
const SET_SHARD_REGEX: &str = r"SET SHARD TO '[0-9]+'";
15+
const ROLE_REGEX: &str = r"SET SERVER ROLE TO '(PRIMARY|REPLICA)'";
1516

1617
static SHARDING_REGEX_RE: OnceCell<Regex> = OnceCell::new();
1718
static ROLE_REGEX_RE: OnceCell<Regex> = OnceCell::new();
19+
static SET_SHARD_REGEX_RE: OnceCell<Regex> = OnceCell::new();
1820

1921
pub struct QueryRouter {
2022
// By default, queries go here, unless we have better information
@@ -60,7 +62,17 @@ impl QueryRouter {
6062
Err(_) => false,
6163
};
6264

63-
a && b
65+
let c = match SET_SHARD_REGEX_RE.set(
66+
RegexBuilder::new(SET_SHARD_REGEX)
67+
.case_insensitive(true)
68+
.build()
69+
.unwrap(),
70+
) {
71+
Ok(_) => true,
72+
Err(_) => false,
73+
};
74+
75+
a && b && c
6476
}
6577

6678
pub fn new(
@@ -99,12 +111,17 @@ impl QueryRouter {
99111
let len = buf.get_i32();
100112
let query = String::from_utf8_lossy(&buf[..len as usize - 4 - 1]); // Don't read the ternminating null
101113

102-
let rgx = match SHARDING_REGEX_RE.get() {
114+
let sharding_key_rgx = match SHARDING_REGEX_RE.get() {
103115
Some(r) => r,
104116
None => return false,
105117
};
106118

107-
if rgx.is_match(&query) {
119+
let set_shard_rgx = match SET_SHARD_REGEX_RE.get() {
120+
Some(r) => r,
121+
None => return false,
122+
};
123+
124+
if sharding_key_rgx.is_match(&query) {
108125
let shard = query.split("'").collect::<Vec<&str>>()[1];
109126

110127
match shard.parse::<i64>() {
@@ -120,6 +137,15 @@ impl QueryRouter {
120137
// case anyway.
121138
Err(_) => false,
122139
}
140+
} else if set_shard_rgx.is_match(&query) {
141+
let shard = query.split("'").collect::<Vec<&str>>()[1];
142+
match shard.parse::<usize>() {
143+
Ok(shard) => {
144+
self.active_shard = Some(shard);
145+
true
146+
}
147+
Err(_) => false,
148+
}
123149
} else {
124150
false
125151
}
@@ -439,4 +465,28 @@ mod test {
439465
assert!(query_router.infer_role(res));
440466
assert_eq!(query_router.role(), Some(Role::Replica));
441467
}
468+
469+
#[test]
470+
fn test_set_shard_explicitely() {
471+
QueryRouter::setup();
472+
473+
let default_server_role: Option<Role> = None;
474+
let shards = 5;
475+
476+
let mut query_router = QueryRouter::new(default_server_role, shards, false, false);
477+
478+
// Build the special syntax query.
479+
let mut message = BytesMut::new();
480+
let query = BytesMut::from(&b"SET SHARD TO '1'\0"[..]);
481+
482+
message.put_u8(b'Q'); // Query
483+
message.put_i32(query.len() as i32 + 4);
484+
message.put_slice(&query[..]);
485+
486+
assert!(query_router.select_shard(message));
487+
assert_eq!(query_router.shard(), 1); // See sharding.rs (we are using 5 shards on purpose in this test)
488+
489+
query_router.reset();
490+
assert_eq!(query_router.shard(), 0);
491+
}
442492
}

tests/ruby/.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2.7.1

tests/ruby/Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
source "https://rubygems.org"
2+
3+
gem "pg"
4+
gem "activerecord"

tests/ruby/Gemfile.lock

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
activemodel (7.0.2.2)
5+
activesupport (= 7.0.2.2)
6+
activerecord (7.0.2.2)
7+
activemodel (= 7.0.2.2)
8+
activesupport (= 7.0.2.2)
9+
activesupport (7.0.2.2)
10+
concurrent-ruby (~> 1.0, >= 1.0.2)
11+
i18n (>= 1.6, < 2)
12+
minitest (>= 5.1)
13+
tzinfo (~> 2.0)
14+
concurrent-ruby (1.1.9)
15+
i18n (1.10.0)
16+
concurrent-ruby (~> 1.0)
17+
minitest (5.15.0)
18+
pg (1.3.2)
19+
tzinfo (2.0.4)
20+
concurrent-ruby (~> 1.0)
21+
22+
PLATFORMS
23+
x86_64-linux
24+
25+
DEPENDENCIES
26+
activerecord
27+
pg
28+
29+
BUNDLED WITH
30+
2.3.7

tests/ruby/tests.rb

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,74 @@
1-
require 'pg'
1+
require "active_record"
22

3-
conn = PG.connect(host: '127.0.0.1', port: 5433, dbname: 'test')
3+
ActiveRecord.verbose_query_logs = true
4+
ActiveRecord::Base.logger = Logger.new(STDOUT)
45

5-
conn.exec( "SELECT * FROM pg_stat_activity" ) do |result|
6-
puts " PID | User | Query"
7-
result.each do |row|
8-
puts " %7d | %-16s | %s " %
9-
row.values_at('pid', 'usename', 'query')
6+
ActiveRecord::Base.establish_connection(
7+
adapter: "postgresql",
8+
host: "127.0.0.1",
9+
port: 6432,
10+
username: "sharding_user",
11+
password: "sharding_user",
12+
database: "rails_dev",
13+
prepared_statements: false, # Transaction mode
14+
advisory_locks: false, # Same
15+
)
16+
17+
class TestTable < ActiveRecord::Base
18+
self.table_name = "test_table"
19+
end
20+
21+
# # Create the table.
22+
class CreateTestTable < ActiveRecord::Migration[7.0]
23+
# Disable transasctions or things will fly out of order!
24+
disable_ddl_transaction!
25+
26+
SHARDS = 3
27+
28+
def change
29+
SHARDS.times do |x|
30+
# This will make this migration reversible!
31+
reversible do
32+
connection.execute "SET SHARD TO '#{x.to_i}'"
33+
end
34+
35+
# Always wrap the entire migration inside a transaction. If that's not possible,
36+
# execute a `SET SHARD` command before every statement and make sure AR doesn't need
37+
# to load database information beforehand (i.e. it's not the first query in the migration).
38+
connection.transaction do
39+
create_table :test_table, if_not_exists: true do |t|
40+
t.string :name
41+
t.string :description
42+
43+
t.timestamps
44+
end
45+
end
46+
end
1047
end
11-
end
48+
end
49+
50+
begin
51+
CreateTestTable.migrate(:down)
52+
rescue Exception
53+
puts "Tables don't exist yet"
54+
end
55+
56+
CreateTestTable.migrate(:up)
57+
58+
10.times do |x|
59+
x += 1 # Postgres ids start at 1
60+
r = TestTable.connection.execute "SET SHARDING KEY TO '#{x.to_i}'"
61+
62+
# Always wrap writes inside explicit transactions like these because ActiveRecord may fetch table info
63+
# before actually issuing the `INSERT` statement. This ensures that that happens inside a transaction
64+
# and the write goes to the correct shard.
65+
TestTable.connection.transaction do
66+
TestTable.create(id: x, name: "something_special_#{x.to_i}", description: "It's a surprise!")
67+
end
68+
end
69+
70+
10.times do |x|
71+
x += 1 # 0 confuses our sharding function
72+
TestTable.connection.execute "SET SHARDING KEY TO '#{x.to_i}'"
73+
puts TestTable.find_by_id(x).id
74+
end

0 commit comments

Comments
 (0)