Skip to content

Commit 970f7b1

Browse files
authored
feat: add fallback RO db to RW (bxcodec#27)
* chore: backup code * chore: go-sg-demo * chore: add DB connection check and fallback from RO to RW * chore: add fallback logic * chore: fix linter * chore: nolint * chore: remove unnecessary log
1 parent 171ab7f commit 970f7b1

File tree

13 files changed

+487
-12
lines changed

13 files changed

+487
-12
lines changed

.golangci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,4 @@ issues:
145145
run:
146146
timeout: 5m
147147
go: "1.19"
148-
skip-dirs: []
148+
skip-dirs: [examples]

db.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type StmtLoadBalancer LoadBalancer[*sql.Stmt]
5050
// sqlDB is a logical database with multiple underlying physical databases
5151
// forming a single ReadWrite (primary) with multiple ReadOnly(replicas) db.
5252
// Reads and writes are automatically directed to the correct db connection
53+
5354
type sqlDB struct {
5455
primaries []*sql.DB
5556
replicas []*sql.DB
@@ -143,18 +144,23 @@ func (db *sqlDB) Prepare(query string) (_stmt Stmt, err error) {
143144
func (db *sqlDB) PrepareContext(ctx context.Context, query string) (_stmt Stmt, err error) {
144145
roStmts := make([]*sql.Stmt, len(db.replicas))
145146
primaryStmts := make([]*sql.Stmt, len(db.primaries))
146-
147147
errPrimaries := doParallely(len(db.primaries), func(i int) (err error) {
148148
primaryStmts[i], err = db.primaries[i].PrepareContext(ctx, query)
149149
return
150150
})
151+
151152
errReplicas := doParallely(len(db.replicas), func(i int) (err error) {
152153
roStmts[i], err = db.replicas[i].PrepareContext(ctx, query)
154+
// if connection error happens on RO connection,
155+
// ignore and fallback to RW connection
156+
if isDBConnectionError(err) {
157+
roStmts[i] = primaryStmts[0]
158+
return nil
159+
}
153160
return err
154161
})
155162

156163
err = multierr.Combine(errPrimaries, errReplicas)
157-
158164
if err != nil {
159165
return
160166
}
@@ -165,7 +171,7 @@ func (db *sqlDB) PrepareContext(ctx context.Context, query string) (_stmt Stmt,
165171
primaryStmts: primaryStmts,
166172
replicaStmts: roStmts,
167173
}
168-
return
174+
return _stmt, nil
169175
}
170176

171177
// Query executes a query that returns rows, typically a SELECT.
@@ -179,7 +185,11 @@ func (db *sqlDB) Query(query string, args ...interface{}) (*sql.Rows, error) {
179185
// The args are for any placeholder parameters in the query.
180186
// QueryContext uses a radonly db as the physical db.
181187
func (db *sqlDB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
182-
return db.ReadOnly().QueryContext(ctx, query, args...)
188+
rows, err := db.ReadOnly().QueryContext(ctx, query, args...)
189+
if isDBConnectionError(err) {
190+
rows, err = db.ReadWrite().QueryContext(ctx, query, args...)
191+
}
192+
return rows, err
183193
}
184194

185195
// QueryRow executes a query that is expected to return at most one row.
@@ -195,7 +205,12 @@ func (db *sqlDB) QueryRow(query string, args ...interface{}) *sql.Row {
195205
// Errors are deferred until Row's Scan method is called.
196206
// QueryRowContext uses a radonly db as the physical db.
197207
func (db *sqlDB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
198-
return db.ReadOnly().QueryRowContext(ctx, query, args...)
208+
row := db.ReadOnly().QueryRowContext(ctx, query, args...)
209+
if isDBConnectionError(row.Err()) {
210+
row = db.ReadWrite().QueryRowContext(ctx, query, args...)
211+
}
212+
213+
return row
199214
}
200215

201216
// SetMaxIdleConns sets the maximum number of connections in the idle

examples/gosg-demo/Makefile

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Exporting bin folder to the path for makefile
2+
export PATH := $(PWD)/bin:$(PATH)
3+
# Default Shell
4+
export SHELL := bash
5+
# Type of OS: Linux or Darwin.
6+
export OSTYPE := $(shell uname -s)
7+
8+
export MUSL := $(shell [ -x /sbin/apk ] && echo "-tags musl" || echo "")
9+
10+
ifeq ($(OSTYPE),Darwin)
11+
export MallocNanoZone=0
12+
endif
13+
14+
define github_url
15+
https://github.com/$(GITHUB_REPO)/releases/download/v$(VERSION)/$(ARCHIVE)
16+
endef
17+
18+
# creates a directory bin.
19+
bin:
20+
@ mkdir -p $@
21+
22+
# ~~~ Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23+
24+
# ~~ [migrate] ~~~ https://github.com/golang-migrate/migrate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25+
26+
MIGRATE := $(shell command -v migrate || echo "bin/migrate")
27+
migrate: bin/migrate ## Install migrate (database migration)
28+
29+
bin/migrate: VERSION := 4.14.1
30+
bin/migrate: GITHUB_REPO := golang-migrate/migrate
31+
bin/migrate: ARCHIVE := migrate.$(OSTYPE)-amd64.tar.gz
32+
bin/migrate: bin
33+
@ printf "Install migrate... "
34+
@ curl -Ls $(call github_url) | tar -zOxf - ./migrate.$(shell echo $(OSTYPE) | tr A-Z a-z)-amd64 > $@ && chmod +x $@
35+
@ echo "done."
36+
37+
38+
# ~~~ Database Migrations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
39+
40+
POSTGRES_USER ?= postgres
41+
POSTGRES_PASSWORD ?= my_password
42+
POSTGRES_HOST ?= localhost
43+
POSTGRES_PORT ?= 5432
44+
POSTGRES_DATABASE ?= my_database
45+
46+
47+
PG_DSN := "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DATABASE)?sslmode=disable"
48+
49+
migrate-up: $(MIGRATE) ## Apply all (or N up) migrations.
50+
@ read -p "How many migration you wants to perform (default value: [all]): " N; \
51+
migrate -database $(PG_DSN) -path=internal/postgres/migrations up $${N}
52+
# if you encounter the dirty version, fix the error, then use the below command
53+
# migrate -database $(PG_DSN) -path=internal/postgres/migrations force previous_version up
54+
55+
.PHONY: migrate-down
56+
migrate-down: $(MIGRATE) ## Apply all (or N down) migrations.
57+
@ read -p "How many migration you wants to perform (default value: [all]): " N; \
58+
migrate -database $(PG_DSN) -path=internal/postgres/migrations down $${N}
59+
60+
.PHONY: migrate-drop
61+
migrate-drop: $(MIGRATE) ## Drop everything inside the database.
62+
migrate -database $(PG_DSN) -path=internal/postgres/migrations drop
63+
64+
.PHONY: migrate-create
65+
migrate-create: $(MIGRATE) ## Create a set of up/down migrations with a specified name.
66+
@ read -p "Please provide name for the migration: " Name; \
67+
migrate create -ext sql -dir internal/postgres/migrations $${Name}
68+
69+
up:
70+
@ docker-compose up -d
71+
down:
72+
@ docker-compose up -d
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
version: "2"
2+
3+
services:
4+
postgres-rw:
5+
image: "docker.io/bitnami/postgresql:11-debian-10"
6+
ports:
7+
- "5432:5432"
8+
volumes:
9+
- "postgresql_master_data:/bitnami/postgresql"
10+
environment:
11+
- POSTGRESQL_PGAUDIT_LOG=READ,WRITE
12+
- POSTGRESQL_LOG_HOSTNAME=true
13+
- POSTGRESQL_REPLICATION_MODE=master
14+
- POSTGRESQL_REPLICATION_USER=repl_user
15+
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
16+
- POSTGRESQL_USERNAME=postgres
17+
- POSTGRESQL_DATABASE=my_database
18+
# - ALLOW_EMPTY_PASSWORD=yes
19+
- POSTGRESQL_PASSWORD=my_password
20+
postgres-ro:
21+
image: "docker.io/bitnami/postgresql:11-debian-10"
22+
ports:
23+
- "5433:5432"
24+
depends_on:
25+
- postgres-rw
26+
environment:
27+
- POSTGRESQL_USERNAME=postgres
28+
- POSTGRESQL_PASSWORD=my_password
29+
- POSTGRESQL_MASTER_HOST=postgres-rw
30+
- POSTGRESQL_PGAUDIT_LOG=READ,WRITE
31+
- POSTGRESQL_LOG_HOSTNAME=true
32+
- POSTGRESQL_REPLICATION_MODE=slave
33+
- POSTGRESQL_REPLICATION_USER=repl_user
34+
- POSTGRESQL_REPLICATION_PASSWORD=repl_password
35+
- POSTGRESQL_MASTER_PORT_NUMBER=5432
36+
37+
volumes:
38+
postgresql_master_data:
39+
driver: local
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
BEGIN;
2+
3+
DROP TABLE IF EXISTS articles;
4+
5+
END;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
BEGIN;
2+
3+
CREATE TABLE IF NOT EXISTS articles (
4+
article_id serial PRIMARY KEY,
5+
title VARCHAR ( 150 ) NOT NULL,
6+
content TEXT NOT NULL,
7+
created_time TIMESTAMP NOT NULL
8+
);
9+
10+
END;

0 commit comments

Comments
 (0)