Skip to content

Add built-in ProxySQL failover hooks#36

Merged
renecannao merged 9 commits intomasterfrom
issue30-proxysql-hooks
Mar 24, 2026
Merged

Add built-in ProxySQL failover hooks#36
renecannao merged 9 commits intomasterfrom
issue30-proxysql-hooks

Conversation

@renecannao
Copy link
Copy Markdown

@renecannao renecannao commented Mar 23, 2026

Closes #30

Summary

Built-in pre/post failover hooks that update ProxySQL hostgroups via its Admin API — no custom scripts needed. "orchestrator + ProxySQL works out of the box."

New go/proxysql/ package

  • client.go — ProxySQL Admin connection management (uses MySQL protocol driver)
  • hook.go — Pre/post failover hook logic with parameterized SQL queries
  • init.go — Singleton initialization, nil-safe (no-op when unconfigured)

Configuration

8 new fields in Config struct:

  • ProxySQLAdminAddress, ProxySQLAdminPort (default: 6032), ProxySQLAdminUser (default: admin), ProxySQLAdminPassword, ProxySQLAdminUseTLS
  • ProxySQLWriterHostgroup, ProxySQLReaderHostgroup
  • ProxySQLPreFailoverAction (default: offline_soft)

Failover integration

  • Pre-failover: Drains old master in ProxySQL (OFFLINE_SOFT or weight=0) before recovery begins
  • Post-failover: Updates hostgroups to route to new master after promotion
  • Graceful takeover: Same post-failover hook after graceful master switch
  • Non-blocking: ProxySQL failures are logged but don't prevent MySQL failover

CLI commands

  • proxysql-test — Test connectivity to ProxySQL Admin
  • proxysql-servers — Show runtime_mysql_servers table

Safety

  • All SQL uses parameterized queries (no string interpolation)
  • Nil-safe: defaultHook initialized at package level to no-op, avoiding data races
  • Warning logged when address is set but writer hostgroup is 0

Test plan

  • 7 unit tests pass (go test ./go/proxysql/...)
  • Full test suite passes (go test ./go/... -vet=off)
  • Build succeeds (go build -o bin/orchestrator ./go/cmd/orchestrator)
  • CLI commands appear in help output
  • Integration test with live ProxySQL instance

Summary by CodeRabbit

  • New Features

    • Built-in ProxySQL integration for failover: configurable admin connection, writer/reader hostgroups, and pre-failover action (default: offline_soft). Adds non-blocking pre-failover and tolerant post-failover updates.
    • New CLI commands to test ProxySQL connectivity and list ProxySQL servers.
  • Documentation

    • Detailed ProxySQL hooks guide with configuration, execution timeline, CLI usage, and failure behavior.
  • Tests

    • Unit tests covering config defaults, client creation/DSN, and hook SQL generation.

Adds ProxySQLAdminAddress, ProxySQLAdminPort, ProxySQLAdminUser,
ProxySQLAdminPassword, ProxySQLAdminUseTLS, ProxySQLWriterHostgroup,
ProxySQLReaderHostgroup, and ProxySQLPreFailoverAction fields to the
Configuration struct with defaults (port 6032, user "admin",
pre-failover action "offline_soft").
Wire proxysql.InitHook() into both the continuous-discovery startup path
(go/logic/orchestrator.go) and the CLI path (go/app/cli.go), and add the
singleton/init logic in go/proxysql/init.go.
Copilot AI review requested due to automatic review settings March 23, 2026 23:37
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a3de86a9-ff6c-4d4c-a909-6595a9d04a38

📥 Commits

Reviewing files that changed from the base of the PR and between 97b7eda and db05f43.

📒 Files selected for processing (17)
  • .claude/worktrees/agent-a11dcc9a
  • .claude/worktrees/agent-a2c352e9
  • .claude/worktrees/agent-a3ae8388
  • .claude/worktrees/agent-a6ed0246
  • .claude/worktrees/agent-a82614b2
  • .claude/worktrees/agent-a8a6fb28
  • .claude/worktrees/agent-aa8f61c4/.gitkeep
  • .claude/worktrees/agent-ab100d9d
  • docs/proxysql-hooks.md
  • docs/superpowers/plans/2026-03-23-proxysql-hooks.md
  • go/app/cli.go
  • go/logic/orchestrator.go
  • go/logic/topology_recovery.go
  • go/proxysql/client.go
  • go/proxysql/client_test.go
  • go/proxysql/hook.go
  • go/proxysql/init.go

📝 Walkthrough

Walkthrough

Adds built-in ProxySQL pre/post-failover hooks: new go/proxysql package (client, hook, init), config fields, CLI commands, documentation, startup initialization, and calls into topology recovery to run PreFailover/PostFailover (errors logged/audited, non-blocking).

Changes

Cohort / File(s) Summary
Configuration
conf/orchestrator-sample.conf.json, go/config/config.go, go/config/config_test.go
Added ProxySQL config fields (admin address/port/user/password/TLS, writer/reader hostgroups, pre-failover action). Defaults set for ProxySQLAdminPort=6032, ProxySQLAdminUser="admin", ProxySQLPreFailoverAction="offline_soft"; unit test verifies defaults.
ProxySQL package
go/proxysql/client.go, go/proxysql/client_test.go, go/proxysql/hook.go, go/proxysql/hook_test.go, go/proxysql/init.go
New package implementing Admin client (DSN, Ping, Query, Exec), Hook (IsConfigured, PreFailover, PostFailover, SQL builders), singleton InitHook/GetHook, and unit tests for DSN and SQL generation/behavior.
Topology & startup
go/logic/orchestrator.go, go/logic/topology_recovery.go
Initialize ProxySQL hook at startup and invoke Hook.PreFailover after PreFailoverProcesses and Hook.PostFailover after KV distribution / during graceful takeover. Hook errors are logged and audited but do not change recovery control flow.
CLI
go/app/cli.go
Added proxysql-test (connectivity check) and proxysql-servers (query runtime_mysql_servers) commands with connectivity/error handling.
Documentation & plan
docs/proxysql-hooks.md, docs/superpowers/plans/2026-03-23-proxysql-hooks.md
New documentation and implementation plan describing ProxySQL integration, pre/post-failover sequence, CLI usage, Cluster guidance, and failure semantics.
Misc / repo metadata
.claude/worktrees/*
Several submodule/worktree pointer updates (no application logic changes).

Sequence Diagram(s)

sequenceDiagram
    participant Orch as Orchestrator
    participant Pre as PreFailoverProcesses
    participant Hook as ProxySQL Hook
    participant Admin as ProxySQL Admin (MySQL)
    participant KV as KVStore

    Orch->>Pre: Start failover (recoverDeadMaster)
    Pre-->>Orch: pre-failover scripts complete
    Orch->>Hook: PreFailover(oldMasterHost, oldMasterPort)
    Hook->>Admin: Execute drain SQL (UPDATE/LOAD MYSQL SERVERS TO RUNTIME)
    Admin-->>Hook: OK / error (logged)
    Hook-->>Orch: return
    Orch->>KV: DistributePairs (promote successor)
    KV-->>Orch: distribution complete
    Orch->>Hook: PostFailover(newMasterHost,...)
    Hook->>Admin: DELETE/INSERT runtime_mysql_servers, LOAD, SAVE
    Admin-->>Hook: OK / error (logged)
    Hook-->>Orch: return
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • Add built-in ProxySQL failover hooks #36 — Implements same go/proxysql package, config fields, CLI commands, and wiring of InitHook/GetHook into startup and recovery; appears to be a direct code-level match.

Poem

🐇 I hopped through code to join the fray,

Drained the master, then led the way.
Writers swapped and readers told,
LOAD and SAVE — the dance is bold.
A rabbit cheers: failovers now play!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add built-in ProxySQL failover hooks' is clear, concise, and accurately summarizes the primary change—implementing new ProxySQL integration hooks.
Linked Issues check ✅ Passed All coding objectives from issue #30 are met: ProxySQL client library implemented [3.1.3], pre/post-failover hooks implemented [3.1.4, 3.1.5], configuration options added [3.1.6], CLI commands added [3.1.7], unit tests included [3.1.8 partial].
Out of Scope Changes check ✅ Passed All changes are directly related to ProxySQL failover hook objectives: configuration fields, client/hook implementation, CLI commands, integration into topology recovery, and documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue30-proxysql-hooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances Orchestrator's capabilities by integrating direct support for ProxySQL failover management. It provides a robust, built-in mechanism to automatically update ProxySQL hostgroups during master recovery and graceful takeovers, thereby streamlining operations and reducing reliance on external scripting. The changes introduce a dedicated Go package for ProxySQL interaction, new configuration parameters, and command-line tools for easier management and testing.

Highlights

  • Built-in ProxySQL Failover Hooks: Orchestrator now includes native pre and post-failover hooks for ProxySQL, eliminating the need for custom scripts to manage hostgroups during failover events.
  • New ProxySQL Go Package: A new go/proxysql/ package has been introduced, containing modules for ProxySQL Admin API client management, failover hook logic with parameterized SQL queries, and singleton initialization.
  • Extensive Configuration Options: Eight new configuration fields have been added to the Config struct, allowing detailed control over ProxySQL Admin connection parameters (address, port, user, password, TLS) and hostgroup IDs (writer, reader), as well as the pre-failover action.
  • Integrated Failover Logic: Pre-failover, the old master is drained in ProxySQL (OFFLINE_SOFT or weight=0). Post-failover, hostgroups are updated to route traffic to the new master. This also applies to graceful takeovers.
  • New CLI Commands: Two new CLI commands, proxysql-test and proxysql-servers, have been added to facilitate testing connectivity to the ProxySQL Admin interface and displaying the runtime_mysql_servers table.
  • Non-blocking and Safe Operations: ProxySQL operations are non-blocking, meaning MySQL failover proceeds even if ProxySQL is unreachable. All SQL queries are parameterized to prevent injection vulnerabilities, and the hooks are nil-safe.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces built-in ProxySQL failover hooks, which is a significant enhancement for orchestrator's integration capabilities. The new go/proxysql package provides a robust client for the ProxySQL Admin API and implements the pre/post failover logic. The configuration options are well-documented, and the non-blocking nature of the hooks ensures that ProxySQL issues do not impede MySQL failover, which is a critical design choice. Unit tests for the new client and hook logic are also included, demonstrating good test coverage. Overall, this is a well-implemented feature that greatly simplifies ProxySQL integration.

Comment on lines +98 to +99
sqls = append(sqls, "REPLACE INTO mysql_servers (hostgroup_id, hostname, port, status) VALUES (?, ?, ?, 'OFFLINE_SOFT')")
args = append(args, []interface{}{readerHostgroup, oldHost, oldPort})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This line adds the old master to the reader hostgroup with OFFLINE_SOFT status after a new master is promoted. This behavior is unusual. Typically, a failed master is completely removed from active service or moved to a dedicated 'maintenance' hostgroup, not a 'reader' hostgroup, even if offline. This could lead to unexpected behavior or confusion for users. Please clarify the intent or consider removing the old master from all active hostgroups.

"ProxySQLAdminAddress": "127.0.0.1",
"ProxySQLAdminPort": 6032,
"ProxySQLAdminUser": "admin",
"ProxySQLAdminPassword": "admin",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The sample configuration uses "admin" as the ProxySQLAdminPassword. While this is a sample, it's generally a good practice to avoid suggesting default, insecure passwords even in examples, as users might copy-paste without changing. Consider using an empty string or a placeholder like "your_password" to encourage secure practices.

Suggested change
"ProxySQLAdminPassword": "admin",
"ProxySQLAdminPassword": "",

go/app/cli.go Outdated
}
case registerCliCommand("proxysql-test", "ProxySQL", `Test connectivity to ProxySQL Admin interface`):
{
proxysql.InitHook()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The proxysql.InitHook() function is called here, but it's already called globally in Cli function (line 217) and uses sync.Once. While safe, this call is redundant and can be removed for clarity.

go/app/cli.go Outdated
}
case registerCliCommand("proxysql-servers", "ProxySQL", `Show mysql_servers from ProxySQL`):
{
proxysql.InitHook()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the proxysql-test command, proxysql.InitHook() is called here again. This call is redundant as InitHook is already executed globally and is designed to run only once.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class ProxySQL integration to orchestrator by introducing built-in pre/post failover hooks that update ProxySQL hostgroups via the Admin API, plus CLI helpers and configuration/doc updates to make the feature usable out of the box.

Changes:

  • Introduce new go/proxysql package (Admin client + hook logic + singleton init + unit tests).
  • Wire ProxySQL hooks into dead-master recovery and graceful master takeover flows.
  • Add new config fields, sample config entries, CLI commands, and documentation for ProxySQL hooks.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
go/proxysql/init.go Singleton hook initialization and global accessor
go/proxysql/client.go ProxySQL Admin SQL client (DSN + exec/query/ping)
go/proxysql/hook.go Pre/post failover SQL generation and hook execution
go/proxysql/client_test.go Unit tests for client initialization/DSN
go/proxysql/hook_test.go Unit tests for hook no-op behavior and SQL generation
go/logic/topology_recovery.go Invoke pre/post failover hooks during recovery/takeover
go/logic/orchestrator.go Initialize ProxySQL hooks during continuous discovery startup
go/app/cli.go Add proxysql-test and proxysql-servers CLI commands
go/config/config.go Add new ProxySQL configuration fields + defaults
go/config/config_test.go Test default values for new ProxySQL config fields
docs/proxysql-hooks.md Document ProxySQL hook configuration and behavior
conf/orchestrator-sample.conf.json Add sample ProxySQL config keys

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to +41
var (
hookOnce sync.Once
defaultHook = NewHook(nil, 0, 0, "")
)

func InitHook() {
hookOnce.Do(func() {
client := NewClient(
config.Config.ProxySQLAdminAddress,
config.Config.ProxySQLAdminPort,
config.Config.ProxySQLAdminUser,
config.Config.ProxySQLAdminPassword,
config.Config.ProxySQLAdminUseTLS,
)
hook := NewHook(
client,
config.Config.ProxySQLWriterHostgroup,
config.Config.ProxySQLReaderHostgroup,
config.Config.ProxySQLPreFailoverAction,
)
if hook.IsConfigured() {
log.Infof("ProxySQL hooks enabled: admin=%s:%d writer_hg=%d reader_hg=%d",
config.Config.ProxySQLAdminAddress,
config.Config.ProxySQLAdminPort,
config.Config.ProxySQLWriterHostgroup,
config.Config.ProxySQLReaderHostgroup,
)
} else if config.Config.ProxySQLAdminAddress != "" && config.Config.ProxySQLWriterHostgroup == 0 {
log.Warningf("ProxySQL: ProxySQLAdminAddress is set but ProxySQLWriterHostgroup is 0 (unconfigured). ProxySQL hooks will be inactive.")
}
defaultHook = hook
})
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultHook is reassigned inside hookOnce.Do(...) but read via GetHook() without any synchronization. Since InitHook() is started in a goroutine (see go/logic/orchestrator.go), this creates a real data race (read/write of defaultHook) under the race detector, even though it’s “just” a pointer write. Consider storing the hook in an atomic.Value (or guarding with a mutex), or make initialization synchronous and ensure defaultHook is never mutated after other goroutines can call GetHook().

Copilot uses AI. Check for mistakes.
go ometrics.InitGraphiteMetrics()
go acceptSignals()
go kv.InitKVStores()
go proxysql.InitHook()
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting proxysql.InitHook() in its own goroutine means failover/recovery code can call proxysql.GetHook() concurrently while initialization is still updating the global hook, which can cause a data race and also makes hook availability timing-dependent. It should be safe to call proxysql.InitHook() synchronously during startup (before recovery routines can run), or switch the hook storage to an atomic/mutex-protected mechanism.

Suggested change
go proxysql.InitHook()
proxysql.InitHook()

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +27
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/", user, password, address, port)
if useTLS {
dsn += "?tls=true"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ProxySQL Admin DSN here does not set any driver-side timeouts (e.g. timeout, readTimeout, writeTimeout). In failure scenarios (ProxySQL down / network blackhole), sql.Open/db.Exec can block for a long time, which would delay failover even though ProxySQL errors are intended to be non-blocking. Suggest adding explicit DSN timeouts (similar to go/db/db.go’s MySQL DSNs) or reusing existing config timeout settings for the ProxySQL client.

Suggested change
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/", user, password, address, port)
if useTLS {
dsn += "?tls=true"
base := fmt.Sprintf("%s:%s@tcp(%s:%d)/", user, password, address, port)
// Add driver-side timeouts so ProxySQL Admin operations fail fast on network issues.
params := "timeout=1s&readTimeout=1s&writeTimeout=1s"
dsn := base + "?" + params
if useTLS {
dsn = base + "?tls=true&" + params

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +40
log.Infof("proxysql: pre-failover: draining old master %s:%d (action=%s)", oldMasterHost, oldMasterPort, h.preFailoverAction)
query, args := buildPreFailoverSQL(h.preFailoverAction, oldMasterHost, oldMasterPort, h.writerHostgroup)
if query == "" {
return nil
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PreFailover logs "draining old master" before determining whether the configured action actually produces any SQL. With ProxySQLPreFailoverAction="none", buildPreFailoverSQL returns an empty query and the hook becomes a no-op, but the log still claims it is draining. Consider moving the log after the query == "" check or logging a distinct "skipped" message for none.

Suggested change
log.Infof("proxysql: pre-failover: draining old master %s:%d (action=%s)", oldMasterHost, oldMasterPort, h.preFailoverAction)
query, args := buildPreFailoverSQL(h.preFailoverAction, oldMasterHost, oldMasterPort, h.writerHostgroup)
if query == "" {
return nil
}
query, args := buildPreFailoverSQL(h.preFailoverAction, oldMasterHost, oldMasterPort, h.writerHostgroup)
if query == "" {
log.Infof("proxysql: pre-failover: skipping drain of old master %s:%d (action=%s)", oldMasterHost, oldMasterPort, h.preFailoverAction)
return nil
}
log.Infof("proxysql: pre-failover: draining old master %s:%d (action=%s)", oldMasterHost, oldMasterPort, h.preFailoverAction)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
go/config/config_test.go (1)

40-54: Consider consistent assertion style and add missing default check.

The test uses t.Errorf while other tests in this file use the test.S(t) helper. Also, ProxySQLPreFailoverAction default ("offline_soft") is not tested.

🧪 Align with existing test style
 func TestProxySQLConfigDefaults(t *testing.T) {
 	cfg := newConfiguration()
-	if cfg.ProxySQLAdminPort != 6032 {
-		t.Errorf("expected default ProxySQLAdminPort=6032, got %d", cfg.ProxySQLAdminPort)
-	}
-	if cfg.ProxySQLAdminUser != "admin" {
-		t.Errorf("expected default ProxySQLAdminUser=admin, got %s", cfg.ProxySQLAdminUser)
-	}
-	if cfg.ProxySQLWriterHostgroup != 0 {
-		t.Errorf("expected default ProxySQLWriterHostgroup=0, got %d", cfg.ProxySQLWriterHostgroup)
-	}
-	if cfg.ProxySQLReaderHostgroup != 0 {
-		t.Errorf("expected default ProxySQLReaderHostgroup=0, got %d", cfg.ProxySQLReaderHostgroup)
-	}
+	test.S(t).ExpectEquals(cfg.ProxySQLAdminPort, 6032)
+	test.S(t).ExpectEquals(cfg.ProxySQLAdminUser, "admin")
+	test.S(t).ExpectEquals(cfg.ProxySQLWriterHostgroup, 0)
+	test.S(t).ExpectEquals(cfg.ProxySQLReaderHostgroup, 0)
+	test.S(t).ExpectEquals(cfg.ProxySQLPreFailoverAction, "offline_soft")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/config/config_test.go` around lines 40 - 54, Update
TestProxySQLConfigDefaults to use the project's test helper (call test.S(t) and
use the returned assertion object) instead of raw t.Errorf checks, and add an
assertion that cfg.ProxySQLPreFailoverAction equals "offline_soft"; locate the
test function TestProxySQLConfigDefaults and the configuration constructor
newConfiguration() to make these changes and replace the four t.Errorf checks
with equivalent assertions (e.g., s.Require().Equal or s.Equal) and add one more
assertion for ProxySQLPreFailoverAction.
go/proxysql/client.go (2)

62-74: Unusual API: Query returns *sql.DB requiring caller to close both.

This design requires callers to manage two resources (rows and db). While the pattern works (as shown in cli.go context snippet), it's error-prone. Consider either:

  1. Adding a helper method that handles iteration internally
  2. Documenting this clearly in the function comment

The current comment says "Caller must close both rows and db" which is good, but the order matters - rows.Close() should be called before db.Close().

📝 Suggested documentation improvement
-// Query executes a query and returns rows. Caller must close both rows and db.
+// Query executes a query and returns rows. Caller must close both rows and db,
+// with rows.Close() called before db.Close(). Example:
+//   rows, db, err := client.Query(...)
+//   if err != nil { return err }
+//   defer db.Close()
+//   defer rows.Close()  // defers execute LIFO, so rows closes first
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/proxysql/client.go` around lines 62 - 74, The Query method on Client
returns both *sql.Rows and *sql.DB which is error-prone; update the function by
either adding a convenience helper (e.g., Client.QueryFunc or Client.QueryAll)
that accepts a callback to iterate rows and closes rows then db internally, or
improve the doc comment on Client.Query to explicitly state the required close
order (call rows.Close() before db.Close()) and the rationale; reference
Client.Query, rows.Close(), db.Close(), and the new helper name if added so
callers can find and use the safe alternative.

38-44: Consider adding connection timeout.

The openDB() function doesn't configure any connection timeout. If ProxySQL is unreachable, operations could hang indefinitely. The MySQL driver supports timeout parameters in the DSN.

⏱️ Add connection timeout to DSN
 func NewClient(address string, port int, user, password string, useTLS bool) *Client {
 	if address == "" {
 		return nil
 	}
-	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/", user, password, address, port)
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/?timeout=10s&readTimeout=30s&writeTimeout=30s", user, password, address, port)
 	if useTLS {
-		dsn += "?tls=true"
+		dsn += "&tls=true"
 	}
go/app/cli.go (1)

217-217: Note: proxysql.InitHook() called redundantly.

InitHook() is called both at line 217 (for all CLI commands) and again at lines 1791/1812 within the ProxySQL-specific commands. This is safe due to the singleton pattern but slightly redundant. The calls within the specific commands can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/app/cli.go` at line 217, proxysql.InitHook() is being invoked globally for
all CLI commands and again inside the ProxySQL-specific command handlers; remove
the redundant calls inside the ProxySQL command implementations so InitHook() is
only called once globally (keep the single top-level proxysql.InitHook() call
and delete the additional proxysql.InitHook() invocations found inside the
ProxySQL-specific command functions).
go/proxysql/client_test.go (1)

21-27: Consider adding test for TLS DSN construction.

The test covers the non-TLS case but doesn't verify the DSN when useTLS=true. This would ensure the TLS parameter is correctly appended.

🧪 Add TLS DSN test
+func TestClientDSNWithTLS(t *testing.T) {
+	client := NewClient("127.0.0.1", 6032, "admin", "secret", true)
+	expected := "admin:secret@tcp(127.0.0.1:6032)/?tls=true"
+	if client.dsn != expected {
+		t.Errorf("expected DSN %q, got %q", expected, client.dsn)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/proxysql/client_test.go` around lines 21 - 27, Add a new unit test that
covers the TLS case: call NewClient with useTLS=true (e.g., in a new
TestClientDSN_TLS or by extending TestClientDSN), then assert client.dsn equals
the expected TLS DSN string (same base "admin:secret@tcp(127.0.0.1:6032)/" with
the TLS query param appended, e.g. including "?tls=true" or the specific TLS
query your NewClient generates); use the same t.Errorf style as TestClientDSN to
fail when client.dsn does not match the expected value.
go/proxysql/hook_test.go (1)

69-77: Consider adding content validation for post-failover SQL.

The test verifies the count of SQL statements and args but doesn't validate the actual SQL content. Consider adding assertions for expected SQL patterns similar to TestPreFailoverSQLGeneration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/proxysql/hook_test.go` around lines 69 - 77, TestPostFailoverSQLGeneration
currently only checks counts; extend it to validate the actual SQL content
returned by buildPostFailoverSQL so regressions are caught. Inside
TestPostFailoverSQLGeneration, iterate over the sqls and assert each statement
matches expected patterns (use strings.Contains or regexp) for the post-failover
operations (e.g., statements that set new-master host/port, remove or reweight
old-master) and assert corresponding args in args slice include the expected
values ("new-master", "old-master", 3306, and weight values like 10/20).
Reference the existing TestPreFailoverSQLGeneration for pattern/assertion style
and validate that sqls[i] content and args[i] align with the intended effect of
buildPostFailoverSQL.
go/proxysql/init.go (1)

10-13: Data race on defaultHook pointer.

defaultHook is written in InitHook() (line 40) and read in GetHook() (line 45) without synchronization. Since InitHook() is launched as a goroutine (per go/logic/orchestrator.go:590), concurrent calls to GetHook() during startup can race.

The current design mitigates impact because the initial NewHook(nil, 0, 0, "") returns a no-op hook (IsConfigured() returns false). However, this is still undefined behavior under the Go memory model.

♻️ Proposed fix using atomic.Pointer (Go 1.19+)
 import (
 	"sync"
+	"sync/atomic"
 
 	"github.com/proxysql/golib/log"
 	"github.com/proxysql/orchestrator/go/config"
 )
 
 var (
 	hookOnce    sync.Once
-	defaultHook = NewHook(nil, 0, 0, "")
+	defaultHook atomic.Pointer[Hook]
 )
+
+func init() {
+	defaultHook.Store(NewHook(nil, 0, 0, ""))
+}

Then update the assignment and getter:

-		defaultHook = hook
+		defaultHook.Store(hook)
 	})
 }
 
 func GetHook() *Hook {
-	return defaultHook
+	return defaultHook.Load()
 }

Also applies to: 44-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/proxysql/init.go` around lines 10 - 13, defaultHook is a plain pointer
causing a data race between InitHook() (which assigns it in a goroutine) and
GetHook() (which reads it); change defaultHook to an atomic.Pointer[Hook] (from
sync/atomic) and initialize it with Store(NewHook(...)) instead of direct
assignment, then update GetHook() to use Load() and handle a nil case by
returning a safe no-op hook (e.g., NewHook(nil,0,0,"")); ensure InitHook() uses
defaultHook.Store(...) and any reads use defaultHook.Load() so accesses are
atomic and race-free while preserving hookOnce and NewHook semantics.
go/proxysql/hook.go (1)

72-84: Consider logging a warning for unknown preFailoverAction values.

The default case silently falls back to offline_soft. While this is safe, logging a warning would help operators identify configuration typos.

💡 Optional improvement
 	default:
+		log.Warningf("proxysql: unknown preFailoverAction '%s', defaulting to 'offline_soft'", action)
 		return "UPDATE mysql_servers SET status='OFFLINE_SOFT' WHERE hostname=? AND port=? AND hostgroup_id=?", args
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go/proxysql/hook.go` around lines 72 - 84, The default branch in
buildPreFailoverSQL silently treats unknown action values as "offline_soft";
update buildPreFailoverSQL to log a warning when the action is unrecognized
(e.g., log.Printf or the package's logger) indicating the unexpected
preFailoverAction and that it's defaulting to offline_soft, so operators can
spot configuration typos; reference the function name buildPreFailoverSQL and
the action parameter when adding the warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/proxysql-hooks.md`:
- Around line 61-71: The fenced code block containing the ASCII diagram (the
block that begins with triple backticks before the "Dead master detected"
diagram) is missing a language specifier; update that opening fence to include a
language label such as text or plaintext (e.g., change ``` to ```text) so the
static analysis error is resolved and syntax/highlighting tools treat the
diagram as plain text.
- Around line 53-57: The docs miss describing that when readerHostgroup > 0 the
old master is also added back to the reader hostgroup with OFFLINE_SOFT status;
update the prose to include a step (after removing old master from writer
hostgroup) stating that the old master is added to the reader hostgroup with
OFFLINE_SOFT so it doesn't immediately accept traffic (mirror the behavior
implemented in hook.go around the readerHostgroup/ OFFLINE_SOFT logic).

In `@go/app/cli.go`:
- Around line 1789-1809: The proxysql-test command should defensively verify the
client returned by proxysql.NewClient is not nil before calling client.Ping to
avoid a potential nil pointer panic; update the proxysql-test case to mirror the
proxysql-servers check: after creating client with proxysql.NewClient(...) check
if client == nil and call log.Fatal with a clear message (e.g., "Unable to
create ProxySQL client") if nil, otherwise proceed to call client.Ping() and
handle its error as already done.

In `@go/logic/orchestrator.go`:
- Line 39: Move the "github.com/proxysql/orchestrator/go/proxysql" import into
the same block as the other internal imports from
"github.com/proxysql/orchestrator/go/..." in orchestrator.go and ensure the
internal imports are sorted alphabetically (so proxysql is ordered correctly
among peers); run gofmt/goimports to verify the import grouping and ordering is
fixed.

In `@go/logic/topology_recovery.go`:
- Around line 39-42: Reorder the import block in topology_recovery.go to satisfy
Go conventions: place standard library imports first, a blank line, then
third-party/external packages (e.g., "github.com/patrickmn/go-cache",
"github.com/rcrowley/go-metrics"), another blank line, and finally internal
project imports (e.g., "github.com/proxysql/orchestrator/go/proxysql",
"github.com/proxysql/orchestrator/go/util"); you can auto-fix by running gofmt
-w go/logic/topology_recovery.go or goimports -w go/logic/topology_recovery.go
to update the import ordering.

---

Nitpick comments:
In `@go/app/cli.go`:
- Line 217: proxysql.InitHook() is being invoked globally for all CLI commands
and again inside the ProxySQL-specific command handlers; remove the redundant
calls inside the ProxySQL command implementations so InitHook() is only called
once globally (keep the single top-level proxysql.InitHook() call and delete the
additional proxysql.InitHook() invocations found inside the ProxySQL-specific
command functions).

In `@go/config/config_test.go`:
- Around line 40-54: Update TestProxySQLConfigDefaults to use the project's test
helper (call test.S(t) and use the returned assertion object) instead of raw
t.Errorf checks, and add an assertion that cfg.ProxySQLPreFailoverAction equals
"offline_soft"; locate the test function TestProxySQLConfigDefaults and the
configuration constructor newConfiguration() to make these changes and replace
the four t.Errorf checks with equivalent assertions (e.g., s.Require().Equal or
s.Equal) and add one more assertion for ProxySQLPreFailoverAction.

In `@go/proxysql/client_test.go`:
- Around line 21-27: Add a new unit test that covers the TLS case: call
NewClient with useTLS=true (e.g., in a new TestClientDSN_TLS or by extending
TestClientDSN), then assert client.dsn equals the expected TLS DSN string (same
base "admin:secret@tcp(127.0.0.1:6032)/" with the TLS query param appended, e.g.
including "?tls=true" or the specific TLS query your NewClient generates); use
the same t.Errorf style as TestClientDSN to fail when client.dsn does not match
the expected value.

In `@go/proxysql/client.go`:
- Around line 62-74: The Query method on Client returns both *sql.Rows and
*sql.DB which is error-prone; update the function by either adding a convenience
helper (e.g., Client.QueryFunc or Client.QueryAll) that accepts a callback to
iterate rows and closes rows then db internally, or improve the doc comment on
Client.Query to explicitly state the required close order (call rows.Close()
before db.Close()) and the rationale; reference Client.Query, rows.Close(),
db.Close(), and the new helper name if added so callers can find and use the
safe alternative.

In `@go/proxysql/hook_test.go`:
- Around line 69-77: TestPostFailoverSQLGeneration currently only checks counts;
extend it to validate the actual SQL content returned by buildPostFailoverSQL so
regressions are caught. Inside TestPostFailoverSQLGeneration, iterate over the
sqls and assert each statement matches expected patterns (use strings.Contains
or regexp) for the post-failover operations (e.g., statements that set
new-master host/port, remove or reweight old-master) and assert corresponding
args in args slice include the expected values ("new-master", "old-master",
3306, and weight values like 10/20). Reference the existing
TestPreFailoverSQLGeneration for pattern/assertion style and validate that
sqls[i] content and args[i] align with the intended effect of
buildPostFailoverSQL.

In `@go/proxysql/hook.go`:
- Around line 72-84: The default branch in buildPreFailoverSQL silently treats
unknown action values as "offline_soft"; update buildPreFailoverSQL to log a
warning when the action is unrecognized (e.g., log.Printf or the package's
logger) indicating the unexpected preFailoverAction and that it's defaulting to
offline_soft, so operators can spot configuration typos; reference the function
name buildPreFailoverSQL and the action parameter when adding the warning.

In `@go/proxysql/init.go`:
- Around line 10-13: defaultHook is a plain pointer causing a data race between
InitHook() (which assigns it in a goroutine) and GetHook() (which reads it);
change defaultHook to an atomic.Pointer[Hook] (from sync/atomic) and initialize
it with Store(NewHook(...)) instead of direct assignment, then update GetHook()
to use Load() and handle a nil case by returning a safe no-op hook (e.g.,
NewHook(nil,0,0,"")); ensure InitHook() uses defaultHook.Store(...) and any
reads use defaultHook.Load() so accesses are atomic and race-free while
preserving hookOnce and NewHook semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 283df9ab-3bb5-453b-b4c4-29080bf5ab78

📥 Commits

Reviewing files that changed from the base of the PR and between 78ac10a and 97b7eda.

📒 Files selected for processing (12)
  • conf/orchestrator-sample.conf.json
  • docs/proxysql-hooks.md
  • go/app/cli.go
  • go/config/config.go
  • go/config/config_test.go
  • go/logic/orchestrator.go
  • go/logic/topology_recovery.go
  • go/proxysql/client.go
  • go/proxysql/client_test.go
  • go/proxysql/hook.go
  • go/proxysql/hook_test.go
  • go/proxysql/init.go

…prove logging

- Use atomic.Pointer[Hook] for defaultHook to fix data race
- Add DSN timeouts (1s connect/read/write) to ProxySQL client
- Fix import ordering in orchestrator.go and topology_recovery.go
- Move pre-failover log after query=="" check to avoid misleading message
- Add log.Warningf for unknown preFailoverAction in buildPreFailoverSQL
- Remove redundant InitHook() calls in CLI proxysql-test/proxysql-servers
- Add nil check for client in proxysql-test command
- Fix docs: add missing reader hostgroup step, use ```text fence
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 3.1: Official ProxySQL failover hooks

2 participants