Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ tar xzf proxysql-<version>-linux-amd64.tar.gz
The archive contains `bin/proxysql`, a sample `etc/proxysql.cnf`, the `systemd/`
units, and helper tools. The v4.0 build additionally ships the runtime plugins
under `lib/proxysql/` (`ProxySQL_MySQLX_Plugin.so`, `ProxySQL_GenAI_Plugin.so`).
See [AWS locality-aware backend selection](doc/aws-locality-awareness.md) for
the optional external-provider contract and MySQL configuration controls.

Alternatively you can also use the available repositories:

Expand Down
34 changes: 29 additions & 5 deletions doc/PLUGIN_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ All types are defined in `include/ProxySQL_Plugin.h`:
```cpp
struct ProxySQL_PluginDescriptor {
const char *name; // Human-readable plugin name
uint32_t abi_version; // PROXYSQL_PLUGIN_ABI_VERSION (1 through 5)
uint32_t abi_version; // PROXYSQL_PLUGIN_ABI_VERSION (1 through 8)
proxysql_plugin_init_cb init; // bool (*)(ProxySQL_PluginServices *)
proxysql_plugin_start_cb start; // bool (*)()
proxysql_plugin_stop_cb stop; // bool (*)()
Expand All @@ -115,7 +115,7 @@ struct ProxySQL_PluginDescriptor {
| Field | Type | Description |
|--------------------|---------------|-----------------------------------------------------------|
| `name` | `const char*` | Plugin identifier, used in logging. |
| `abi_version` | `uint32_t` | Set from `PROXYSQL_PLUGIN_ABI_VERSION`. Value `1` = pre-chassis descriptor (six fields). Value `2` = adds `register_schemas` (four-phase lifecycle). Value `3` = `ProxySQL_PluginServices` adds `register_runtime_view`; ABI 4 adds `db_kind` to that view; ABI 5 adds the AWS IAM provider callbacks. A v3/v3.1 core rejects `abi_version > 1`; the current PROXYSQL40 core accepts `[1, 5]`. |
| `abi_version` | `uint32_t` | Set from `PROXYSQL_PLUGIN_ABI_VERSION`. Value `1` is the pre-chassis six-field descriptor; ABI 2 adds `register_schemas`; ABI 3 adds `register_runtime_view`; ABI 4 adds the view's `db_kind`; ABI 5 adds IAM provider install/limits; ABI 6 adds metadata-provider install; ABI 7 adds the MySQL locality projection callback; ABI 8 adds IAM provider rollback. A v3/v3.1 core rejects `abi_version > 1`; the current PROXYSQL40 core accepts `[1, 8]`. |
| `init` | callback | Phase D — called with live services; register tables and commands here (or finish context setup if `register_schemas` already did it). |
| `start` | callback | Phase E — start threads, open sockets, load config. |
| `stop` | callback | Called on shutdown. Pairs with `init`, not `start`: if `init` returned true and `start` later failed, `stop` is still called so the plugin can release resources it allocated in `init`. |
Expand All @@ -128,12 +128,12 @@ Return `true` on success, `false` on failure. A `false` return from

#### ABI version

`include/ProxySQL_Plugin.h` exposes `PROXYSQL_PLUGIN_ABI_VERSION` (5 under
`include/ProxySQL_Plugin.h` exposes `PROXYSQL_PLUGIN_ABI_VERSION` (8 under
PROXYSQL40, undefined in pre-chassis builds — the descriptor is then a
legacy six-field struct with `abi_version = 1`). Plugins MUST assign
`abi_version` from this macro rather than hard-coding a literal; the
core's loader uses it to detect layout skew and reject plugins built
for an unsupported ABI. ABIs 3 through 5 keep the descriptor layout
for an unsupported ABI. ABIs 3 through 8 keep the descriptor layout
identical to ABI 2; each adds only tail fields to service or view structs.
Plugins compiled against an older ABI therefore still load on the current
core, where the trailing fields remain invisible to them. See
Expand Down Expand Up @@ -172,6 +172,12 @@ struct ProxySQL_PluginServices {
// ABI 5 tail extensions, live only while init() is running:
proxysql_plugin_install_aws_iam_token_source_cb install_aws_iam_token_source;
proxysql_plugin_get_aws_iam_limits_cb get_aws_iam_limits;
// ABI 6 tail extension, live only while init() is running:
proxysql_plugin_install_aws_metadata_provider_cb install_aws_metadata_provider;
// ABI 7 tail extension, live during register_schemas() and init():
proxysql_plugin_refresh_mysql_aws_locality_stats_cb refresh_mysql_aws_locality_stats;
// ABI 8 tail extension, live only while init() is running:
proxysql_plugin_uninstall_aws_iam_token_source_cb uninstall_aws_iam_token_source;
};
```

Expand All @@ -183,6 +189,24 @@ module. A plugin must not call this callback outside `init()` or retain the
service pointer after initialization. Without an installed provider, IAM
authentication remains available as a policy but fails closed.

`install_aws_metadata_provider` installs an optional external asynchronous
metadata provider for MySQL locality selection. Core owns the retained lease
registry: new leases are rejected during shutdown, active manager/callback
leases drain, the provider's `shutdown()` and destroy callback run, and the
extra module handle closes last. Without an installed provider, locality uses
configured weights and reports the fixed `provider_unavailable` category.

`refresh_mysql_aws_locality_stats` projects the current MySQL-owned immutable
locality snapshot into a schema supplied by the calling plugin. It performs no
provider or network I/O and is live during `register_schemas()` so an external
provider can register its stats table and runtime-view callback together.
Public core deliberately registers no locality table on its own.

`uninstall_aws_iam_token_source` lets the same plugin roll back its IAM source
when a later initialization step fails. It rejects a null or different source,
stops new leases, drains active leases, destroys the source, and releases the
retained module handle before returning.

### Service Callbacks

#### `register_table`
Expand Down Expand Up @@ -553,7 +577,7 @@ void register_stats_table(ProxySQL_PluginServices& services,
- **No dependency resolution**: Plugins are loaded in the order listed in
`proxysql.cnf`. If one plugin depends on another, the dependency must be
listed first.
- **ABI version range**: The current core accepts `abi_version` values in `[1, 5]`. Newly built plugins should set `abi_version = PROXYSQL_PLUGIN_ABI_VERSION`.
- **ABI version range**: The current core accepts `abi_version` values in `[1, 8]`. Newly built plugins should set `abi_version = PROXYSQL_PLUGIN_ABI_VERSION`.
- **Compiler coupling**: Plugins must match the ProxySQL core's C++ compiler
and standard library due to `std::string` in `ProxySQL_PluginCommandResult`.

Expand Down
114 changes: 114 additions & 0 deletions doc/aws-locality-awareness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# AWS locality-aware MySQL backend selection

ProxySQL 4.0 can apply temporary locality multipliers while selecting eligible
Amazon RDS and Aurora MySQL backends. The MySQL module owns the configuration,
policy validation, immutable selection snapshot, and effective-weight
calculation. Locality never changes `mysql_servers.weight`,
`runtime_mysql_servers.weight`, saved configuration, or ProxySQL Cluster
checksums.

Locality metadata is supplied asynchronously by an optional compatible
external provider. Without a provider, or when metadata is unavailable or too
old, selection stays neutral and uses the configured server weights.

## Hostgroup policy

Add `aws.locality_awareness` to the existing
`mysql_hostgroup_attributes.hostgroup_settings` JSON. Both multipliers are
required:

```sql
INSERT INTO mysql_hostgroup_attributes(hostgroup_id, hostgroup_settings)
Comment thread
renecannao marked this conversation as resolved.
VALUES (
10,
'{
"aws": {
"locality_awareness": {
"same_region_multiplier": 2.0,
"same_az_multiplier": 4.0,
"refresh_interval_seconds": 300,
"stale_ttl_seconds": 1800
}
}
}'
);

LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
```

The accepted values are:

```text
1.0 <= same_region_multiplier <= same_az_multiplier <= 10.0

refresh_interval_seconds default: 300
stale_ttl_seconds default: 1800

30 <= refresh_interval_seconds <= 86400
refresh_interval_seconds <= stale_ttl_seconds <= 604800
```

Multipliers are JSON numbers. An invalid `aws.locality_awareness` object
disables locality bias for that hostgroup when servers are loaded. Diagnostics
identify the rejected field and hostgroup without logging the supplied value.

## Master switch

The process-wide MySQL variable defaults to `false`:

```sql
SET mysql-aws_locality_awareness = true;
LOAD MYSQL VARIABLES TO RUNTIME;
SAVE MYSQL VARIABLES TO DISK;
```

Disabling the variable immediately restores configured-weight selection,
cancels or supersedes outstanding locality requests, and stops new refresh
scheduling. It does not change existing backend connections or server rows.

## Selection contract

For each selection attempt, after the normal health, capacity, lag, GTID,
backoff, and session-compatibility checks, ProxySQL calculates:

```text
remote or unknown configured_weight
same Region, different AZ int(configured_weight * same_region_multiplier)
same AZ int(configured_weight * same_az_multiplier)
```

Conversion to an integer truncates toward zero. The tiers are not cumulative,
and configured weight zero stays zero. Global hostgroup selection and
thread-local idle-connection reuse use the same immutable snapshot and never
perform provider or network work on the selection path.

ProxySQL recognizes eligible RDS and Aurora endpoint shapes to form neutral
metadata requests. A compatible provider is responsible for authoritative
endpoint discovery and normalized Region, Availability Zone, and account
metadata. Custom CNAMEs, arbitrary MySQL hosts, proxy endpoints, malformed
names, and endpoints the provider cannot confirm remain neutral.

## Provider absence and failures

The public provider interface uses retained leases so shutdown rejects new
work, drains active callbacks, joins the locality manager worker, and only then
permits provider destruction and module unload. Provider absence is exposed as
the fixed `provider_unavailable` category.

Metadata states are `pending`, `fresh`, `stale`, `expired`, `error`, and
`disabled`. Only `fresh` and unexpired `stale` values activate a multiplier.
All other states use multiplier `1.0`, so effective weights equal configured
weights. A failed refresh can retain the last successful value through the
bounded stale TTL; it becomes neutral after expiry.

An external provider may register a read-only runtime diagnostics table using
the plugin table/view services and the MySQL-owned projection callback. Public
core does not register `stats_mysql_aws_locality`; without a provider that
registers it, the table does not exist.

## Rollback

Set `mysql-aws_locality_awareness` to `false` and load MySQL variables to
runtime. To remove a policy, delete the `aws.locality_awareness` object from
that hostgroup's `hostgroup_settings`, then load MySQL servers to runtime.
141 changes: 141 additions & 0 deletions include/Aws_Locality_Manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#ifndef __CLASS_AWS_LOCALITY_MANAGER_H
#define __CLASS_AWS_LOCALITY_MANAGER_H

#include "Aws_Locality_Types.h"
#include "json_fwd.hpp"

#include <chrono>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>

AwsLocalityPolicy parse_aws_locality_policy(
const nlohmann::json& policy_json,
uint32_t hostgroup_id,
AwsLocalityPolicyError& error);

AwsEndpointCandidate recognize_rds_endpoint(
uint32_t hostgroup_id,
std::string_view hostname,
uint16_t port);

AwsLocalityClass classify_aws_locality(
const AwsLocalLocation& local,
const AwsBackendLocation& backend);

uint64_t aws_locality_effective_weight(
int64_t configured_weight,
double multiplier);

uint64_t aws_locality_saturating_add(uint64_t lhs, uint64_t rhs);
size_t aws_locality_weighted_index(
const uint64_t* weights,
size_t count,
uint64_t random_value);

using AwsMetadataProviderDestroyFn = void (*)(AwsMetadataProvider*);

class AwsMetadataProviderLease {
public:
AwsMetadataProviderLease() = default;
~AwsMetadataProviderLease();
AwsMetadataProviderLease(AwsMetadataProviderLease&& other) noexcept;
AwsMetadataProviderLease& operator=(AwsMetadataProviderLease&& other) noexcept;

AwsMetadataProvider* get() const { return provider_; }
AwsMetadataProvider* operator->() const { return provider_; }
explicit operator bool() const { return provider_ != nullptr; }

AwsMetadataProviderLease(const AwsMetadataProviderLease&) = delete;
AwsMetadataProviderLease& operator=(const AwsMetadataProviderLease&) = delete;

private:
explicit AwsMetadataProviderLease(AwsMetadataProvider* provider)
: provider_(provider) {}
void release();
AwsMetadataProvider* provider_ { nullptr };

friend AwsMetadataProviderLease acquire_global_aws_metadata_provider();
};

bool install_global_aws_metadata_provider(
AwsMetadataProvider* provider,
AwsMetadataProviderDestroyFn destroy,
void* module_handle);
AwsMetadataProviderLease acquire_global_aws_metadata_provider();
void shutdown_global_aws_metadata_provider();

struct AwsLocalitySnapshotEntry {
uint32_t hostgroup_id { 0 };
std::string hostname;
uint16_t port { 0 };
AwsEndpointType endpoint_type { AwsEndpointType::unknown };
int64_t configured_weight { 0 };
AwsLocalLocation local;
AwsBackendLocation backend;
AwsLocalityClass locality { AwsLocalityClass::unknown };
double multiplier { 1.0 };
AwsLocalityMetadataStatus status { AwsLocalityMetadataStatus::disabled };
int64_t last_success_timestamp { 0 };
int64_t last_attempt_timestamp { 0 };
std::string failure_category;
};

struct AwsLocalitySnapshot {
uint64_t generation { 0 };
bool enabled { false };
std::unordered_multimap<uint64_t, AwsLocalitySnapshotEntry> entries;
std::unordered_set<uint32_t> hostgroups;

const AwsLocalitySnapshotEntry* find(
uint32_t hostgroup_id,
std::string_view hostname,
uint16_t port) const;
uint64_t effective_weight(
uint32_t hostgroup_id,
std::string_view hostname,
uint16_t port,
int64_t configured_weight) const;
Comment thread
renecannao marked this conversation as resolved.
bool has_hostgroup(uint32_t hostgroup_id) const {
return hostgroups.find(hostgroup_id) != hostgroups.end();
}
};

struct AwsLocalityManagerConfig {
using SteadyClock = std::function<std::chrono::steady_clock::time_point()>;
using WallClock = std::function<std::chrono::system_clock::time_point()>;

AwsLocalityManagerConfig();
SteadyClock steady_clock;
WallClock wall_clock;
std::chrono::milliseconds request_timeout { std::chrono::seconds(5) };
std::chrono::milliseconds disable_wait_timeout { std::chrono::milliseconds(250) };
std::function<void()> before_completion;
};

class MySQLAwsLocalityManager {
public:
explicit MySQLAwsLocalityManager(AwsLocalityManagerConfig config = {});
~MySQLAwsLocalityManager();
MySQLAwsLocalityManager(const MySQLAwsLocalityManager&) = delete;
MySQLAwsLocalityManager& operator=(const MySQLAwsLocalityManager&) = delete;

void configure(std::vector<AwsLocalityHostgroupConfig> hostgroups);
void set_enabled(bool enabled);
void request_refresh();
std::shared_ptr<const AwsLocalitySnapshot> snapshot() const;
std::vector<AwsLocalitySnapshotEntry> diagnostic_rows() const;
void shutdown();

private:
class Impl;
std::unique_ptr<Impl> impl_;
};

#endif // __CLASS_AWS_LOCALITY_MANAGER_H
Loading
Loading