Skip to content

Commit 85d77ae

Browse files
sgriflevkk
andauthored
Ensure we handle pipelined cross-shard queries (#1161)
When mulitple queries are received before a Sync message, we need to treat them as inside an implicit transaction. Before this commit, we'd see that the first statement routes to a single shard, open a direct-to-shard connection to that shard, and forward the message. This would result in the implicit transaction on the shard's end, purely because it's receiving the messages in the same order we do. But we were completely unaware of the transaction-like behavior on our end, unless the client was explicitly starting a transaction. With this change, we now correctly recognize that we're in a transaction any time we receive multiple executable messages prior to a sync, and behave the same as we would if the first query we received in the pipeline were `BEGIN` Co-authored-by: Lev Kokotov <lev.kokotov@gmail.com>
1 parent 412d359 commit 85d77ae

5 files changed

Lines changed: 92 additions & 2 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/jackc/pgx/v5"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func countOnShardByComment(t *testing.T, conn *pgx.Conn, shard int, id int64) int {
14+
t.Helper()
15+
16+
var count int
17+
err := conn.QueryRow(context.Background(),
18+
fmt.Sprintf("/* pgdog_shard: %d */ SELECT COUNT(*) FROM sharded WHERE id = $1", shard), id).Scan(&count)
19+
require.NoError(t, err)
20+
return count
21+
}
22+
23+
func TestBatchRoutesShardedTable(t *testing.T) {
24+
ctx := context.Background()
25+
26+
proxy, err := pgx.Connect(ctx, testConnStr)
27+
require.NoError(t, err)
28+
defer proxy.Close(ctx)
29+
30+
_, err = proxy.Exec(ctx, "TRUNCATE TABLE sharded")
31+
require.NoError(t, err)
32+
33+
const shard0ID int64 = 1
34+
const shard1ID int64 = 11
35+
36+
batch := &pgx.Batch{}
37+
batch.Queue("INSERT INTO sharded (id, value) VALUES ($1, $2)", shard0ID, "shard_0")
38+
batch.Queue("INSERT INTO sharded (id, value) VALUES ($1, $2)", shard1ID, "shard_1")
39+
40+
results := proxy.SendBatch(ctx, batch)
41+
for i := range 2 {
42+
tag, err := results.Exec()
43+
require.NoError(t, err, "batch insert %d", i)
44+
assert.EqualValues(t, 1, tag.RowsAffected(), "batch insert %d rows affected", i)
45+
}
46+
require.NoError(t, results.Close())
47+
48+
assert.Equal(t, 1, countOnShardByComment(t, proxy, 0, shard0ID), "id=%d must be on shard 0", shard0ID)
49+
assert.Equal(t, 0, countOnShardByComment(t, proxy, 1, shard0ID), "id=%d must not be on shard 1", shard0ID)
50+
assert.Equal(t, 0, countOnShardByComment(t, proxy, 0, shard1ID), "id=%d must not be on shard 0", shard1ID)
51+
assert.Equal(t, 1, countOnShardByComment(t, proxy, 1, shard1ID), "id=%d must be on shard 1", shard1ID)
52+
}
53+
54+
func TestBatchRollsBackOnError(t *testing.T) {
55+
ctx := context.Background()
56+
57+
proxy, err := pgx.Connect(ctx, testConnStr)
58+
require.NoError(t, err)
59+
defer proxy.Close(ctx)
60+
61+
_, err = proxy.Exec(ctx, "TRUNCATE TABLE sharded")
62+
require.NoError(t, err)
63+
64+
const shard0ID int64 = 1
65+
const shard1ID int64 = 11
66+
67+
batch := &pgx.Batch{}
68+
batch.Queue("INSERT INTO sharded (id, value) VALUES ($1, $2)", shard0ID, "shard_0")
69+
batch.Queue("INSERT INTO sharded (id, value) VALUES ($1, $2)", shard1ID, "shard_1")
70+
batch.Queue("INSERT INTO sharded (id, value) VALUES ($1, $2)", shard0ID, "shard_0")
71+
72+
results := proxy.SendBatch(ctx, batch)
73+
tag, err := results.Exec()
74+
require.NoError(t, err, "batch insert 1")
75+
assert.EqualValues(t, 1, tag.RowsAffected(), "batch insert 1 rows affected")
76+
tag, err = results.Exec()
77+
require.NoError(t, err, "batch insert 2")
78+
assert.EqualValues(t, 1, tag.RowsAffected(), "batch insert 2 rows affected")
79+
80+
_, err = results.Exec()
81+
require.ErrorContains(t, err, "duplicate key value violates unique constraint")
82+
require.Error(t, results.Close())
83+
84+
assert.Equal(t, 0, countOnShardByComment(t, proxy, 0, shard0ID), "transaction must have rolled back")
85+
assert.Equal(t, 0, countOnShardByComment(t, proxy, 1, shard0ID), "transaction must have rolled back")
86+
}

pgdog/src/frontend/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,7 @@ impl Client {
551551
} else {
552552
let total = spliced.len();
553553
let mut reqs = spliced.into_iter().enumerate();
554+
self.transaction.get_or_insert(TransactionType::Implicit);
554555
while let Some((num, mut req)) = reqs.next() {
555556
debug!("processing spliced request {}/{}", num + 1, total);
556557
let mut context = QueryEngineContext::new(self).spliced(&mut req, reqs.len());

pgdog/src/frontend/client/query_engine/query.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ impl QueryEngine {
172172
TransactionState::Error => {
173173
let error_state = match context.transaction {
174174
Some(TransactionType::ReadOnly) => Some(TransactionType::ErrorReadOnly),
175-
Some(TransactionType::ReadWrite) => Some(TransactionType::ErrorReadWrite),
175+
Some(TransactionType::ReadWrite | TransactionType::Implicit) => {
176+
Some(TransactionType::ErrorReadWrite)
177+
}
176178
_ => None,
177179
};
178180
context.transaction = error_state;

pgdog/src/frontend/client/transaction_type.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub enum TransactionType {
33
ReadOnly,
44
#[default]
55
ReadWrite,
6+
Implicit,
67
ErrorReadWrite,
78
ErrorReadOnly,
89
}

pgdog/src/frontend/router/parser/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl<'a> QueryParserContext<'a> {
8181
let role = self.router_context.parameter_hints.compute_role();
8282
let txn_write = matches!(
8383
self.router_context.transaction(),
84-
Some(TransactionType::ReadWrite)
84+
Some(TransactionType::ReadWrite | TransactionType::Implicit)
8585
) && self.rw_conservative();
8686
// prefer_primary defaults reads to the primary; an explicit replica hint opts out.
8787
txn_write

0 commit comments

Comments
 (0)