feat(plugin-redis): add Sentinel and Cluster connection modes (#1021) - #1215
Merged
datlechin merged 6 commits intoAug 20, 2026
Merged
Conversation
tonghs
marked this pull request as ready for review
May 11, 2026 09:05
tonghs
force-pushed
the
feat/plugin-redis-sentinel-1021
branch
from
May 12, 2026 05:06
ad0454a to
08b678b
Compare
tonghs
force-pushed
the
feat/plugin-redis-sentinel-1021
branch
from
May 19, 2026 14:47
1ab7f96 to
a0d1d25
Compare
# Conflicts: # CHANGELOG.md # Plugins/RedisDriverPlugin/RedisPluginDriver.swift # TablePro/Resources/Localizable.xcstrings # TablePro/Views/ConnectionForm/ViewModels/AuthPaneViewModel.swift # docs/databases/redis.mdx
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1021 in full: Redis Sentinel and Redis Cluster, not just Phase 1.
Reworked on top of @tonghs's original Sentinel work, rebased onto current
main. The Sentinel design changed after measurement (see below); the Cluster half is new.What this does
A Connection Mode field on the Redis form picks Standalone, Sentinel or Cluster.
Sentinel asks the quorum which node is the primary and opens the data connection to it. It learns the quorum's other Sentinels on the first lookup, so a later one still works when the node you listed is down, and it re-checks the primary's address on every health check so a failover moves the connection with it.
Cluster reads the shard map, opens a connection to every primary, and routes each command to the shard that owns its key. It follows
MOVEDandASK, splits multi-key commands across shards and recombines them, sums key counts cluster-wide, and walks every shard when browsing keys.Pointing a mode at the wrong kind of server now says which field to change, instead of connecting and then failing on the first command. That was the issue's actual complaint: "both failure modes look like bugs rather than missing features".
Why the design is what it is
Everything below was measured against a live Redis 8.10.1 lab (6-node cluster, 3 Sentinels, a standalone primary/replica pair). Several measurements overturned the obvious design.
Failover cannot be detected from the data connection. After
SENTINEL failover, the demoted primary kept reportingrole:masterand accepting writes with+OKfor ~4.8s, and those writes were gone afterwards. No-READONLYever arrived andmaster_failover_statestayedno-failover. So the address is re-checked against the quorum rather than inferred from what the node says about itself.Transactions are off under Cluster.
MULTIcarries no key, so a client must pick a node before it has seen one, and Redis rejects a queued command that hashes elsewhere:MULTI; SET beta yon the wrong node gives-MOVEDat QUEUED time and-EXECABORT. On three shards that kills most legal single-slot transactions.SaveChangesalready runs statements serially when the flag is false, so this is a capability flag rather than new code.Multi-key commands split by slot, not by node. The spec is explicit that the split follows hash slots "even if all the slots are managed by the same shard". Grouping by node passed every unit test and then failed against the real cluster with
CROSSSLOT; the live harness caught it.An unknown command is routed as keyless. Hashing
argv[1]misroutes container commands:SCRIPT LOADreturns+OKfrom one node and neverMOVED, so nothing corrects the guess.The SCAN cursor names the node by cluster id. Redis accepts another node's cursor without complaint and answers with a plausible-looking page, so a cursor keyed on list position loses keys silently whenever the topology moves.
Routing comes from the server. One
COMMANDcall at connect (112KB, one round trip) supplies key positions and Redis 7 request/response policy tips, including the ones that only exist on a subcommand entry (COMMAND INFO configcarries no tips;config|setis what saysall_nodes). Redis 6 reports no tips, so the curated table fills those in rather than being replaced.COMMAND GETKEYSresolvesmovablekeyscommands likeSORT ... STORE.No hiredis-cluster. hiredis has zero cluster symbols, but holding N contexts is fine: 6 concurrent connections over 6 threads ran 1200 commands clean, and
MOVED/ASKarrive as ordinary error replies with the context still usable. That answers the issue's Q3. Q1 is one Redis type with a mode field (matching DBeaver's "Deployment"), Q2 is one aggregated keyspace (matching RedisInsight and ARDM), Q4 is one credential per plane.CRC16 slot assignment is verified against
CLUSTER KEYSLOTon 20 vectors including UTF-8, the empty key and every degenerate hash-tag case.Fixes that ship with it
Each one makes the feature unsafe or incomplete if left out.
-READONLY,-WRONGTYPEand the rest back as ordinary replies, and every write path discarded the reply. Measured against a real read-only replica: the grid cleared its dirty flags and the write was lost.-READONLYis exactly the signal Sentinel failover turns on, so Sentinel could not be correct while this stood.REDIS_ERR_IO, the same code as a failed write, soINCRran twice. The write and the read are now split, and only a command that provably never reached the server, or that cannot change anything, is replayed.SCANswallowed an error reply and painted "no keys". Cluster makes SCAN the main browse path.withBrandingreplaced them with the app's copy, so Redis was missing Key Separator and its whole ElastiCache IAM section, and no plugin could add a field without changing the app too.SET key value KEEPTTLcleared the TTL and reported OK, because an unrecognised option was dropped before the command went on the wire. Unmodelled options now pass through untouched, the wayEXPIREalready did.host, which is blank whenever the form shows a host list. This affected MongoDB replica sets too.Security
The
withBrandingfix has a consequence worth calling out: because the app did not know those plugin fields existed, it did not know which were secrets. A Snowflake password, a BigQuery service account key or OAuth refresh token, an Elasticsearch API key, a DuckDB token and a DynamoDB secret access key were written to the connections file in plain text and included when a connection was exported. They now go to the Keychain and are stripped from exports. Existing values move on the next save.Verification
build(app + 14 bundled plugins)test, 39 suiteslint(TablePro,Plugins/RedisDriverPlugin, tests, UI tests)The live harness drives the real channels against the lab through hiredis: routing across shards,
DBSIZEsumming (cluster=27 sum(nodes)=27),KEYSunioning,MGETorder across shards,EXISTSsumming,SCANcovering every shard, cross-slot refusal, same-slotRENAME, Sentinel resolution, quorum discovery, and both wrong-mode messages.Not covered: plugin packaging and notarization (only a release tag runs those), and the
AllPluginsaggregate, which fails on this machine inside theoracle-niofork's@TaskLocalmacro, a known local-toolchain issue unrelated to this change (zero Redis errors in that log). CI runs it.New pure logic lives in files listed under
TableProTests.sources, so it is compiled and tested rather than mirrored; PR #1215's strayTableProTests/PluginTestSources/symlink is removed in favour of that existing convention. The UI test drives the real connection form through the three modes.Notes
.dropdown,.hostList,.secure,visibleWhenandConnectionStage.customall already exist, and the@frozenFieldType/FieldSectionare untouched. NocurrentPluginKitVersionbump, norelease-all-plugins.sh.RedisPlugin.pluginVersion1.0.0 → 1.1.0. Redis is bundled and also has aplugin-redis-v*registry arm.sslCaCertPathform field. It is the legacy spelling of the SSL/TLS pane's CA certificate, read only as a fallback, and stored values keep working.Localizable.xcstrings; they fall back to English, as plugin strings already do.Fixes #1021