From a4f42498ce0532d77e5a4738e3cf30bc2b2f6f12 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Thu, 3 Sep 2026 18:53:34 +0300 Subject: [PATCH 01/15] Feature: Add core support for the Anser extension Two additive changes, both usable on their own, so that the Anser adaptive-information-sharing subsystem can live entirely in gpcontrib/anser instead of in the server. libpq/auth.c gains a pair of hooks for extensions that maintain their own internal connections: CustomAuthClaims_hook recognizes such a connection from a marker in its startup packet, and CustomAuthCheckPassword_hook validates the credential it sends as password. Both are consulted before pg_hba.conf, mirroring the existing PARALLEL RETRIEVE CURSOR path. The wire exchange stays in auth.c, so no static helper is exported. lib/bloomfilter.c gains accessors for the otherwise opaque filter -- bloom_bitset_bytes(), bloom_bitset_data() -- plus bloom_create_from_bitset(), which builds a filter and loads its bitset in one step, rejecting a wrongly-sized bitset instead of partially loading it. Together they let a caller outside the backend serialize and reconstruct a filter. Co-Authored-By: Claude Opus 5 (1M context) --- src/backend/lib/bloomfilter.c | 41 +++++++++++++++++++++++++++++++ src/backend/libpq/auth.c | 45 +++++++++++++++++++++++++++++++++++ src/include/lib/bloomfilter.h | 6 +++++ src/include/libpq/auth.h | 16 +++++++++++++ 4 files changed, 108 insertions(+) diff --git a/src/backend/lib/bloomfilter.c b/src/backend/lib/bloomfilter.c index bde46f5b6e6..c7f0ed3f0fd 100644 --- a/src/backend/lib/bloomfilter.c +++ b/src/backend/lib/bloomfilter.c @@ -293,6 +293,47 @@ mod_m(uint32 val, uint64 m) return val & (m - 1); } +Size +bloom_bitset_bytes(const bloom_filter *filter) +{ + return filter != NULL ? (Size) (filter->m / BITS_PER_BYTE) : 0; +} + +const unsigned char * +bloom_bitset_data(const bloom_filter *filter) +{ + return filter != NULL ? filter->bitset : NULL; +} + +/* + * Create a filter sized exactly as bloom_create(total_elems, work_mem, seed) and + * initialize its bitset from the supplied bytes in one step. Returns NULL if the + * supplied length is not the filter's bitset size, so a wrongly-sized bitset is + * rejected rather than partially loaded. This is the only supported way to + * populate a filter from serialized bytes; afterwards a filter is only ever grown + * by bloom_add_element (or a raw bitwise OR of two equal-sized serialized parts, + * as Anser's coordinator fold does). + */ +bloom_filter * +bloom_create_from_bitset(int64 total_elems, int bloom_work_mem, uint64 seed, + const unsigned char *bitset, Size bitset_len) +{ + bloom_filter *filter; + + if (bitset == NULL) + return NULL; + + filter = bloom_create(total_elems, bloom_work_mem, seed); + if (bitset_len != (Size) (filter->m / BITS_PER_BYTE)) + { + bloom_free(filter); + return NULL; + } + + memcpy(filter->bitset, bitset, bitset_len); + return filter; +} + double bloom_false_positive_rate(bloom_filter *filter) { diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index b6021e169dd..9fcb196a33d 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -256,6 +256,13 @@ static int PerformRadiusTransaction(const char *server, const char *secret, cons */ ClientAuthentication_hook_type ClientAuthentication_hook = NULL; +/* + * These hooks let an extension authenticate its own internal connections + * before pg_hba.conf is consulted; see custom_conn_authentication() below. + */ +CustomAuthClaims_hook_type CustomAuthClaims_hook = NULL; +CustomAuthCheckPassword_hook_type CustomAuthCheckPassword_hook = NULL; + /* * Tell the user the authentication failed, but not (much about) why. * @@ -559,6 +566,33 @@ retrieve_conn_authentication(Port *port) FakeClientAuthentication(port); } +/* + * A connection claimed by an extension via CustomAuthClaims_hook uses the + * password it sends as an extension-defined credential, bypassing pg_hba -- + * the same model as retrieve_conn_authentication() above. Only the check is + * delegated: the extension decides whether the credential entitles the client + * to connect as port->user_name, and on success the connection becomes an + * ordinary backend for that user. + */ +static void +custom_conn_authentication(Port *port) +{ + char *passwd; + const char *msg1 = "Failed to retrieve the authentication password"; + const char *msg2 = "Authentication failure (invalid credential)"; + + sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); + passwd = recv_password_packet(port); + if (passwd == NULL) + ereport(FATAL, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("%s", msg1))); + + if (CustomAuthCheckPassword_hook == NULL || + !CustomAuthCheckPassword_hook(port, passwd)) + ereport(FATAL, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("%s", msg2))); + + FakeClientAuthentication(port); +} + /* * Special client authentication for QD to QE connections. This is run at the * QE. This is non-trivial because a QE some times runs at the master (i.e., an @@ -718,6 +752,17 @@ ClientAuthentication(Port *port) return; } + /* + * An extension may own the authentication of its own internal connections + * (identified by a marker option in the startup packet), likewise before + * pg_hba is consulted. + */ + if (CustomAuthClaims_hook != NULL && CustomAuthClaims_hook(port)) + { + custom_conn_authentication(port); + return; + } + /* * If this is a QD to QE connection, we might be able to short circuit * client authentication. diff --git a/src/include/lib/bloomfilter.h b/src/include/lib/bloomfilter.h index 05b736f566e..51afa223e03 100644 --- a/src/include/lib/bloomfilter.h +++ b/src/include/lib/bloomfilter.h @@ -27,5 +27,11 @@ extern double bloom_false_positive_rate(bloom_filter *filter); extern uint64 bloom_total_bits(bloom_filter *filter); extern bloom_filter *bloom_create_aggresive(int64 total_elems, int work_mem, uint64 seed); +extern Size bloom_bitset_bytes(const bloom_filter *filter); +extern const unsigned char *bloom_bitset_data(const bloom_filter *filter); +extern bloom_filter *bloom_create_from_bitset(int64 total_elems, + int bloom_work_mem, uint64 seed, + const unsigned char *bitset, + Size bitset_len); #endif /* BLOOMFILTER_H */ diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index 50f80f48970..fd1c5c1df89 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -30,6 +30,22 @@ extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, typedef void (*ClientAuthentication_hook_type) (Port *, int); extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook; +/* + * Hooks for an extension that maintains its own internal connections, such as + * a segment -> coordinator connection carrying a per-session token. The claims + * hook is consulted before pg_hba.conf and answers whether this connection + * belongs to the extension, normally by looking for a marker option in + * port->cmdline_options / port->guc_options. When it claims the connection, + * the backend asks the client for a password and hands it to the check hook, + * which returns true if the connection may proceed as port->user_name. The + * wire exchange stays in auth.c; the extension only supplies the two answers. + */ +typedef bool (*CustomAuthClaims_hook_type) (Port *port); +extern PGDLLIMPORT CustomAuthClaims_hook_type CustomAuthClaims_hook; +typedef bool (*CustomAuthCheckPassword_hook_type) (Port *port, + const char *passwd); +extern PGDLLIMPORT CustomAuthCheckPassword_hook_type CustomAuthCheckPassword_hook; + /* * Support for time-based authentication * From 4cb853337334cd83ce9566aa18b46ae7d31b5e0c Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Thu, 3 Sep 2026 22:31:26 +0300 Subject: [PATCH 02/15] Feature: Add the Anser adaptive information sharing extension Anser is a runtime pub/sub facility for MPP execution: producers on the segments publish a small piece of information about a running query (today a bloom filter over a join-build key), the coordinator unions the per-segment parts into one payload, and consumers on the segments receive it and prune work with it. State lives in a fixed coordinator-resident shared-memory channel map serviced by two background workers -- gather (drains producer submissions, unions parts, enforces the produce deadline) and send (delivers to waiting consumers, recycles channels). See gpcontrib/anser/README.md for the architecture, the state machine and the data flow. It is packaged as a shared_preload_libraries extension so that a kernel rebase does not have to carry it: everything is reached through an existing extensibility point -- shmem_request_hook and shmem_startup_hook for the shared state and its LWLock tranche, RegisterBackgroundWorker for the two services, planner_hook for the runtime-filter injection pass (which covers ORCA too, since ORCA is dispatched from inside standard_planner), RegisterCustomScanMethods for the injected nodes, DefineCustom*Variable for the anser.* GUCs, and the CustomAuth*_hook pair for segment -> coordinator connections. Segments cannot reach the coordinator's shared memory, so they open an ordinary libpq connection back to the QD and call anser.producer_begin / anser.publish / anser.consume_wait, authenticating with a per-session token instead of requiring pg_hba entries for segment hosts (the PARALLEL RETRIEVE CURSOR model; see src/anserauth.c). Every failure path is fail-open: a broken connection, an absent extension, an exhausted channel map or an expired produce deadline all degrade to unfiltered execution, never to a wrong result. Requires shared_preload_libraries='anser', anser.enable=on, and CREATE EXTENSION anser in each database that should use runtime filters -- the transport resolves its functions by name, so the catalog entries have to exist there. Co-Authored-By: Claude Opus 5 (1M context) --- gpcontrib/Makefile | 6 +- gpcontrib/anser/Makefile | 61 + gpcontrib/anser/README.md | 468 ++++++ gpcontrib/anser/anser--1.0.sql | 57 + gpcontrib/anser/anser.control | 24 + gpcontrib/anser/include/anser.h | 250 +++ gpcontrib/anser/include/anserbloom.h | 81 + gpcontrib/anser/include/anserclient.h | 60 + gpcontrib/anser/include/anserfilter.h | 77 + gpcontrib/anser/include/anserplan.h | 74 + gpcontrib/anser/src/anser.c | 2041 +++++++++++++++++++++++ gpcontrib/anser/src/anserauth.c | 430 +++++ gpcontrib/anser/src/anserbloomconsume.c | 240 +++ gpcontrib/anser/src/anserbloomproduce.c | 168 ++ gpcontrib/anser/src/anserclient.c | 453 +++++ gpcontrib/anser/src/anserfilter.c | 285 ++++ gpcontrib/anser/src/anserfuncs.c | 200 +++ gpcontrib/anser/src/anserinit.c | 282 ++++ gpcontrib/anser/src/anserplan.c | 451 +++++ gpcontrib/anser/src/anserplanexec.c | 674 ++++++++ gpcontrib/anser/src/anserservice.c | 203 +++ 21 files changed, 6583 insertions(+), 2 deletions(-) create mode 100644 gpcontrib/anser/Makefile create mode 100644 gpcontrib/anser/README.md create mode 100644 gpcontrib/anser/anser--1.0.sql create mode 100644 gpcontrib/anser/anser.control create mode 100644 gpcontrib/anser/include/anser.h create mode 100644 gpcontrib/anser/include/anserbloom.h create mode 100644 gpcontrib/anser/include/anserclient.h create mode 100644 gpcontrib/anser/include/anserfilter.h create mode 100644 gpcontrib/anser/include/anserplan.h create mode 100644 gpcontrib/anser/src/anser.c create mode 100644 gpcontrib/anser/src/anserauth.c create mode 100644 gpcontrib/anser/src/anserbloomconsume.c create mode 100644 gpcontrib/anser/src/anserbloomproduce.c create mode 100644 gpcontrib/anser/src/anserclient.c create mode 100644 gpcontrib/anser/src/anserfilter.c create mode 100644 gpcontrib/anser/src/anserfuncs.c create mode 100644 gpcontrib/anser/src/anserinit.c create mode 100644 gpcontrib/anser/src/anserplan.c create mode 100644 gpcontrib/anser/src/anserplanexec.c create mode 100644 gpcontrib/anser/src/anserservice.c diff --git a/gpcontrib/Makefile b/gpcontrib/Makefile index 32c134c95e6..89e1adb0cb7 100644 --- a/gpcontrib/Makefile +++ b/gpcontrib/Makefile @@ -15,7 +15,8 @@ recurse_targets = "" recurse_targets = gp_exttable_fdw ifeq "$(enable_debug_extensions)" "yes" - recurse_targets = gp_sparse_vector \ + recurse_targets = anser \ + gp_sparse_vector \ gp_distribution_policy \ gp_internal_tools \ gp_debug_numsegments \ @@ -28,7 +29,8 @@ ifeq "$(enable_debug_extensions)" "yes" pg_hint_plan \ reject_partition_fullscan else - recurse_targets = gp_sparse_vector \ + recurse_targets = anser \ + gp_sparse_vector \ gp_distribution_policy \ gp_internal_tools \ gp_legacy_string_agg \ diff --git a/gpcontrib/anser/Makefile b/gpcontrib/anser/Makefile new file mode 100644 index 00000000000..6893ea4e772 --- /dev/null +++ b/gpcontrib/anser/Makefile @@ -0,0 +1,61 @@ +#------------------------------------------------------------------------- +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# Makefile for the anser extension. +# +# IDENTIFICATION +# gpcontrib/anser/Makefile +# +#------------------------------------------------------------------------- + +MODULE_big = anser +OBJS = \ + $(WIN32RES) \ + src/anser.o \ + src/anserauth.o \ + src/anserbloomconsume.o \ + src/anserbloomproduce.o \ + src/anserclient.o \ + src/anserfilter.o \ + src/anserfuncs.o \ + src/anserinit.o \ + src/anserplan.o \ + src/anserplanexec.o \ + src/anserservice.o + +PGFILEDESC = "anser - adaptive information sharing runtime filters" + +EXTENSION = anser +DATA = anser--1.0.sql + +# src/anserclient.c opens libpq connections back to the coordinator. +PG_CPPFLAGS = -I$(srcdir)/include -I$(libpq_srcdir) +SHLIB_LINK_INTERNAL = $(libpq) +SHLIB_PREREQS = submake-libpq + +ifdef USE_PGXS +PG_CONFIG ?= pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = gpcontrib/anser +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/gpcontrib/anser/README.md b/gpcontrib/anser/README.md new file mode 100644 index 00000000000..8b7d7ba1888 --- /dev/null +++ b/gpcontrib/anser/README.md @@ -0,0 +1,468 @@ +# Anser — adaptive information sharing + +Anser is a runtime pub/sub facility for MPP query execution. Producers on the +segments publish a small piece of information about a query (today: a bloom +filter over a join-build key), the coordinator unions the per-segment parts into +one global payload, and consumers on the segments receive it and use it to prune +work (today: skip probe rows that cannot join). The shared state lives in a +fixed coordinator-resident shared-memory **channel map**, serviced by two +background workers (gather + send). + +This document covers installation, the architecture, the segment→coordinator +network transport and its token authentication, the configuration surface, what +a *channel* is, and the *channel state machine*. For the plan-tree integration +see `anserplan.c`; for the payload/bloom protocol see `anserfilter.c` and +`lib/bloomfilter.c`. + +## Installation + +Anser is an extension, but it is not a plain `CREATE EXTENSION` extension: +shared memory, background workers and planner/authentication hooks can only be +set up by a **preloaded** library, and the transport additionally needs three +SQL functions to exist in each database it is used in. Three steps, all +required: + +1. **Preload the library on every host** — the whole subsystem hangs off + `_PG_init` (`anserinit.c`), which only wires anything up when the library is + preloaded: + + ``` + gpconfig -c shared_preload_libraries -v "'anser'" --skipvalidation + gpconfig -c anser.enable -v on + gpstop -ra + ``` + + `anser.enable` is `PGC_POSTMASTER`: with it off, no shared memory is + requested and the two services are not registered. + +2. **Create the extension in each database that should use runtime filters**: + + ```sql + CREATE EXTENSION anser; + ``` + + This is what makes `anser.producer_begin` / `anser.publish` / + `anser.consume_wait` resolvable — segment executors call them **by name** + over libpq, so they must be present in that database's catalog. GUCs cannot + substitute: they do not create catalog entries. Install it in `template1` + to have new databases inherit it. Without the extension the plan pass skips + injection entirely (`anser_transport_installed()` in `anserplan.c`), so + queries run exactly as they would with the feature off rather than paying + for filters the segments cannot deliver. + +3. **Turn the filter on** where you want it — `anser.runtime_filter` is + `PGC_USERSET`, so per session, per role, or cluster-wide. + +Two further operational notes: + +- The gather and send services are ordinary background workers registered by + `_PG_init`, so they consume two `max_worker_processes` slots on the + coordinator. If the slots are exhausted the services never start; producers + then hit the produce deadline and every consumer fails open (unfiltered + execution, correct results). +- `anser_test` is a second control file over the same library, exposing the + internal C API to the regression tests. It is test-only and requires + superuser; do not create it in production databases. + +## Architecture + +All Anser state lives in fixed **coordinator shared memory**, allocated once at +postmaster start. Producers and consumers are ordinary query backends +(coordinator-resident, or on segments reaching the coordinator over libpq); they +never talk to each other directly and never own the shared state — they only +hand work to, or wait on, two **background workers** that do. + +Three shared structures, two hand-off points: + +- **Channel map** — the hash of channels (one per runtime condition per query), + holding each channel's state, accounting, and payload. The single source of + truth. +- **Submission queue** — the producer → gather hand-off. A producer copies its + serialized part into a free queue entry, signals the gather worker, and blocks + for an ACK; it never touches the channel payload itself. +- **Wait table** — the send → consumer hand-off, an array of **slots**. A *slot* + is one consumer's reservation on a channel: it records the consumer's key, a + pointer to that backend's latch, and a place for the send worker to stamp the + delivered payload (or a cancel). A blocked consumer owns one slot and sleeps on + its latch until the send worker flips it. + +The two **background workers** exist because the shared state has to keep moving +independent of any one transient/blocked backend: + +- **Gather service** — drains the submission queue: for each part it folds + (bitwise-OR unions) the data into the target channel's single payload, advances + the channel toward `READY`, and ACKs the producer. It also runs periodic + maintenance: time out stragglers (`anser.timeout_ms`) and sweep terminal or + orphaned channels. +- **Send service** — delivers: once a channel is `READY` it copies the combined + payload into every waiting slot and wakes those consumers' latches; when all + expected consumers are served it recycles the channel to `CONSUMED`. + +Both workers sleep on a latch and wake on demand — a producer's submission sets +the gather latch, a publish/registration sets the send latch — plus a periodic +timeout so maintenance runs even when idle. Concurrency is guarded by two +LWLocks, always taken in the order `AnserChannelLock` → `AnserRingLock`. + +The pay-off of this split: a producer can publish and leave, a consumer can block +without pinning anything, and the coordinator still unions once and fans the +result out — see the data-flow section below. + +### How it attaches to the server + +Everything is reached through existing extensibility points, so the server +carries no Anser-specific code (`anserinit.c`): + +| Hook | Used for | +| --- | --- | +| `shmem_request_hook` | `RequestAddinShmemSpace` for the three shared structures, plus `RequestNamedLWLockTranche("anser", 2)` | +| `shmem_startup_hook` | `AnserShmemInit()`, which also resolves `AnserChannelLock` / `AnserRingLock` from the tranche | +| `RegisterBackgroundWorker` | the gather and send services (coordinator only — `AnserStartRule`) | +| `planner_hook` | runs the injection pass on the finished plan; wrapping the hook covers ORCA too, since it is dispatched from inside `standard_planner()` | +| `RegisterCustomScanMethods` | the producer/consumer nodes, so their methods resolve by name in every backend that deserializes a dispatched plan | +| `DefineCustom*Variable` | the `anser.*` GUCs below | +| `CustomAuthClaims_hook` / `CustomAuthCheckPassword_hook` | token authentication for segment→QD connections (see the transport section) | + +The last pair is the only hook Anser added to the server; the others were +already there. + +### Gather-service wakeup cycle + +The gather worker owns the **gather latch** and sleeps on it between passes. +Setting that latch is the "producer work is pending" signal — raised by +`AnserRegisterCondition`, `AnserProducerBegin`, `AnserPublish`, and (the common +one) `AnserEnqueueSubmission` when a remote producer drops a part into a free +submission-queue slot and blocks for its ACK. + +``` +producer backend gather worker (looping) +──────────────── ─────────────────────── +enqueue part → slot = PENDING +SetLatch(gather_latch) ───────────────► WaitLatch(gather_latch) returns +block on own latch ResetLatch(gather_latch) + │ AnserGatherServiceCycle(): + │ for each PENDING slot: + │ AnserGatherApply() ← fold/union part, + │ advance channel toward READY + │ slot = ACCEPTED/REJECTED + ▼ SetLatch(producer_latch) ─┐ +wake, read ACK, free slot ◄───────────────────────────────────────── ┘ + AnserCancelStaleChannels() (timeouts) + AnserReapSubmissionSlots() + AnserServiceMaintenance() (orphan sweep) + SetLatch(send_latch) on READY ─► send worker + WaitLatch(gather_latch) … (sleep again) +``` + +Key properties: + +- **No lost wakeups.** If the latch is set while the worker is mid-pass (not yet + waiting), it stays set and the next `WaitLatch` returns immediately. +- **Two hand-offs.** The cycle wakes each producer via *its own* latch (the ACK), + and wakes the **send** worker via the send latch once a channel reaches `READY` + — the gather worker never delivers to consumers itself. +- **Timed fallback.** The same cycle also runs every + `ANSER_SERVICE_WAKEUP_INTERVAL_MS` even with no latch set, so stale + `COLLECTING` channels time out and orphaned channels get swept while idle. + +### Send-service wakeup cycle + +The send worker owns the **send latch** and sleeps on it. Setting that latch +means "a channel is now deliverable or cancellable, or a consumer is now +waiting" — raised by the gather worker when a channel reaches `READY` +(`AnserGatherApply`), by the publish/cancel/timeout paths when a channel is +cancelled, and by a consumer when it subscribes (`AnserConsumerWait`) or abandons +its wait (`AnserAbandonWaitSlot`). + +``` +gather worker / canceller / send worker (looping) consumer backend +consumer subscribe ───────────────────── ──────────────── +────────────────────────── subscribe: slot = WAITING +channel → READY | CANCELLED | CONSUMED +SetLatch(send_latch) ───────────────────► WaitLatch(send_latch) returns + ResetLatch(send_latch) + AnserSendServiceCycle(): + for each READY/terminal channel: + for each WAITING slot on it: + READY → copy payload → slot, + slot = DELIVERED ────► wake, read payload, + terminal → slot = CANCELLED ───► (or cancel → fail open), + SetLatch(consumer_latch) free slot + done_consumers++ + all served → recycle CONSUMED + AnserReapWaitSlots() + WaitLatch(send_latch) … (sleep again) +``` + +Key properties: + +- **Per-consumer delivery.** Each `WAITING` slot gets its *own* pinned copy of the + merged payload, so delivery is per-consumer — one consumer's cancel (or a DSM + shortage that cancels just it) never affects another's delivery. +- **Straggler safety.** A consumer that registers on an already-terminal channel + is handed a cancel and fails open, instead of blocking on a channel the sweep + would otherwise never reclaim. +- **Recycle.** Once `done_consumers` reaches `expected_consumers` (one per + segment) the channel becomes `CONSUMED` and its payload is freed. +- **No lost wakeups / timed fallback.** Like the gather worker: a latch set + mid-pass is honored next loop, and the same cycle runs every + `ANSER_SERVICE_WAKEUP_INTERVAL_MS` so straggler cancels and recycling still + happen while idle. + +## Network transport: how segments connect and authenticate + +Coordinator-resident producers/consumers touch the channel map directly. +Segment executors cannot — the map lives in the coordinator's shared memory — so +they open an **ordinary libpq connection back to the QD** and drive the +`anser.producer_begin` / `anser.publish` / `anser.consume_wait` SQL +functions from that backend (`anserclient.c`). The QD address comes from +`gp_qd_hostname` / `gp_qd_port`, which the dispatcher injects into every QE; the +connection reuses the query's database and the **session user** +(`MyProcPort->user_name` — the authenticated login role, unaffected by +`SET ROLE`), and sets `application_name=anser_rf` so these backends are +identifiable on the coordinator. + +### Authentication: per-session token (the parallel-retrieve-cursor model) + +The backward connection must not depend on `pg_hba.conf`: a stock +`gpinitsystem` cluster grants `trust` to coordinator IPs on the *segments* (that +is what makes QD→QE dispatch connections work), but never adds segment hosts to +the *coordinator's* pg_hba — so a segment→QD connection would be rejected by +default. Anser therefore authenticates these connections the same way +`PARALLEL RETRIEVE CURSOR` retrieve sessions do (`retrieve_conn_authentication` +in `libpq/auth.c`): + +1. **Token registration (QD, plan time).** When the planner pass injects a + runtime filter into a query, the QD registers a **per-session token**: + 128 bits of `pg_strong_random`, hex-encoded, stored in the shared-memory + *session token hash* keyed by `(gp_session_id, session user)` + (`AnserGetOrCreateSessionToken` in `anser.c`). One token per session; the + entry is removed when the session exits. +2. **Delivery to segments.** The token travels inside the dispatched plan (a + `String` in the producer/consumer `CustomScan.custom_private`), so it only + crosses the already-trusted QD→QE dispatch channel. +3. **Connection (segment).** The segment executor connects with the startup + marker `anser.conn=true` (passed via the libpq `options` keyword) and the + token as the connection `password`. +4. **Verification (QD, auth time).** `ClientAuthentication` checks the marker + **before** pg_hba is consulted and calls the extension's hooks + (`AnserConnClaims` / `AnserConnCheckPassword`): it + requests the password, resolves `user_name` to a role OID, and calls + `AnserSessionTokenIsValid`, which scans the token hash for a matching + `(user, token)` pair. On match the connection becomes an ordinary backend + for that user (`FakeClientAuthentication`); on mismatch it is rejected with + `FATAL`. + +``` +segment executor coordinator +──────────────── ─────────── +libpq connect: user=, + options="-c anser.conn=true", + password= + ── startup ────────► ClientAuthentication: + marker seen → skip pg_hba + ◄── AUTH_REQ_PASSWORD +token ─────────────────────────────► AnserSessionTokenIsValid(user, token) + scans session token hash (shmem) + ◄── OK / FATAL +SELECT anser.producer_begin(...) ──► ... runs as the session user +``` + +Properties and limits of this model: + +- **No pg_hba change needed** on the coordinator for segment hosts; no password + of the user ever leaves the client. +- The token is **per session, not per query**, and grants a *full* SQL backend + as that user (unlike retrieve sessions, which are utility-mode and + `RETRIEVE`-only). Anyone who learns a live session's token can connect as its + user — the token never leaves the trusted dispatch channel, but it does + appear in debug-level plan dumps (`debug_print_plan`), so treat those logs as + sensitive. +- **Channel-level access control is unchanged and independent**: a channel is + bound to the role that created it (`AnserChannelEntry.creator_role`), so even + an authenticated connection can only produce/consume on channels its own role + created (or any, if superuser). +- **Fail open.** If no token was registered (subsystem off, token hash full) or + authentication fails for any reason, the connection attempt returns NULL and + the segment runs unfiltered — never an error, never wrong results. +- **Without a token** the client omits the marker and password, and the + connection goes through ordinary pg_hba authentication (previous behavior); + this also covers hand-built or test deployments where the admin chose to + provision pg_hba entries instead. + +The `anser.conn` GUC itself is a marker only (`PGC_BACKEND`, not settable in +`postgresql.conf`, not synced to segments); its value is read from the raw +startup options during authentication. + +## GUCs + +| GUC | Default | Context | Meaning | +| --- | --- | --- | --- | +| `anser.enable` | `off` | POSTMASTER | Master switch. When on, the channel-map shared memory is sized/created and the gather + send background workers are started at postmaster start. Off = the whole subsystem is absent (zero shmem, no workers). | +| `anser.runtime_filter` | `off` | USERSET | Enables the post-planning pass that injects bloom-filter producer/consumer nodes into a matching plan. Requires `anser.enable`; without it the pass is a no-op even when the subsystem is up. | +| `anser.max_channels` | `0` (auto) | POSTMASTER | Number of channels the map can hold; sizes the channel hash, the producer submission queue, and (× `anser.max_consumers_per_channel`) the consumer wait table. `0` auto-sizes to `max_connections * gp_max_slices` — at most `max_connections` concurrent queries, each opening up to `gp_max_slices` runtime-filter channels — falling back to a fixed per-connection budget (8) when `gp_max_slices` is unbounded (`0`). Captured once at postmaster start so it is stable across all backends. | +| `anser.max_info_size` | `65 MB` | POSTMASTER | Maximum serialized payload (unioned bloom filter + part header) a channel may hold; caps per-channel memory and bounds the effective bloom-filter size. The default is `64 MB + 1 MB` so a full 64 MB power-of-two bitset fits with its header; `bloom_create` also floors every bitset at 1 MB. | +| `anser.max_consumers_per_channel` | `64` | POSTMASTER | Wait-table slots reserved per channel; the consumer wait table is sized `anser.max_channels * this`. Bounds how many consumers can block on one channel at once. | +| `anser.timeout_ms` | `1000` | USERSET | Produce/collect deadline. A channel that is still collecting parts when this elapses is cancelled by the maintenance sweep, so waiting consumers fail open (run unfiltered) rather than hang. | + +## Data flow: producer → gather (bitwise union) → consumer + +The parts from all segments are combined into **one** payload by a **bitwise OR +on the coordinator**, and that single combined payload is delivered to every +consumer. This is the core of Anser and worth stating precisely, because it is +*not* a concatenation: + +``` +segment 0 producer: bitset 0000 0001 ┐ +segment 1 producer: bitset 0000 0010 ├─ libpq ─► gather service (coordinator) +segment N producer: ... ┘ │ + │ fold each part into the + │ running merged bitset: + │ 0000 0001 + │ OR 0000 0010 + ▼ = 0000 0011 (one part) + channel payload = single merged bitset + │ + send service ──────────┼───────────────┐ + ▼ ▼ ▼ + consumer seg 0 consumer seg 1 ... consumer seg N + each receives the SAME combined 0000 0011 +``` + +Step by step: + +1. **Produce (per segment, in parallel).** Each segment's producer builds a bloom + filter over its local build keys (`bloom_create` from `total_elems` / + `max_payload` / a `condition_key`-derived seed, all carried in the plan node — + *not* on the wire) and serializes it as one *part*. Because every producer and + the consumer pass the identical parameters, every part has a byte-for-byte + identical size and shape. Segment producers push their parts to the coordinator + concurrently over their own libpq connections — the network transfer is + parallel, and the submission queue is sized `channels * per-channel producers` + so they hand off without serializing. + +2. **Gather (coordinator, once per part).** The coordinator never reconstructs a + filter — it works on raw bytes. The **first** part is stored verbatim; every + later part is folded into the channel's payload with an in-place **bitwise OR** + of the bitset (`AnserBloomFoldPartInPlace` in `anserfilter.c`, from + `AnserStorePayloadDSM` in `anser.c`). The payload is therefore always a + **single merged bitset**, the size of one filter — it does **not** grow with + the segment count. The OR requires the incoming part to be the same size as the + accumulator (guaranteed by the shared parameters); a mismatch makes the fold + fail and the channel is cancelled (consumers fail open). + +3. **Deliver (coordinator → every consumer).** Once every expected part is folded + (channel `READY`), the send service delivers a copy of that one combined + bitset to each waiting consumer. Delivery is O(segments) bytes, not + O(segments²), and the union work is done once on the master rather than + repeated in every consumer. + +4. **Consume (per segment).** Each consumer rebuilds an empty filter from its own + plan parameters (the same `total_elems` / `max_payload` / seed the producers + used) and loads the received bitset into it (`AnserBloomDeserializePart`), + requiring the received length to match exactly (else it fails open). It does + **not** re-union anything; the merged header's part count is surfaced as the + `Rows Removed by Bloom Filter` / parts-received EXPLAIN stats. + +Correctness note: the combined filter is the OR (super-set) of every segment's +build keys, so it can only ever have *false positives*, never false negatives — +a probe row it rejects genuinely cannot join. Anser therefore only changes +performance, never results; any failure along this path degrades to "no filter" +(fail open). + +## Channel + +A **channel** is one rendezvous point between the producers and consumers of a +single piece of runtime information, for a single query. It is a shared-memory +entry (`AnserChannelEntry`) in the coordinator's channel hash, addressed by an +`AnserChannelKey`: + +``` +AnserChannelKey = { gp_session_id, gp_command_count, condition_id, condition_key[64] } +``` + +- `gp_session_id` + `gp_command_count` scope the channel to one query execution, + so keys never collide across sessions or across statements in a session. +- `condition_id` distinguishes multiple filters within the same query. +- `condition_key` is an opaque string describing the filtered condition (today a + synthetic `rf:.=.` string). Both sides derive it + independently and must agree — it is what makes a producer and a consumer meet + on the same channel. + +The entry also tracks bookkeeping used by the state machine: `expected_producers` +/ `done_producers` (one part per segment), `consumers` / `expected_consumers` / +`done_consumers` (delivery accounting), the `creator_role` (only that role or a +superuser may produce/consume on it), the payload (`dsm_handle` + `data_len`), +and `created_at` / `updated_at` timestamps used by the maintenance sweep. + +The channel map is finite and fixed-size. Terminal channels are reclaimed by the +background maintenance sweep (or, under map pressure, by emergency reclamation on +registration) so their slots can be reused; a fresh registration landing on a +terminal entry resets it in place. + +## `AnserChannelState` and the state flow + +A channel moves through five states (`AnserChannelState`): + +| State | Meaning | +| --- | --- | +| `PENDING` | Registered; no producer part received yet. | +| `COLLECTING` | At least one part received; still waiting for the rest. | +| `READY` | All expected parts collected and unioned; payload deliverable. | +| `CANCELLED` | Aborted (timeout / explicit cancel / owner death / query cancel). Terminal. Consumers fail open. | +| `CONSUMED` | Every expected consumer has been delivered the payload. Terminal. | + +``` + register (RegisterCondition / ProducerBegin) + │ + ▼ + ┌─────────────┐ + │ PENDING │ + └─────────────┘ + │ first part published + ▼ + ┌─────────────┐ + ┌──────────────│ COLLECTING │ + │ └─────────────┘ + │ │ done_producers == expected_producers + │ ▼ + │ ┌─────────────┐ + cancel / │ │ READY │ + timeout / │ └─────────────┘ + owner death│ │ done_consumers == expected_consumers + / query │ ▼ + cancel │ ┌─────────────┐ + │ │ CONSUMED │ (terminal) + ▼ └─────────────┘ + ┌─────────────┐ │ + │ CANCELLED │ (terminal) │ + └─────────────┘ │ + │ │ + └──────────┬──────────┘ + ▼ + maintenance sweep reclaims slot (→ NOT_FOUND) + or a fresh register() resets the entry to PENDING +``` + +**Transitions:** + +- **create → `PENDING`** — `AnserRegisterCondition` / `AnserProducerBegin` insert + the entry (or reset a terminal one) with `expected_producers` set. +- **`PENDING` → `COLLECTING`** — the first part is published (`AnserPublish` / + the gather service applying a submitted part). The part is unioned into the + payload and `done_producers` is incremented. +- **`COLLECTING` → `READY`** — the part that makes `done_producers` reach + `expected_producers` completes the union; the global payload is now + deliverable and the send service wakes waiting consumers. +- **`READY` → `CONSUMED`** — the send service delivers the payload to each + waiting consumer; when `done_consumers` reaches `expected_consumers` (one per + segment) the channel is recycled to `CONSUMED` and its payload freed. Abandoned + consumers (cancelled mid-wait) stop counting so this can still be reached. +- **`PENDING`/`COLLECTING` → `CANCELLED`** — via `anser.timeout_ms` expiry + (maintenance sweep on a still-`COLLECTING` channel), a producer publishing a + cancel part, a whole-query `AnserCancelQuery`, or the creator backend dying. + Any consumer blocked on the channel is woken with a cancel and **fails open** + (runs unfiltered) — Anser never changes results, only performance. +- **terminal (`CANCELLED`/`CONSUMED`) → gone** — the background maintenance sweep + (unless paused via the test-only `sweep_enabled` knob) removes terminal and + orphaned channels, freeing the slot; a later registration reusing the same key + starts over at `PENDING`. diff --git a/gpcontrib/anser/anser--1.0.sql b/gpcontrib/anser/anser--1.0.sql new file mode 100644 index 00000000000..5ae42479a1d --- /dev/null +++ b/gpcontrib/anser/anser--1.0.sql @@ -0,0 +1,57 @@ +/* gpcontrib/anser/anser--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION anser" to load this file. \quit + +/* + * The coordinator-side edges of the Anser network transport. Segment + * executors call these over libpq while running a plan that carries Anser + * runtime-filter nodes; they are not a user-facing API. + * + * Execution location is left at the default (EXECUTE ON ANY), even though the + * channel map only exists in coordinator shared memory: CREATE FUNCTION + * accepts EXECUTE ON COORDINATOR only for set-returning functions (see + * validate_sql_exec_location() in commands/functioncmds.c). It costs nothing + * here -- the transport calls these as "SELECT anser.publish(...)" with no + * FROM clause, which a coordinator backend evaluates locally -- and a call that + * did somehow reach a segment would find no channel map and return false, i.e. + * fail open. + * + * The default EXECUTE grant to PUBLIC -- and the USAGE grant on the schema + * below -- are intentional and must not be revoked: segments connect back as + * the query's own role, so restricting these functions would silently disable + * runtime filtering for every non-superuser query. Callers are confined to + * the channels their own role created (see anserfuncs.c). + */ + +GRANT USAGE ON SCHEMA anser TO PUBLIC; + +CREATE FUNCTION anser.producer_begin( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + expected_producers int4) +RETURNS bool +AS 'MODULE_PATHNAME', 'anser_producer_begin' +LANGUAGE C STRICT VOLATILE; + +CREATE FUNCTION anser.publish( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + payload bytea, + cancelled bool) +RETURNS bool +AS 'MODULE_PATHNAME', 'anser_publish' +LANGUAGE C STRICT VOLATILE; + +CREATE FUNCTION anser.consume_wait( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text) +RETURNS bytea +AS 'MODULE_PATHNAME', 'anser_consume_wait' +LANGUAGE C STRICT VOLATILE; diff --git a/gpcontrib/anser/anser.control b/gpcontrib/anser/anser.control new file mode 100644 index 00000000000..2546b23edc0 --- /dev/null +++ b/gpcontrib/anser/anser.control @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# anser extension +comment = 'Anser adaptive information sharing (runtime bloom filters)' +default_version = '1.0' +module_pathname = '$libdir/anser' +schema = 'anser' +relocatable = false +superuser = true diff --git a/gpcontrib/anser/include/anser.h b/gpcontrib/anser/include/anser.h new file mode 100644 index 00000000000..9ed57dc307f --- /dev/null +++ b/gpcontrib/anser/include/anser.h @@ -0,0 +1,250 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anser.h + * Shared-memory channel map for the Anser adaptive information + * sharing subsystem. + * + * IDENTIFICATION + * gpcontrib/anser/include/anser.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSER_H +#define ANSER_H + +#include "postgres.h" + +#include "datatype/timestamp.h" +#include "storage/dsm.h" +#include "storage/latch.h" +#include "storage/lwlock.h" + +#define ANSER_CONDITION_KEY_SIZE 64 + +/* + * Poll interval (milliseconds) a backend sleeps on its latch between rechecks + * when the awaited change does NOT set its latch, so it must recheck shared + * state itself (waiting for a channel state in AnserWaitForState, or for a free + * submission slot in AnserEnqueueSubmission). Kept small so waits stay + * responsive without busy-looping. + */ +#define ANSER_WAIT_POLL_INTERVAL_MS 10L + +/* + * Safety wakeup (milliseconds) for latch-driven waits where the event always + * sets the waiter's latch (a producer's submission ACK in AnserWaitSubmissionAck, + * a consumer's delivery in AnserWaitSlotResult). The wait normally ends on the + * latch; this timeout only bounds how long a lost wakeup could stall it. + */ +#define ANSER_WAIT_LATCH_TIMEOUT_MS 1000L + +/* + * Wakeup interval (milliseconds) for a background service's main loop. Each + * service runs its data-path pass whenever its latch fires; this timed wakeup + * additionally bounds how long a stale COLLECTING channel or a dead-backend slot + * can linger between latches before periodic maintenance reclaims it. + */ +#define ANSER_SERVICE_WAKEUP_INTERVAL_MS 1000L + +/* + * Registered adaptive-information condition for one running command. + * + * condition_key is an opaque symbol that identifies the condition (the + * optimizer-generated equivalence-class symbols described in the Anser + * paper); the channel map only compares keys for equality. + */ +typedef struct AnserChannelKey +{ + int gp_session_id; + int gp_command_count; + uint32 condition_id; + char condition_key[ANSER_CONDITION_KEY_SIZE]; +} AnserChannelKey; + +/* + * Channel lifecycle: PENDING (created, awaiting producers) -> COLLECTING + * (first part received) -> READY (all expected parts unioned) -> + * CONSUMED (all expected consumers delivered). CANCELLED replaces any + * state on produce timeout, producer cancel, or owning query end. + */ +typedef enum AnserChannelState +{ + ANSER_CHANNEL_PENDING = 0, + ANSER_CHANNEL_COLLECTING, + ANSER_CHANNEL_READY, + ANSER_CHANNEL_CANCELLED, + ANSER_CHANNEL_CONSUMED +} AnserChannelState; + +/* + * One channel in the shared-memory map: the condition key, lifecycle state, + * creator ownership, producer/consumer accounting, and the DSM handle of + * the gathered payload. + */ +typedef struct AnserChannelEntry +{ + AnserChannelKey key; + AnserChannelState state; + Oid creator_role; /* authenticated role that created the channel; + * only this role (or a superuser) may + * produce/consume on it -- see anserfuncs.c */ + int32 expected_producers; + int32 done_producers; + int32 consumers; + int32 expected_consumers; /* consumers to deliver before recycling the + * payload (one per segment); 0 = unknown */ + int32 done_consumers; + Size data_len; + dsm_handle dsm_handle; + TimestampTz updated_at; /* last activity; drives the produce timeout */ +} AnserChannelEntry; + +/* + * Shared control block: effective sizing limits, the background services' + * latches, and the maintenance-sweep switch. + */ +typedef struct AnserControl +{ + uint32 max_channels; + Size max_info_size; + Latch gather_latch; + Latch send_latch; + bool sweep_enabled; /* when false, the periodic maintenance sweep + * leaves terminal channels in place; a test-only + * knob so terminal state can be observed + * deterministically. Emergency (map-full) + * reclamation is unaffected. */ +} AnserControl; + +/* GUCs */ +extern bool gp_anser_enable; +extern bool gp_anser_runtime_filter; +extern bool gp_anser_conn; /* startup-option marker for token-auth conns */ +extern int gp_anser_max_channels; +extern int gp_anser_max_info_size; +extern int gp_anser_timeout_ms; +extern int gp_anser_max_consumers_per_channel; + +/* + * The two LWLocks of the "anser" named tranche, requested in _PG_init and + * resolved in AnserShmemInit: AnserChannelLock guards the channel map, the + * consumer wait table and the session-token hash; AnserRingLock guards the + * inbound submission queue. + */ +extern LWLock *AnserChannelLock; +extern LWLock *AnserRingLock; + +#define ANSER_LWLOCK_TRANCHE "anser" +#define ANSER_NUM_LWLOCKS 2 + +/* Shared-memory setup. */ +extern Size AnserShmemSize(void); +extern void AnserShmemInit(void); + +/* + * The session-token hash owned by anserauth.c; folded into the sizing and + * setup above so all Anser shared state is requested in one place. + */ +extern Size AnserAuthShmemSize(void); +extern void AnserAuthShmemInit(void); + +/* + * Effective channel-map size (anser.max_channels, or its auto-sizing from + * max_connections * gp_max_slices). Computed once and cached for the life of + * the process; see the definition in anser.c. + */ +extern int AnserMaxChannels(void); + +/* Public channel-manager API. */ +extern bool AnserSubscribe(const AnserChannelKey *channel_key); +extern bool AnserPublish(const AnserChannelKey *channel_key, + const void *payload, Size payload_len, + bool cancelled); +extern bool AnserWaitProducersRegistered(const AnserChannelKey *channel_key, + long timeout_ms); +extern bool AnserWaitReady(const AnserChannelKey *channel_key, + bool *cancelled); +extern bool AnserConsumeReady(const AnserChannelKey *channel_key, + void *buffer, Size buffer_size, Size *payload_len, + bool *cancelled); +extern AnserChannelState AnserChannelGetState(const AnserChannelKey *channel_key, + bool *found); +extern int AnserChannelConsumerCount(const AnserChannelKey *channel_key); +extern int AnserChannelPayloadBytes(const AnserChannelKey *channel_key); +extern void AnserCancelQuery(int gp_session_id, int gp_command_count); +extern void AnserAttachServiceLatch(bool gather_service); +extern void AnserDetachServiceLatch(bool gather_service); +extern void AnserWaitServiceLatch(bool gather_service, long timeout_ms); +extern void AnserWakeServiceLatch(bool gather_service); +extern void AnserServiceMaintenance(void); +extern void AnserSetSweepEnabled(bool enabled); + +/* + * Network-path API. + * + * These entry points back the anser.* SQL functions that remote + * (segment) producers and consumers call over libpq. Unlike the direct + * AnserPublish/AnserConsume* API above, they do not touch the channel payload + * from the calling backend: producers hand their part to the gather service + * through the inbound submission queue and block for an ACK; consumers register + * a wait slot and block until the send service delivers or cancels it. + */ +extern bool AnserProducerBegin(const AnserChannelKey *channel_key, + int expected_producers, + Oid caller_role, bool caller_is_super); +extern bool AnserProducerSubmit(const AnserChannelKey *channel_key, + int expected_producers, + const void *payload, Size payload_len, + bool cancelled, + Oid caller_role, bool caller_is_super); +extern bool AnserConsumerWait(const AnserChannelKey *channel_key, + void **payload, Size *payload_len, + bool *cancelled, + Oid caller_role, bool caller_is_super); + +/* + * Data-path cycles executed by the background services. Each performs one + * non-blocking pass over the shared state; the service loops call them + * whenever their latch fires or the maintenance timer elapses. + */ +extern void AnserGatherServiceCycle(void); +extern void AnserSendServiceCycle(void); + +/* + * Background-service entry points. The two mains are resolved by name from + * this library (bgw_function_name), so they must be exported. + */ +extern PGDLLEXPORT void AnserGatherServiceMain(Datum main_arg); +extern PGDLLEXPORT void AnserSendServiceMain(Datum main_arg); +extern bool AnserStartRule(Datum main_arg); + +/* + * Session-token authentication for the segment -> coordinator libpq + * transport (parallel-retrieve-cursor model; see anser.c). The QD calls + * AnserGetOrCreateSessionToken at plan time; the coordinator backend accepting + * the connection calls AnserConnClaims/AnserConnCheckPassword, which _PG_init + * installs as the core custom-authentication hooks (see libpq/auth.h). + */ +extern char *AnserGetOrCreateSessionToken(Oid user_id); +extern bool AnserSessionTokenIsValid(Oid user_id, const char *token_hex); +extern bool AnserConnClaims(struct Port *port); +extern bool AnserConnCheckPassword(struct Port *port, const char *passwd); + +#endif /* ANSER_H */ diff --git a/gpcontrib/anser/include/anserbloom.h b/gpcontrib/anser/include/anserbloom.h new file mode 100644 index 00000000000..f8cc8cefe83 --- /dev/null +++ b/gpcontrib/anser/include/anserbloom.h @@ -0,0 +1,81 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserbloom.h + * Standalone Anser Bloom filter producer/consumer executor helpers. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserbloom.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERBLOOM_H +#define ANSERBLOOM_H + +#include "postgres.h" + +#include "anser.h" +#include "lib/bloomfilter.h" + +/* + * Opaque executor state handles for the Bloom filter producer and + * consumer; the struct definitions are private to + * anserbloomproduce.c and anserbloomconsume.c. + */ +typedef struct AnserBloomFilterProduceState AnserBloomFilterProduceState; +typedef struct AnserBloomFilterConsumeState AnserBloomFilterConsumeState; + +/* Producer side: build one Bloom filter part and publish it to the channel. */ + +/* + * `token` is the QD session token used to authenticate the segment -> QD + * libpq connection (parallel-retrieve-cursor model); NULL means connect + * without it and rely on pg_hba. Ignored on the coordinator-local path. + */ +extern AnserBloomFilterProduceState *ExecInitAnserBloomFilterProduce( + const AnserChannelKey *channel_key, + int64 total_elems, + Size max_payload_bytes, + uint32 part_index, + uint32 total_parts, + const char *token); +extern void ExecAnserBloomFilterProduceAddDatum(AnserBloomFilterProduceState *state, + Datum value, bool isnull); +extern bool ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state); +extern bool ExecAnserBloomFilterProduceCancel(AnserBloomFilterProduceState *state); +extern void ExecEndAnserBloomFilterProduce(AnserBloomFilterProduceState *state); + +/* Consumer side: gather all parts of a channel into one Bloom filter. */ +extern AnserBloomFilterConsumeState *ExecInitAnserBloomFilterConsume( + const AnserChannelKey *channel_key, + int64 total_elems, + Size max_payload_bytes, + uint32 expected_parts, + const char *token); +extern bool ExecAnserBloomFilterConsume(AnserBloomFilterConsumeState *state, + long registration_timeout_ms); +extern bloom_filter *ExecAnserBloomFilterConsumerGetFilter( + AnserBloomFilterConsumeState *state); +extern uint32 ExecAnserBloomFilterConsumerReceivedParts( + AnserBloomFilterConsumeState *state); +extern bool ExecAnserBloomFilterConsumerWasCancelled( + AnserBloomFilterConsumeState *state); +extern void ExecEndAnserBloomFilterConsume(AnserBloomFilterConsumeState *state); + +#endif /* ANSERBLOOM_H */ diff --git a/gpcontrib/anser/include/anserclient.h b/gpcontrib/anser/include/anserclient.h new file mode 100644 index 00000000000..f3999604255 --- /dev/null +++ b/gpcontrib/anser/include/anserclient.h @@ -0,0 +1,60 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserclient.h + * libpq client helpers that let a remote (segment) backend reach the + * coordinator-resident Anser services over an ordinary connection to the QD. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserclient.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERCLIENT_H +#define ANSERCLIENT_H + +#include "anser.h" + +/* + * Publish one producer part to the coordinator. Opens a short-lived libpq + * connection to the QD, runs anser.producer_begin + anser.publish, and + * closes. Fail-open: any connection/protocol error best-effort publishes a + * cancel for the dataset and returns false, never raising. + * + * `token` is the QD session token used to authenticate the connection (the + * parallel-retrieve-cursor model: anser.conn=true + token as password, + * bypassing pg_hba); NULL or "" connects without it and relies on pg_hba. + */ +extern bool AnserClientPublish(const AnserChannelKey *channel_key, + uint32 expected_producers, + const void *payload, Size payload_len, + bool cancelled, const char *token); + +/* + * Wait for delivery of a channel payload from the coordinator. Opens a + * query-lifetime libpq connection to the QD, runs anser.consume_wait, and + * blocks (interruptibly) until the row arrives. On success *payload points at a + * palloc'd copy of the bytes. Connection loss is treated as a cancel for this + * consumer only. `token` is as in AnserClientPublish. + */ +extern bool AnserClientConsumeWait(const AnserChannelKey *channel_key, + void **payload, Size *payload_len, + bool *cancelled, const char *token); + +#endif /* ANSERCLIENT_H */ diff --git a/gpcontrib/anser/include/anserfilter.h b/gpcontrib/anser/include/anserfilter.h new file mode 100644 index 00000000000..06bd832798e --- /dev/null +++ b/gpcontrib/anser/include/anserfilter.h @@ -0,0 +1,77 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserfilter.h + * Bloom-filter payload helpers for Anser channels. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserfilter.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERFILTER_H +#define ANSERFILTER_H + +#include "postgres.h" + +#include "lib/bloomfilter.h" + +#define ANSER_BLOOM_PART_MAGIC 0x41424631U /* ABF1 */ +#define ANSER_BLOOM_PART_VERSION 1U + +/* + * On-wire framing for a serialized bloom part. It deliberately does NOT carry + * the bitset parameters (size / seed / hash count): both the producer and the + * consumer build the filter with bloom_create from the same plan parameters, so + * the shape is agreed by construction and never reconstructed from the wire. + * magic/version guard the framing; part_index/total_parts track the coordinator + * fold count (surfaced as diagnostics). Kept as a struct for forward + * extensibility. + */ +typedef struct AnserBloomPartHeader +{ + uint32 magic; + uint32 version; + uint32 part_index; + uint32 total_parts; +} AnserBloomPartHeader; + +extern uint64 AnserBloomSeed(const char *condition_key); +extern bloom_filter *AnserBloomCreate(int64 total_elems, + Size max_payload_bytes, + uint64 seed); +extern Size AnserBloomSerializedSize(const bloom_filter *filter); +extern bool AnserBloomSerializePart(const bloom_filter *filter, + uint32 part_index, + uint32 total_parts, + void *buffer, + Size buffer_size, + Size *payload_len); +extern bloom_filter *AnserBloomDeserializePart(const void *payload, + Size payload_len, + int64 total_elems, + Size max_payload_bytes, + uint64 seed, + uint32 *part_index, + uint32 *total_parts); +extern bool AnserBloomLooksLikePart(const void *payload, Size payload_len); +extern bool AnserBloomFoldPartInPlace(void *acc, Size acc_len, + const void *part, Size part_len); + +#endif /* ANSERFILTER_H */ diff --git a/gpcontrib/anser/include/anserplan.h b/gpcontrib/anser/include/anserplan.h new file mode 100644 index 00000000000..704537feb0b --- /dev/null +++ b/gpcontrib/anser/include/anserplan.h @@ -0,0 +1,74 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserplan.h + * Post-planning transformation that injects Anser runtime bloom-filter + * producer/consumer nodes into a finished plan tree. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserplan.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERPLAN_H +#define ANSERPLAN_H + +#include "nodes/plannodes.h" + +/* + * Post-plan pass: recognize the supported join shape in a finished PlannedStmt + * and inject an Anser bloom-filter producer (on the hash build side) and a + * consumer (above the probe scan). Called once from planner(), so it covers + * both the Postgres planner and ORCA. A no-op unless the Anser runtime-filter + * GUCs are on and this is a coordinator SELECT. + */ +extern void AnserApplyRuntimeFilters(PlannedStmt *stmt); + +/* + * Register the two CustomScan providers (producer, consumer) so their methods + * resolve by name when a dispatched plan is deserialized. Must run once per + * backend (QD and every QE) before any plan execution. + */ +extern void AnserRegisterRuntimeFilterMethods(void); + +/* + * Node builders (implemented in anserplanexec.c, where the CustomScan method + * tables live). Each wraps `child` in a pass-through CustomScan carrying the + * runtime-filter parameters in custom_private; the caller assigns plan_node_id. + * `key_attno` is the build (producer) / probe (consumer) join-key attno in the + * child's output tuple. `token` is the QD session token segment executors + * present when connecting back to the QD (NULL or "" means none: fall back to + * pg_hba-driven authentication). + */ +extern CustomScan *AnserBuildBloomProducerScan(Plan *child, AttrNumber key_attno, + uint32 condition_id, + const char *condition_key, + int64 total_elems, + Size max_payload_bytes, + int64 planned_bytes, + const char *token); +extern CustomScan *AnserBuildBloomConsumerScan(Plan *child, AttrNumber key_attno, + uint32 condition_id, + const char *condition_key, + int64 total_elems, + Size max_payload_bytes, + int64 planned_bytes, + const char *token); + +#endif /* ANSERPLAN_H */ diff --git a/gpcontrib/anser/src/anser.c b/gpcontrib/anser/src/anser.c new file mode 100644 index 00000000000..35d624daa8c --- /dev/null +++ b/gpcontrib/anser/src/anser.c @@ -0,0 +1,2041 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anser.c + * Shared-memory channel map for the Anser adaptive information + * sharing subsystem. + * + * IDENTIFICATION + * gpcontrib/anser/src/anser.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserfilter.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "miscadmin.h" +#include "storage/dsm_impl.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/proc.h" +#include "storage/procarray.h" +#include "storage/shmem.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/hsearch.h" +#include "utils/timestamp.h" +#include "utils/wait_event.h" + +#define ANSER_CONTROL_NAME "Anser Control" +#define ANSER_CHANNEL_HASH_NAME "Anser Channel Hash" +#define ANSER_SUBMISSION_QUEUE_NAME "Anser Submission Queue" +#define ANSER_WAIT_TABLE_NAME "Anser Consumer Wait Table" + +bool gp_anser_enable = false; +bool gp_anser_runtime_filter = false; +bool gp_anser_conn = false; /* marker GUC, set only via startup options */ +int gp_anser_max_channels = 0; /* 0 = auto (see AnserMaxChannels) */ +int gp_anser_max_info_size = 64 * 1024 * 1024 + 1024 * 1024; +int gp_anser_timeout_ms = 1000; +int gp_anser_max_consumers_per_channel = 64; + +/* + * Fallback per-connection channel budget used to auto-size the channel map when + * gp_max_slices is left unbounded (0). Each concurrent query can open at most + * one channel per runtime-filter slice, so the map is sized for + * max_connections * max_slices; when max_slices is unbounded we assume this many + * filter-carrying slices per query. Only used to derive the default; an + * explicit anser.max_channels overrides it entirely. + */ +#define ANSER_AUTO_SLICES_PER_CONN 8 + +/* + * Inbound submission queue. + * + * A remote producer backend (anser.publish) hands one part to the gather + * service through a free slot here, then blocks on its own proc latch until the + * gather service flips the slot to a terminal state and wakes it. The producer + * keeps its payload DSM segment attached for the whole wait, so the gather + * service can attach the same handle without a pin/unpin dance. + */ +typedef enum AnserSubmissionState +{ + ANSER_SUBMIT_FREE = 0, /* slot available */ + ANSER_SUBMIT_PENDING, /* filled by producer, awaiting gather */ + ANSER_SUBMIT_ACCEPTED, /* gather appended the part */ + ANSER_SUBMIT_REJECTED /* gather refused (cancel/overflow/lost DSM) */ +} AnserSubmissionState; + +typedef struct AnserSubmissionEntry +{ + AnserSubmissionState state; + AnserChannelKey key; + int32 expected_producers; + dsm_handle dsm_handle; /* producer's part, DSM_HANDLE_INVALID if none */ + Size len; + bool cancelled; + int producer_pid; + Latch *producer_latch; +} AnserSubmissionEntry; + +/* + * Consumer wait table. + * + * A remote consumer backend (anser.consume_wait) registers a slot and blocks + * on its proc latch. The send service, when a channel becomes READY, copies the + * payload into a fresh pinned DSM segment per waiting consumer, stamps the + * handle here, and wakes the consumer, which attaches, copies the bytes out, and + * frees the segment. On CANCELLED it just flips the slot and wakes. + */ +typedef enum AnserWaitSlotState +{ + ANSER_WAIT_FREE = 0, /* slot available */ + ANSER_WAIT_WAITING, /* consumer registered, blocked */ + ANSER_WAIT_DELIVERED, /* send service stamped a payload segment */ + ANSER_WAIT_CANCELLED /* send service cancelled this consumer */ +} AnserWaitSlotState; + +typedef struct AnserWaitSlot +{ + AnserWaitSlotState state; + AnserChannelKey key; + dsm_handle dsm_handle; /* per-consumer payload copy, pinned by sender */ + Size len; + int consumer_pid; + Latch *consumer_latch; +} AnserWaitSlot; + +/* + * The named tranche requested in _PG_init, resolved in AnserShmemInit(). NULL + * until then, which is one of the things AnserInitialized() checks. + */ +LWLock *AnserChannelLock = NULL; +LWLock *AnserRingLock = NULL; + +static AnserControl *AnserCtl = NULL; +static HTAB *AnserChannelHash = NULL; +static AnserSubmissionEntry *AnserSubmissionQueue = NULL; +static AnserWaitSlot *AnserWaitTable = NULL; +/* + * Data-path operations -- the internal machinery the public API and the gather/ + * send service cycles drive: producer submissions, gather apply, consumer wait + * slots, payload storage/delivery, and the maintenance sweeps. + */ +static int AnserEnqueueSubmission(const AnserChannelKey *channel_key, + int expected_producers, dsm_handle handle, + Size len, bool cancelled); +static bool AnserWaitSubmissionAck(int slot); +static void AnserAbandonSubmission(int slot); +static bool AnserGatherApply(const AnserChannelKey *channel_key, + int expected_producers, dsm_handle handle, + Size len, bool cancelled); +static int AnserRegisterWaitSlot(const AnserChannelKey *channel_key); +static bool AnserWaitSlotResult(int slot, void **payload, Size *payload_len, + bool *cancelled); +static void AnserAbandonWaitSlot(int slot); +static bool AnserWaitForState(const AnserChannelKey *channel_key, + long timeout_ms, bool registration_only, + bool *cancelled); +static bool AnserStorePayloadDSM(AnserChannelEntry *entry, + const void *payload, Size payload_len); +static void AnserReleasePayloadDSM(AnserChannelEntry *entry); +static bool AnserDeliverChannelData(const AnserChannelEntry *entry, + void *buffer, Size buffer_size, + Size *payload_len); +static void AnserCancelStaleChannels(void); +static void AnserSweepOrphanChannels(void); +static void AnserReapSubmissionSlots(void); +static void AnserReapWaitSlots(void); + +/* + * Internal helpers -- shared-memory sizing, small predicates, and key building + * used by the operations above. + */ +static bool AnserInitialized(void); +static Size AnserChannelHashSize(void); +static int AnserSubmissionQueueLen(void); +static int AnserWaitTableLen(void); +static Size AnserSubmissionQueueSize(void); +static Size AnserWaitTableSize(void); +static bool AnserPidIsLive(int pid); +static bool AnserChannelHasWaiters(const AnserChannelKey *channel_key); +static bool AnserChannelOwnerIsAlive(const AnserChannelEntry *entry); +static bool AnserChannelAccessAllowed(const AnserChannelKey *channel_key, + Oid caller_role, bool caller_is_super, + bool *found); + +/* + * Shared-memory setup -- one-time structure initialization at postmaster start. + */ +static void AnserInitializeControl(bool found); +static void AnserInitializeChannelHash(void); +static void AnserInitializeSubmissionQueue(bool found); +static void AnserInitializeWaitTable(bool found); + +Size +AnserShmemSize(void) +{ + Size size = 0; + + if (!gp_anser_enable) + return 0; + + size = add_size(size, MAXALIGN(sizeof(AnserControl))); + size = add_size(size, AnserChannelHashSize()); + size = add_size(size, AnserSubmissionQueueSize()); + size = add_size(size, AnserWaitTableSize()); + size = add_size(size, AnserAuthShmemSize()); + + return size; +} + +/* + * Create (postmaster) or attach to (EXEC_BACKEND child) the Anser shared state. + * Called from the shmem_startup_hook, after _PG_init has requested both the + * space and the LWLock tranche. + */ +void +AnserShmemInit(void) +{ + LWLockPadded *locks; + bool found; + + if (!gp_anser_enable) + return; + + locks = GetNamedLWLockTranche(ANSER_LWLOCK_TRANCHE); + AnserChannelLock = &locks[0].lock; + AnserRingLock = &locks[1].lock; + + AnserCtl = (AnserControl *) ShmemInitStruct(ANSER_CONTROL_NAME, + sizeof(AnserControl), + &found); + AnserInitializeControl(found); + AnserInitializeChannelHash(); + + AnserSubmissionQueue = (AnserSubmissionEntry *) + ShmemInitStruct(ANSER_SUBMISSION_QUEUE_NAME, + AnserSubmissionQueueSize(), &found); + AnserInitializeSubmissionQueue(found); + + AnserWaitTable = (AnserWaitSlot *) + ShmemInitStruct(ANSER_WAIT_TABLE_NAME, + AnserWaitTableSize(), &found); + AnserInitializeWaitTable(found); + + AnserAuthShmemInit(); +} + +bool +AnserSubscribe(const AnserChannelKey *channel_key) +{ + AnserChannelEntry *entry; + bool found; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &found); + if (!found) + { + LWLockRelease(AnserChannelLock); + return false; + } + + entry->consumers++; + entry->updated_at = GetCurrentTimestamp(); + LWLockRelease(AnserChannelLock); + + return true; +} + +bool +AnserPublish(const AnserChannelKey *channel_key, const void *payload, + Size payload_len, bool cancelled) +{ + AnserChannelEntry *entry; + bool found; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &found); + if (!found) + { + LWLockRelease(AnserChannelLock); + return false; + } + + if (cancelled) + { + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = GetCurrentTimestamp(); + SetLatch(&AnserCtl->send_latch); + LWLockRelease(AnserChannelLock); + return true; + } + + if (payload_len > (Size) gp_anser_max_info_size) + { + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = GetCurrentTimestamp(); + SetLatch(&AnserCtl->send_latch); + LWLockRelease(AnserChannelLock); + return false; + } + + if (entry->state == ANSER_CHANNEL_PENDING) + entry->state = ANSER_CHANNEL_COLLECTING; + + if (payload != NULL && payload_len > 0) + { + if (!AnserStorePayloadDSM(entry, payload, payload_len)) + { + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = GetCurrentTimestamp(); + SetLatch(&AnserCtl->send_latch); + LWLockRelease(AnserChannelLock); + return false; + } + } + + entry->done_producers++; + if (entry->done_producers >= entry->expected_producers) + entry->state = ANSER_CHANNEL_READY; + entry->updated_at = GetCurrentTimestamp(); + + SetLatch(&AnserCtl->gather_latch); + SetLatch(&AnserCtl->send_latch); + LWLockRelease(AnserChannelLock); + + return true; +} + +bool +AnserWaitProducersRegistered(const AnserChannelKey *channel_key, long timeout_ms) +{ + bool cancelled = false; + + return AnserWaitForState(channel_key, timeout_ms, true, &cancelled) && + !cancelled; +} + +bool +AnserWaitReady(const AnserChannelKey *channel_key, bool *cancelled) +{ + return AnserWaitForState(channel_key, -1, false, cancelled); +} + +bool +AnserConsumeReady(const AnserChannelKey *channel_key, void *buffer, + Size buffer_size, Size *payload_len, bool *cancelled) +{ + AnserChannelEntry *entry; + bool found; + bool ready; + bool is_cancelled; + bool delivered = false; + + Assert(payload_len != NULL); + Assert(cancelled != NULL); + + *payload_len = 0; + *cancelled = false; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + LWLockAcquire(AnserChannelLock, LW_SHARED); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &found); + if (!found) + { + LWLockRelease(AnserChannelLock); + return false; + } + + ready = (entry->state == ANSER_CHANNEL_READY); + is_cancelled = (entry->state == ANSER_CHANNEL_CANCELLED); + if (ready && !is_cancelled) + delivered = AnserDeliverChannelData(entry, buffer, buffer_size, + payload_len); + LWLockRelease(AnserChannelLock); + + if (!ready || is_cancelled || !delivered) + { + if (is_cancelled) + *cancelled = true; + return false; + } + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &found); + if (!found) + { + LWLockRelease(AnserChannelLock); + return false; + } + + is_cancelled = (entry->state == ANSER_CHANNEL_CANCELLED); + if (is_cancelled) + { + *cancelled = true; + LWLockRelease(AnserChannelLock); + return false; + } + + if (entry->state != ANSER_CHANNEL_READY) + { + LWLockRelease(AnserChannelLock); + return false; + } + + entry->done_consumers++; + if (entry->consumers == 0 || entry->done_consumers >= entry->consumers) + { + entry->state = ANSER_CHANNEL_CONSUMED; + AnserReleasePayloadDSM(entry); + } + entry->updated_at = GetCurrentTimestamp(); + LWLockRelease(AnserChannelLock); + + return true; +} + +AnserChannelState +AnserChannelGetState(const AnserChannelKey *channel_key, bool *found) +{ + AnserChannelEntry *entry; + bool local_found; + AnserChannelState state = ANSER_CHANNEL_CANCELLED; + + if (found != NULL) + *found = false; + + if (!AnserInitialized() || channel_key == NULL) + return state; + + LWLockAcquire(AnserChannelLock, LW_SHARED); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &local_found); + if (local_found) + state = entry->state; + LWLockRelease(AnserChannelLock); + + if (found != NULL) + *found = local_found; + return state; +} + +/* + * Bytes of payload the channel currently holds, or -1 if it is not in the map. + * Introspection for tests observing the payload-DSM lifetime: > 0 while a + * payload is pinned, 0 once it has been freed but the entry still lingers, and + * -1 once the entry has been reclaimed (payload freed and removed). + */ +int +AnserChannelPayloadBytes(const AnserChannelKey *channel_key) +{ + AnserChannelEntry *entry; + bool found; + int bytes = -1; + + if (!AnserInitialized() || channel_key == NULL) + return -1; + + LWLockAcquire(AnserChannelLock, LW_SHARED); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &found); + if (found) + bytes = (int) entry->data_len; + LWLockRelease(AnserChannelLock); + + return bytes; +} + +/* + * Number of consumers currently subscribed to a channel, or -1 if the channel + * is unknown. Read-only introspection used by tests to sequence a publish only + * after all expected consumers have registered. + */ +int +AnserChannelConsumerCount(const AnserChannelKey *channel_key) +{ + AnserChannelEntry *entry; + bool found; + int count = -1; + + if (!AnserInitialized() || channel_key == NULL) + return -1; + + LWLockAcquire(AnserChannelLock, LW_SHARED); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, + HASH_FIND, &found); + if (found) + count = entry->consumers; + LWLockRelease(AnserChannelLock); + + return count; +} + +void +AnserAttachServiceLatch(bool gather_service) +{ + if (!AnserInitialized()) + return; + + OwnLatch(gather_service ? &AnserCtl->gather_latch : &AnserCtl->send_latch); +} + +void +AnserDetachServiceLatch(bool gather_service) +{ + if (!AnserInitialized()) + return; + + DisownLatch(gather_service ? &AnserCtl->gather_latch : &AnserCtl->send_latch); +} + +void +AnserWaitServiceLatch(bool gather_service, long timeout_ms) +{ + if (!AnserInitialized()) + { + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + timeout_ms, + PG_WAIT_EXTENSION); + ResetLatch(MyLatch); + return; + } + + if (gather_service) + { + (void) WaitLatch(&AnserCtl->gather_latch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + timeout_ms, + PG_WAIT_EXTENSION); + ResetLatch(&AnserCtl->gather_latch); + } + else + { + (void) WaitLatch(&AnserCtl->send_latch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + timeout_ms, + PG_WAIT_EXTENSION); + ResetLatch(&AnserCtl->send_latch); + } +} + +void +AnserWakeServiceLatch(bool gather_service) +{ + if (!AnserInitialized()) + return; + + if (gather_service) + SetLatch(&AnserCtl->gather_latch); + else + SetLatch(&AnserCtl->send_latch); +} + +void +AnserServiceMaintenance(void) +{ + if (!AnserInitialized()) + return; + + /* + * The periodic sweep is gated so tests can pause reclamation and observe + * terminal (CANCELLED/CONSUMED) channels deterministically. Emergency + * reclamation on a full map is not gated -- it calls the sweep directly. + */ + if (!AnserCtl->sweep_enabled) + return; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + AnserSweepOrphanChannels(); + LWLockRelease(AnserChannelLock); +} + +/* + * Enable or disable the periodic maintenance sweep. Test-only: production + * always leaves it enabled. Toggling it lets a test freeze terminal channels + * in place (to assert their state) and then re-enable + force a sweep to prove + * reclamation works. + */ +void +AnserSetSweepEnabled(bool enabled) +{ + if (!AnserInitialized()) + return; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + AnserCtl->sweep_enabled = enabled; + LWLockRelease(AnserChannelLock); +} + +void +AnserCancelQuery(int gp_session_id, int gp_command_count) +{ + HASH_SEQ_STATUS status; + AnserChannelEntry *entry; + + if (!AnserInitialized()) + return; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + hash_seq_init(&status, AnserChannelHash); + while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) + { + if (entry->key.gp_session_id == gp_session_id && + entry->key.gp_command_count == gp_command_count) + { + /* + * Do not free the payload DSM here: a READY channel may already have + * DELIVERED wait slots borrowing it (consumers mid-read). Just mark + * it cancelled; the sweep releases the DSM once no slot still + * references it (unlike the gather/timeout cancels, this one can hit + * a channel past READY). + */ + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = GetCurrentTimestamp(); + } + } + SetLatch(&AnserCtl->send_latch); + LWLockRelease(AnserChannelLock); +} + +/* + * Register/refresh a channel on behalf of a remote producer and arm the produce + * deadline by moving it to COLLECTING. Idempotent: repeated begins from the + * several producers of one channel just refresh expected_producers and the + * deadline. This is the "a producer opened a connection" signal. + */ +bool +AnserProducerBegin(const AnserChannelKey *channel_key, int expected_producers, + Oid caller_role, bool caller_is_super) +{ + AnserChannelEntry *entry; + bool found; + TimestampTz now; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + if (expected_producers <= 0) + { + ereport(WARNING, + (errmsg("could not begin Anser channel: expected producers must be greater than zero"))); + return false; + } + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, + HASH_ENTER_NULL, &found); + if (entry == NULL) + { + AnserSweepOrphanChannels(); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, + HASH_ENTER_NULL, &found); + } + + if (entry == NULL) + { + LWLockRelease(AnserChannelLock); + ereport(WARNING, + (errmsg("could not begin Anser channel: channel map is full"))); + return false; + } + + /* + * A live channel belongs to the role that created it: another role may not + * hijack it by guessing its (session, command, condition) key. + */ + if (found && + entry->state != ANSER_CHANNEL_CANCELLED && + entry->state != ANSER_CHANNEL_CONSUMED && + !caller_is_super && + OidIsValid(entry->creator_role) && + entry->creator_role != caller_role) + { + LWLockRelease(AnserChannelLock); + return false; + } + + now = GetCurrentTimestamp(); + if (!found) + { + MemSet(entry, 0, sizeof(AnserChannelEntry)); + entry->key = *channel_key; + entry->state = ANSER_CHANNEL_PENDING; + entry->creator_role = caller_role; + entry->dsm_handle = DSM_HANDLE_INVALID; + } + else if (entry->state == ANSER_CHANNEL_CANCELLED || + entry->state == ANSER_CHANNEL_CONSUMED) + { + /* + * Terminal channel already on this key -- an anomaly, since keys are + * unique per (session, command, condition). Do not resurrect it: + * reviving a completed/aborted channel could strand a straggler wait slot + * or hand one query's data to another. Fail so the caller falls open and + * the sweep reclaims the leftover. + */ + LWLockRelease(AnserChannelLock); + return false; + } + + entry->expected_producers = expected_producers; + + /* + * Fix the consumer count when the channel is created, rather than having + * every consumer re-assert it: it is a property of the query topology (one + * consumer per segment executing the consumer slice) known here. The send + * service must deliver to all of them before recycling the payload. + * + * getgpsegmentCount() is the per-segment count, which matches the + * segment-executed filters Anser targets; a coordinator-only consumer + * slice would want 1, which this proxy does not represent. + */ + entry->expected_consumers = getgpsegmentCount(); + + if (entry->state == ANSER_CHANNEL_PENDING) + entry->state = ANSER_CHANNEL_COLLECTING; + entry->updated_at = now; + + SetLatch(&AnserCtl->gather_latch); + LWLockRelease(AnserChannelLock); + + return true; +} + +/* + * Remote-producer publish: hand one part to the gather service and block for + * its ACK. The payload is copied into a DSM segment kept attached for the whole + * wait, so the gather service can read it by handle. Fail-open: any local + * failure downgrades the submission to a cancel so the dataset dies cleanly + * rather than hanging consumers. + */ +bool +AnserProducerSubmit(const AnserChannelKey *channel_key, + int expected_producers, const void *payload, + Size payload_len, bool cancelled, + Oid caller_role, bool caller_is_super) +{ + dsm_segment *seg = NULL; + dsm_handle handle = DSM_HANDLE_INVALID; + int slot; + bool accepted; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + /* Refuse to feed a channel owned by a different role. */ + if (!AnserChannelAccessAllowed(channel_key, caller_role, caller_is_super, + NULL)) + return false; + + if (!cancelled && payload_len > (Size) gp_anser_max_info_size) + { + cancelled = true; + payload = NULL; + payload_len = 0; + } + + if (!cancelled && payload != NULL && payload_len > 0) + { + seg = dsm_create(payload_len, DSM_CREATE_NULL_IF_MAXSEGMENTS); + if (seg == NULL) + { + cancelled = true; + payload_len = 0; + } + else + { + memcpy(dsm_segment_address(seg), payload, payload_len); + handle = dsm_segment_handle(seg); + } + } + + slot = AnserEnqueueSubmission(channel_key, expected_producers, handle, + cancelled ? 0 : payload_len, cancelled); + if (slot < 0) + { + if (seg != NULL) + dsm_detach(seg); + return false; + } + + /* + * If we are interrupted while waiting for the ACK, reclaim our submission + * slot so it does not linger until this backend exits. (Our payload DSM is + * released by the aborting transaction's resource owner.) + */ + PG_TRY(); + { + accepted = AnserWaitSubmissionAck(slot); + } + PG_CATCH(); + { + AnserAbandonSubmission(slot); + PG_RE_THROW(); + } + PG_END_TRY(); + + if (seg != NULL) + dsm_detach(seg); + + return accepted; +} + +/* + * Give up a submission slot after the producer is interrupted mid-wait. If the + * gather service already finished with it, reclaim it now; if it is still + * pending, detach ourselves (clear pid/latch) so the gather neither wakes a gone + * backend nor leaves the slot for us to reclaim -- the reaper frees it once the + * gather marks it terminal. + */ +static void +AnserAbandonSubmission(int slot) +{ + AnserSubmissionEntry *e = &AnserSubmissionQueue[slot]; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + if (e->producer_pid == MyProcPid) + { + if (e->state == ANSER_SUBMIT_ACCEPTED || + e->state == ANSER_SUBMIT_REJECTED) + e->state = ANSER_SUBMIT_FREE; + else if (e->state == ANSER_SUBMIT_PENDING) + { + e->producer_pid = 0; + e->producer_latch = NULL; + } + } + LWLockRelease(AnserRingLock); +} + +/* + * Claim a free submission slot (waiting for one if the queue is momentarily + * full) and mark it PENDING for the gather service. Returns the slot index. + */ +static int +AnserEnqueueSubmission(const AnserChannelKey *channel_key, + int expected_producers, dsm_handle handle, + Size len, bool cancelled) +{ + int len_slots = AnserSubmissionQueueLen(); + + for (;;) + { + int i; + + CHECK_FOR_INTERRUPTS(); + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + for (i = 0; i < len_slots; i++) + { + AnserSubmissionEntry *e = &AnserSubmissionQueue[i]; + + if (e->state == ANSER_SUBMIT_FREE) + { + e->key = *channel_key; + e->expected_producers = expected_producers; + e->dsm_handle = handle; + e->len = len; + e->cancelled = cancelled; + e->producer_pid = MyProcPid; + e->producer_latch = &MyProc->procLatch; + e->state = ANSER_SUBMIT_PENDING; + LWLockRelease(AnserRingLock); + SetLatch(&AnserCtl->gather_latch); + return i; + } + } + LWLockRelease(AnserRingLock); + + ResetLatch(MyLatch); + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + ANSER_WAIT_POLL_INTERVAL_MS, PG_WAIT_EXTENSION); + } +} + +/* + * Block on the proc latch until the gather service reaches a terminal state for + * our slot, then release the slot and report whether the part was accepted. + */ +static bool +AnserWaitSubmissionAck(int slot) +{ + AnserSubmissionEntry *e = &AnserSubmissionQueue[slot]; + + for (;;) + { + AnserSubmissionState st; + + CHECK_FOR_INTERRUPTS(); + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + st = e->state; + if (st == ANSER_SUBMIT_ACCEPTED || st == ANSER_SUBMIT_REJECTED) + { + e->state = ANSER_SUBMIT_FREE; + LWLockRelease(AnserRingLock); + return st == ANSER_SUBMIT_ACCEPTED; + } + LWLockRelease(AnserRingLock); + + ResetLatch(MyLatch); + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + ANSER_WAIT_LATCH_TIMEOUT_MS, PG_WAIT_EXTENSION); + } +} + +/* + * Remote-consumer wait: subscribe, register a wait slot, and block on the proc + * latch until the send service delivers a payload or cancels this consumer. On + * success *payload points at a freshly palloc'd copy of the bytes. The calling + * backend does no channel-map polling; the wait happens entirely here. + */ +bool +AnserConsumerWait(const AnserChannelKey *channel_key, void **payload, + Size *payload_len, bool *cancelled, + Oid caller_role, bool caller_is_super) +{ + int slot; + bool result; + + if (payload != NULL) + *payload = NULL; + if (payload_len != NULL) + *payload_len = 0; + if (cancelled != NULL) + *cancelled = false; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + /* + * Wait for a producer to announce the channel before subscribing. In the + * plan tree producers sit below their consumers, so the channel is usually + * registered first; but execution order across the cluster is not + * guaranteed, so a consumer that arrives early waits (up to + * anser.timeout_ms) for registration instead of failing open at once. + * A false return means the producer never registered in time, or the + * dataset was already cancelled -- either way this consumer fails open. + */ + if (!AnserWaitProducersRegistered(channel_key, (long) gp_anser_timeout_ms)) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + /* + * Only the owning role (or a superuser) may read a channel. Checked after + * registration so there is a recorded creator_role to compare against. + */ + if (!AnserChannelAccessAllowed(channel_key, caller_role, caller_is_super, + NULL)) + return false; + + /* + * Subscribe before registering the wait slot so the channel's consumer + * count is never lower than the number of live wait slots; the send service + * relies on that ordering for its recycle accounting. + */ + if (!AnserSubscribe(channel_key)) + return false; + + slot = AnserRegisterWaitSlot(channel_key); + if (slot < 0) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + SetLatch(&AnserCtl->send_latch); + + /* + * Reclaim our wait slot (and unpin any payload the send service already + * stamped) if we are interrupted before collecting the result, so it does + * not linger until this backend exits. + */ + PG_TRY(); + { + result = AnserWaitSlotResult(slot, payload, payload_len, cancelled); + } + PG_CATCH(); + { + AnserAbandonWaitSlot(slot); + PG_RE_THROW(); + } + PG_END_TRY(); + + return result; +} + +/* + * Give up a wait slot after the consumer is interrupted mid-wait, unpinning any + * per-consumer payload copy the send service stamped but we never collected. + * Guarded by pid so a slot already reclaimed and reused is left untouched. + */ +static void +AnserAbandonWaitSlot(int slot) +{ + AnserWaitSlot *s = &AnserWaitTable[slot]; + AnserChannelKey key; + bool was_waiting = false; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + if (s->consumer_pid == MyProcPid && s->state != ANSER_WAIT_FREE) + { + /* + * The slot's dsm_handle is borrowed from the channel (which owns and + * frees the payload DSM), so abandoning just stops this slot from + * borrowing -- do not unpin it here. + */ + was_waiting = (s->state == ANSER_WAIT_WAITING); + key = s->key; + s->dsm_handle = DSM_HANDLE_INVALID; + s->len = 0; + s->state = ANSER_WAIT_FREE; + } + LWLockRelease(AnserRingLock); + + /* + * A consumer that abandons before any data was delivered to it must no + * longer count toward the channel's expected consumer total; otherwise the + * send service's "delivered to every consumer" recycle test can never be + * satisfied and the channel lingers in READY forever. (A slot that was + * already DELIVERED is left counted: the send service incremented + * done_consumers for it, so the accounting still balances.) + */ + if (was_waiting) + { + AnserChannelEntry *entry; + bool found; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, &key, + HASH_FIND, &found); + if (found && entry->consumers > 0) + { + entry->consumers--; + /* Let the send service re-evaluate recycling. */ + SetLatch(&AnserCtl->send_latch); + } + LWLockRelease(AnserChannelLock); + } +} + +/* + * Claim a free wait-table slot for this consumer. Returns the slot index, or + * -1 if the table is full (the consumer then fails open). + */ +static int +AnserRegisterWaitSlot(const AnserChannelKey *channel_key) +{ + int len_slots = AnserWaitTableLen(); + int i; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + for (i = 0; i < len_slots; i++) + { + AnserWaitSlot *s = &AnserWaitTable[i]; + + if (s->state == ANSER_WAIT_FREE) + { + s->key = *channel_key; + s->dsm_handle = DSM_HANDLE_INVALID; + s->len = 0; + s->consumer_pid = MyProcPid; + s->consumer_latch = &MyProc->procLatch; + s->state = ANSER_WAIT_WAITING; + LWLockRelease(AnserRingLock); + return i; + } + } + LWLockRelease(AnserRingLock); + + return -1; +} + +/* + * Block until the send service resolves our wait slot. On DELIVERED, attach the + * per-consumer payload segment, copy it into palloc'd memory, and free the + * segment (the send service pinned it and handed us ownership). + */ +static bool +AnserWaitSlotResult(int slot, void **payload, Size *payload_len, + bool *cancelled) +{ + AnserWaitSlot *s = &AnserWaitTable[slot]; + AnserChannelKey slot_key = s->key; + + for (;;) + { + AnserWaitSlotState st; + dsm_handle handle = DSM_HANDLE_INVALID; + Size len = 0; + + CHECK_FOR_INTERRUPTS(); + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + st = s->state; + if (st == ANSER_WAIT_DELIVERED) + { + /* + * Read the borrowed channel payload handle but leave the slot + * DELIVERED: that keeps the channel's payload DSM alive (the sweep + * will not reclaim a channel with a DELIVERED slot) until we have + * copied it out below. We flip the slot to FREE only afterward. + */ + handle = s->dsm_handle; + len = s->len; + } + else if (st == ANSER_WAIT_CANCELLED) + { + s->state = ANSER_WAIT_FREE; + } + LWLockRelease(AnserRingLock); + + if (st == ANSER_WAIT_CANCELLED) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + if (st == ANSER_WAIT_DELIVERED) + { + void *buf = NULL; + bool vanished = false; + + if (handle != DSM_HANDLE_INVALID) + { + dsm_segment *seg = dsm_attach(handle); + + if (seg == NULL) + vanished = true; /* should not happen: we hold DELIVERED */ + else + { + if (len > 0) + { + buf = palloc(len); + memcpy(buf, dsm_segment_address(seg), len); + } + /* Borrowed handle -- detach, but the channel owns/frees it. */ + dsm_detach(seg); + } + } + + /* Done reading: release the slot so the channel can be reclaimed. */ + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + if (s->state == ANSER_WAIT_DELIVERED) + s->state = ANSER_WAIT_FREE; + LWLockRelease(AnserRingLock); + + if (vanished) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + if (payload != NULL) + *payload = buf; + if (payload_len != NULL) + *payload_len = len; + return true; + } + + /* + * Still WAITING. Guard against the registration/recycle race: if our + * channel has already been recycled (CONSUMED/CANCELLED) or swept out of + * the map between AnserWaitProducersRegistered and our slot + * registration, the send service will never resolve this slot -- there + * is no live payload to deliver. Reclaim the slot ourselves and fail + * open rather than block forever. + */ + if (st == ANSER_WAIT_WAITING) + { + bool found = false; + AnserChannelState cstate = AnserChannelGetState(&slot_key, &found); + + if (!found || + cstate == ANSER_CHANNEL_CANCELLED || + cstate == ANSER_CHANNEL_CONSUMED) + { + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + if (s->consumer_pid == MyProcPid && + s->state == ANSER_WAIT_WAITING) + s->state = ANSER_WAIT_FREE; + LWLockRelease(AnserRingLock); + + if (cancelled != NULL) + *cancelled = true; + return false; + } + } + + ResetLatch(MyLatch); + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + ANSER_WAIT_LATCH_TIMEOUT_MS, PG_WAIT_EXTENSION); + } +} + +/* + * One gather-service pass: drain the submission queue, cancel channels that have + * sat in COLLECTING past the produce deadline, and reclaim slots left behind by + * producers that died mid-wait. + */ +void +AnserGatherServiceCycle(void) +{ + int len_slots; + int i; + + if (!AnserInitialized() || AnserSubmissionQueue == NULL) + return; + + len_slots = AnserSubmissionQueueLen(); + for (i = 0; i < len_slots; i++) + { + AnserSubmissionEntry *e = &AnserSubmissionQueue[i]; + AnserChannelKey key; + int32 expected_producers; + dsm_handle handle; + Size len; + bool cancelled; + bool accepted; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + if (e->state != ANSER_SUBMIT_PENDING) + { + LWLockRelease(AnserRingLock); + continue; + } + key = e->key; + expected_producers = e->expected_producers; + handle = e->dsm_handle; + len = e->len; + cancelled = e->cancelled; + LWLockRelease(AnserRingLock); + + accepted = AnserGatherApply(&key, expected_producers, handle, len, + cancelled); + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + /* The slot is still ours: only the gather service leaves PENDING. */ + e->state = accepted ? ANSER_SUBMIT_ACCEPTED : ANSER_SUBMIT_REJECTED; + if (e->producer_latch != NULL) + SetLatch(e->producer_latch); + LWLockRelease(AnserRingLock); + } + + AnserCancelStaleChannels(); + AnserReapSubmissionSlots(); +} + +/* + * Apply one submitted part to its channel: attach the producer's payload, append + * it (or cancel the dataset), and advance the channel toward READY. Mirrors the + * direct AnserPublish path but sourced from a DSM handle. + */ +static bool +AnserGatherApply(const AnserChannelKey *channel_key, int expected_producers, + dsm_handle handle, Size len, bool cancelled) +{ + AnserChannelEntry *entry; + bool found; + dsm_segment *seg = NULL; + void *addr = NULL; + + if (!cancelled && handle != DSM_HANDLE_INVALID && len > 0) + { + seg = dsm_attach(handle); + if (seg == NULL) + cancelled = true; /* producer gone / segment lost */ + else + addr = dsm_segment_address(seg); + } + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, + HASH_FIND, &found); + + /* + * No producer_begin registered this channel (or it was already recycled): + * refuse the part rather than creating an unowned channel, which would + * bypass the creator_role access check. The client always begins before + * publishing, so a legitimate part always finds its channel here. A + * dataset already in a terminal state likewise refuses late parts. + */ + if (!found || + entry->state == ANSER_CHANNEL_CANCELLED || + entry->state == ANSER_CHANNEL_CONSUMED) + { + LWLockRelease(AnserChannelLock); + if (seg != NULL) + dsm_detach(seg); + return false; + } + + if (expected_producers > 0) + entry->expected_producers = expected_producers; + + if (cancelled) + { + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = GetCurrentTimestamp(); + AnserReleasePayloadDSM(entry); + LWLockRelease(AnserChannelLock); + if (seg != NULL) + dsm_detach(seg); + SetLatch(&AnserCtl->send_latch); + return true; + } + + if (entry->state == ANSER_CHANNEL_PENDING) + entry->state = ANSER_CHANNEL_COLLECTING; + + if (addr != NULL && len > 0) + { + if (!AnserStorePayloadDSM(entry, addr, len)) + { + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = GetCurrentTimestamp(); + AnserReleasePayloadDSM(entry); + LWLockRelease(AnserChannelLock); + if (seg != NULL) + dsm_detach(seg); + SetLatch(&AnserCtl->send_latch); + return false; + } + } + + entry->done_producers++; + if (entry->done_producers >= entry->expected_producers) + entry->state = ANSER_CHANNEL_READY; + entry->updated_at = GetCurrentTimestamp(); + LWLockRelease(AnserChannelLock); + + if (seg != NULL) + dsm_detach(seg); + SetLatch(&AnserCtl->send_latch); + + return true; +} + +/* + * Cancel any channel that announced producers (COLLECTING) but did not reach + * READY within anser.timeout_ms. Cancellation is whole-dataset: + * all-parts-or-nothing. + */ +static void +AnserCancelStaleChannels(void) +{ + HASH_SEQ_STATUS status; + AnserChannelEntry *entry; + TimestampTz now = GetCurrentTimestamp(); + bool any = false; + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + hash_seq_init(&status, AnserChannelHash); + while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) + { + if (entry->state == ANSER_CHANNEL_COLLECTING && + TimestampDifferenceExceeds(entry->updated_at, now, + gp_anser_timeout_ms)) + { + entry->state = ANSER_CHANNEL_CANCELLED; + entry->updated_at = now; + AnserReleasePayloadDSM(entry); + any = true; + } + } + LWLockRelease(AnserChannelLock); + + if (any) + SetLatch(&AnserCtl->send_latch); +} + +/* + * Reclaim terminal submission slots whose producer backend has exited without + * consuming the ACK (e.g. cancelled mid-wait). + */ +static void +AnserReapSubmissionSlots(void) +{ + int len_slots = AnserSubmissionQueueLen(); + int i; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + for (i = 0; i < len_slots; i++) + { + AnserSubmissionEntry *e = &AnserSubmissionQueue[i]; + + if ((e->state == ANSER_SUBMIT_ACCEPTED || + e->state == ANSER_SUBMIT_REJECTED) && + !AnserPidIsLive(e->producer_pid)) + e->state = ANSER_SUBMIT_FREE; + } + LWLockRelease(AnserRingLock); +} + +/* + * One send-service pass: deliver every READY/CANCELLED channel to its waiting + * consumers and reclaim slots left behind by consumers that have exited. + */ +void +AnserSendServiceCycle(void) +{ + HASH_SEQ_STATUS status; + AnserChannelEntry *entry; + int len_slots; + + if (!AnserInitialized() || AnserWaitTable == NULL) + return; + + len_slots = AnserWaitTableLen(); + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + hash_seq_init(&status, AnserChannelHash); + while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) + { + bool ready = (entry->state == ANSER_CHANNEL_READY); + + /* + * Stragglers that registered after a channel finished (cancelled, or + * already consumed) can no longer be handed data; they are delivered a + * cancel so they fail open instead of blocking forever on a channel the + * sweep would otherwise never reclaim. + */ + bool cancel_waiters = (entry->state == ANSER_CHANNEL_CANCELLED || + entry->state == ANSER_CHANNEL_CONSUMED); + int i; + + if (!ready && !cancel_waiters) + continue; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + for (i = 0; i < len_slots; i++) + { + AnserWaitSlot *s = &AnserWaitTable[i]; + + if (s->state != ANSER_WAIT_WAITING) + continue; + if (memcmp(&s->key, &entry->key, sizeof(AnserChannelKey)) != 0) + continue; + + if (cancel_waiters) + { + s->dsm_handle = DSM_HANDLE_INVALID; + s->len = 0; + s->state = ANSER_WAIT_CANCELLED; + if (s->consumer_latch != NULL) + SetLatch(s->consumer_latch); + continue; + } + + /* + * READY: lend this consumer the channel's single payload segment -- + * the slot borrows entry->dsm_handle rather than getting its own + * copy. The consumer copies it out and only then frees the slot; the + * payload DSM is released once the channel is reclaimed with no slot + * still borrowing it (AnserChannelHasWaiters / the sweep). Holding + * the slot DELIVERED keeps that handle alive across the read. + */ + s->dsm_handle = entry->dsm_handle; + s->len = entry->data_len; + s->state = ANSER_WAIT_DELIVERED; + if (s->consumer_latch != NULL) + SetLatch(s->consumer_latch); + + entry->done_consumers++; + } + LWLockRelease(AnserRingLock); + + /* + * Recycle once every expected consumer has been handed the payload. Do + * NOT free the payload DSM here: consumers still hold DELIVERED slots + * that borrow it. It is released when the sweep reclaims this now + * terminal channel, after every borrowing slot has drained. + */ + if (ready && entry->expected_consumers > 0 && + entry->done_consumers >= entry->expected_consumers) + { + entry->state = ANSER_CHANNEL_CONSUMED; + entry->updated_at = GetCurrentTimestamp(); + } + } + LWLockRelease(AnserChannelLock); + + AnserReapWaitSlots(); +} + +/* + * Reclaim wait slots whose consumer backend has exited, unpinning any payload + * copy the send service already stamped but the consumer never collected. + */ +static void +AnserReapWaitSlots(void) +{ + int len_slots = AnserWaitTableLen(); + int i; + + LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); + for (i = 0; i < len_slots; i++) + { + AnserWaitSlot *s = &AnserWaitTable[i]; + + if (s->state == ANSER_WAIT_FREE) + continue; + if (AnserPidIsLive(s->consumer_pid)) + continue; + + /* + * A dead consumer's slot is freed without touching its dsm_handle: that + * handle is borrowed from the channel (the channel owns and frees the + * payload DSM), so freeing the slot just stops it from borrowing. + */ + s->dsm_handle = DSM_HANDLE_INVALID; + s->len = 0; + s->state = ANSER_WAIT_FREE; + } + LWLockRelease(AnserRingLock); +} + +/* + * Effective size of the channel map. + * + * When anser.max_channels is set explicitly (> 0) it wins. Otherwise the map + * is auto-sized to max_connections * max_slices: at most MaxConnections + * concurrent queries, each opening up to gp_max_slices runtime-filter channels. + * gp_max_slices == 0 means "unbounded", for which we substitute a fixed + * per-connection budget (ANSER_AUTO_SLICES_PER_CONN) so the map stays finite. + * + * This value sizes fixed shared memory at postmaster start, so it must be stable + * for the life of the postmaster and identical in every backend. MaxConnections + * is PGC_POSTMASTER (stable), but gp_max_slices is PGC_USERSET, so we cache the + * computed value on first use. That first use is the postmaster's shmem-sizing + * pass (before any backend forks or any session runs SET), so the cache captures + * the postmaster-level gp_max_slices and is inherited unchanged by every + * backend -- a later per-session SET gp_max_slices cannot resize the map. + * + * Exposed (non-static) so the regression suite can prove the cache holds: see + * anser_test_max_channels_stable_across_slices(). + */ +int +AnserMaxChannels(void) +{ + static int cached = 0; + int slices; + int64 v; + + if (gp_anser_max_channels > 0) + return gp_anser_max_channels; + + if (cached > 0) + return cached; + + slices = (gp_max_slices > 0) ? gp_max_slices : ANSER_AUTO_SLICES_PER_CONN; + v = (int64) MaxConnections * (int64) slices; + + if (v < 1) + v = 1; + if (v > INT_MAX) + v = INT_MAX; + + cached = (int) v; + return cached; +} + +static Size +AnserChannelHashSize(void) +{ + return hash_estimate_size(AnserMaxChannels(), + sizeof(AnserChannelEntry)); +} + +/* + * The submission queue holds parts in flight between blocked producers and the + * gather service. Every segment producing for a channel submits its own part, + * and they hand off concurrently, so -- like the consumer wait table -- we size + * for one in-flight slot per producer per channel (channels * per-channel + * producers, which mirrors the per-channel consumer count = segment count). + * Producers that still find it full wait for a free slot rather than failing. + */ +static int +AnserSubmissionQueueLen(void) +{ + int64 len = (int64) AnserMaxChannels() * + (int64) gp_anser_max_consumers_per_channel; + + /* Guard against int overflow from extreme GUC settings. */ + if (len > INT_MAX) + len = INT_MAX; + + return (int) len; +} + +static int +AnserWaitTableLen(void) +{ + int64 len = (int64) AnserMaxChannels() * + (int64) gp_anser_max_consumers_per_channel; + + /* Guard against int overflow from extreme GUC settings. */ + if (len > INT_MAX) + len = INT_MAX; + + return (int) len; +} + +static Size +AnserSubmissionQueueSize(void) +{ + return mul_size(sizeof(AnserSubmissionEntry), + (Size) AnserSubmissionQueueLen()); +} + +static Size +AnserWaitTableSize(void) +{ + return mul_size(sizeof(AnserWaitSlot), (Size) AnserWaitTableLen()); +} + +static void +AnserInitializeSubmissionQueue(bool found) +{ + if (!found) + MemSet(AnserSubmissionQueue, 0, AnserSubmissionQueueSize()); +} + +static void +AnserInitializeWaitTable(bool found) +{ + if (!found) + MemSet(AnserWaitTable, 0, AnserWaitTableSize()); +} + +static bool +AnserPidIsLive(int pid) +{ + if (pid == 0) + return false; + + return BackendPidGetProc(pid) != NULL; +} + +/* + * Does any consumer still have a WAITING wait slot for this channel? Callers + * hold AnserChannelLock; we take AnserRingLock (channel-lock-then-ring-lock + * order, matching the send cycle) to read the wait table. + */ +static bool +AnserChannelHasWaiters(const AnserChannelKey *channel_key) +{ + int len_slots; + int i; + bool found = false; + + if (AnserWaitTable == NULL) + return false; + + len_slots = AnserWaitTableLen(); + LWLockAcquire(AnserRingLock, LW_SHARED); + for (i = 0; i < len_slots; i++) + { + /* + * A slot still references the channel while it is WAITING (not yet + * delivered) or DELIVERED (delivered but the consumer has not finished + * copying the borrowed payload out). Either blocks reclaim: the sweep + * must not free the payload DSM while a DELIVERED slot could still attach + * it. + */ + if ((AnserWaitTable[i].state == ANSER_WAIT_WAITING || + AnserWaitTable[i].state == ANSER_WAIT_DELIVERED) && + memcmp(&AnserWaitTable[i].key, channel_key, + sizeof(AnserChannelKey)) == 0) + { + found = true; + break; + } + } + LWLockRelease(AnserRingLock); + + return found; +} + +/* + * May this caller produce/consume on the channel? A superuser always may; any + * other role may only touch a channel it created. An unknown channel, or one + * with no recorded creator, is permitted here -- callers handle "not found" + * through their normal paths. If found is non-NULL it receives whether the + * channel currently exists. + */ +static bool +AnserChannelAccessAllowed(const AnserChannelKey *channel_key, Oid caller_role, + bool caller_is_super, bool *found) +{ + AnserChannelEntry *entry; + bool local_found; + bool allowed = true; + + LWLockAcquire(AnserChannelLock, LW_SHARED); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, + HASH_FIND, &local_found); + if (local_found && !caller_is_super && + OidIsValid(entry->creator_role) && + entry->creator_role != caller_role) + allowed = false; + LWLockRelease(AnserChannelLock); + + if (found != NULL) + *found = local_found; + + return allowed; +} + +static void +AnserInitializeControl(bool found) +{ + if (!found) + { + MemSet(AnserCtl, 0, sizeof(AnserControl)); + AnserCtl->max_channels = AnserMaxChannels(); + AnserCtl->max_info_size = gp_anser_max_info_size; + InitSharedLatch(&AnserCtl->gather_latch); + InitSharedLatch(&AnserCtl->send_latch); + AnserCtl->sweep_enabled = true; + } +} + +static void +AnserInitializeChannelHash(void) +{ + HASHCTL hctl; + + MemSet(&hctl, 0, sizeof(hctl)); + hctl.keysize = sizeof(AnserChannelKey); + hctl.entrysize = sizeof(AnserChannelEntry); + + AnserChannelHash = ShmemInitHash(ANSER_CHANNEL_HASH_NAME, + AnserMaxChannels(), + AnserMaxChannels(), + &hctl, + HASH_ELEM | HASH_BLOBS); +} + +/* + * Recycle terminal or orphaned channels before declaring registration failure. + */ +static void +AnserSweepOrphanChannels(void) +{ + HASH_SEQ_STATUS status; + AnserChannelEntry *entry; + AnserChannelKey *remove_keys; + int remove_count = 0; + int i; + + Assert(LWLockHeldByMeInMode(AnserChannelLock, LW_EXCLUSIVE)); + + if (AnserChannelHash == NULL) + return; + + remove_keys = (AnserChannelKey *) palloc(sizeof(AnserChannelKey) * + (Size) AnserMaxChannels()); + + hash_seq_init(&status, AnserChannelHash); + while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) + { + bool recycle = false; + + if (entry->state == ANSER_CHANNEL_CONSUMED || + entry->state == ANSER_CHANNEL_CANCELLED) + recycle = true; + else if (!AnserChannelOwnerIsAlive(entry)) + recycle = true; + + /* + * Never recycle a channel that still has consumers blocked on it: the + * send service must first deliver the payload or a cancel to those wait + * slots. Removing the channel out from under them would strand the + * consumers, which only wake on their slot. + */ + if (recycle && AnserChannelHasWaiters(&entry->key)) + recycle = false; + + if (recycle) + { + AnserReleasePayloadDSM(entry); + remove_keys[remove_count++] = entry->key; + } + } + + for (i = 0; i < remove_count; i++) + (void) hash_search(AnserChannelHash, + &remove_keys[i], + HASH_REMOVE, + NULL); + + pfree(remove_keys); +} + +/* + * Is the query that owns this channel still alive? + * + * Validation is deliberately conservative, at session granularity rather than + * per query/command: a channel lives as long as its owning coordinator session + * does, and AnserCancelQuery() provides explicit cleanup at query end/failure. + */ +static bool +AnserChannelOwnerIsAlive(const AnserChannelEntry *entry) +{ + Assert(entry != NULL); + + /* + * A channel belongs to one query, identified by gp_session_id. It is alive + * as long as that coordinator (QD) session still has a backend in the proc + * array; once the session is gone -- query finished/aborted, or a fixed test + * session id that never maps to a live backend -- the channel is orphaned and + * may be reclaimed. + * + * Liveness is deliberately tied to the session, NOT to the backend that + * created the channel: network-path producers create it from a short-lived + * libpq request backend (AnserClientPublish PQfinish's the connection right + * after publishing), so that backend is normally already gone while the + * channel is still needed by consumers. + */ + if (entry->key.gp_session_id <= 0) + return true; /* no session to check against; keep it */ + + return FindProcByGpSessionId((long) entry->key.gp_session_id) != NULL; +} + +static bool +AnserStorePayloadDSM(AnserChannelEntry *entry, const void *payload, + Size payload_len) +{ + dsm_segment *acc_seg = NULL; + dsm_segment *new_seg; + void *acc_addr = NULL; + + Assert(LWLockHeldByMeInMode(AnserChannelLock, LW_EXCLUSIVE)); + Assert(entry != NULL); + + if (payload == NULL || payload_len == 0) + return true; + + if (payload_len > (Size) gp_anser_max_info_size) + return false; + + /* Attach the channel's running merged payload, if it already has one. */ + if (entry->dsm_handle != DSM_HANDLE_INVALID && entry->data_len > 0) + { + acc_seg = dsm_attach(entry->dsm_handle); + if (acc_seg == NULL) + return false; + acc_addr = dsm_segment_address(acc_seg); + } + + /* + * Subsequent part: fold it into the existing payload in place. Every part on + * a channel shares the same (condition-key-derived) bloom parameters, so it is + * the same serialized size and the union is a bitwise OR of the bitsets -- no + * fresh segment, no full-payload copy. Safe because we hold AnserChannelLock + * and consumers only ever read their own copies. A part that cannot fold in + * place (wrong size, malformed) is rejected: the caller then cancels the + * channel and its consumers fail open. + */ + if (acc_addr != NULL) + { + bool folded = AnserBloomFoldPartInPlace(acc_addr, entry->data_len, + payload, payload_len); + + dsm_detach(acc_seg); + return folded; + } + + /* + * First part: store it verbatim in a fresh, pinned segment. The coordinator + * never reconstructs a filter from the payload -- it only copies the first + * part and OR-folds the rest -- so no bitset parameters are needed here. + */ + new_seg = dsm_create(payload_len, DSM_CREATE_NULL_IF_MAXSEGMENTS); + if (new_seg == NULL) + return false; + memcpy(dsm_segment_address(new_seg), payload, payload_len); + + AnserReleasePayloadDSM(entry); + dsm_pin_segment(new_seg); + entry->dsm_handle = dsm_segment_handle(new_seg); + entry->data_len = payload_len; + dsm_detach(new_seg); + + return true; +} + +static void +AnserReleasePayloadDSM(AnserChannelEntry *entry) +{ + Assert(LWLockHeldByMeInMode(AnserChannelLock, LW_EXCLUSIVE)); + + if (entry == NULL || entry->dsm_handle == DSM_HANDLE_INVALID) + return; + + dsm_unpin_segment(entry->dsm_handle); + entry->dsm_handle = DSM_HANDLE_INVALID; + entry->data_len = 0; +} + +/* + * Consume a ready channel payload. + */ +static bool +AnserDeliverChannelData(const AnserChannelEntry *entry, void *buffer, + Size buffer_size, Size *payload_len) +{ + dsm_segment *seg; + void *addr; + + Assert(LWLockHeldByMe(AnserChannelLock)); + Assert(entry != NULL); + Assert(payload_len != NULL); + + if (entry->dsm_handle == DSM_HANDLE_INVALID) + { + *payload_len = 0; + return (entry->data_len == 0); + } + + if (buffer == NULL && entry->data_len > 0) + return false; + + if (buffer_size < entry->data_len) + return false; + + seg = dsm_attach(entry->dsm_handle); + if (seg == NULL) + return false; + + addr = dsm_segment_address(seg); + if (entry->data_len > 0) + memcpy(buffer, addr, entry->data_len); + dsm_detach(seg); + + *payload_len = entry->data_len; + + return true; +} + +static bool +AnserWaitForState(const AnserChannelKey *channel_key, long timeout_ms, + bool registration_only, bool *cancelled) +{ + TimestampTz start_time = GetCurrentTimestamp(); + + if (cancelled != NULL) + *cancelled = false; + + if (!AnserInitialized() || channel_key == NULL) + return false; + + for (;;) + { + AnserChannelEntry *entry; + bool found; + bool registered; + bool ready; + bool is_cancelled; + + CHECK_FOR_INTERRUPTS(); + + LWLockAcquire(AnserChannelLock, LW_SHARED); + entry = (AnserChannelEntry *) hash_search(AnserChannelHash, + channel_key, + HASH_FIND, + &found); + registered = found && entry->expected_producers > 0; + ready = found && entry->state == ANSER_CHANNEL_READY; + is_cancelled = found && entry->state == ANSER_CHANNEL_CANCELLED; + LWLockRelease(AnserChannelLock); + + if (is_cancelled) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + if (registration_only) + { + if (registered) + return true; + } + else if (ready) + return true; + + if (timeout_ms >= 0 && + TimestampDifferenceExceeds(start_time, GetCurrentTimestamp(), + timeout_ms)) + return false; + + ResetLatch(MyLatch); + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + ANSER_WAIT_POLL_INTERVAL_MS, + PG_WAIT_EXTENSION); + } +} + +static bool +AnserInitialized(void) +{ + return gp_anser_enable && AnserChannelLock != NULL && AnserCtl != NULL && + AnserChannelHash != NULL && AnserWaitTable != NULL; +} diff --git a/gpcontrib/anser/src/anserauth.c b/gpcontrib/anser/src/anserauth.c new file mode 100644 index 00000000000..a213eb0a8f6 --- /dev/null +++ b/gpcontrib/anser/src/anserauth.c @@ -0,0 +1,430 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserauth.c + * Authentication of the segment -> coordinator Anser connections. + * + * Two halves of one mechanism. The QD side owns a shared-memory hash of + * per-session tokens, handed out at plan time (AnserGetOrCreateSessionToken) + * and verified when a segment presents one (AnserSessionTokenIsValid). The + * backend side supplies the two functions _PG_init installs as the server's + * custom-authentication hooks: AnserConnClaims recognizes an Anser connection + * from its startup marker, and AnserConnCheckPassword accepts or rejects the + * token it sends as password. The wire exchange itself stays in + * libpq/auth.c -- see the CustomAuth*_hook comments there. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserauth.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "anser.h" +#include "cdb/cdbvars.h" +#include "common/hashfn.h" +#include "libpq/libpq-be.h" +#include "miscadmin.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/hsearch.h" + +#define ANSER_TOKEN_HASH_NAME "Anser Session Token Hash" + +/* + * Session token hash. + * + * Remote (segment) producers/consumers authenticate their libpq connection to + * the QD with a per-session random token instead of relying on pg_hba entries + * covering the segment hosts -- the parallel-retrieve-cursor model (see + * retrieve_conn_authentication in libpq/auth.c). The QD registers one token + * per (gp_session_id, session user) when a plan gets its first injected + * runtime filter and embeds the token in the dispatched plan; the segment + * connects with anser.conn=true and presents the token as the password, + * and AnserSessionTokenIsValid() verifies it here. Entries are removed when + * the owning QD session exits. + */ +#define ANSER_TOKEN_BYTES 16 /* 128 bits, as ENDPOINT_TOKEN_ARR_LEN */ +#define ANSER_TOKEN_HEX_LEN (ANSER_TOKEN_BYTES * 2) + +/* Token hash key: one token per (gp_session_id, session user). */ +typedef struct AnserTokenTag +{ + int session_id; + Oid user_id; +} AnserTokenTag; + +/* Token hash entry: the hex-encoded random token registered by a session. */ +typedef struct AnserTokenEntry +{ + AnserTokenTag tag; + char token_hex[ANSER_TOKEN_HEX_LEN + 1]; +} AnserTokenEntry; + +static HTAB *AnserTokenHash = NULL; + +/* Set once this backend has registered its session-token cleanup hook. */ +static bool anser_token_exit_registered = false; + +static bool AnserAuthInitialized(void); +static void AnserInitializeTokenHash(void); +static void AnserTokenSessionCleanup(int code, Datum arg); + +/* + * Client authentication for incoming segment -> QD connections. + */ +static bool AnserConnMarkedInCmdOptions(char *cmd_options); +static bool AnserConnMarkedInGucOptions(List *guc_options); + +/* + * Shared-memory sizing and setup for the session-token hash, called from + * AnserShmemSize() / AnserShmemInit() so all Anser shared state is requested + * and created in one place. + */ +Size +AnserAuthShmemSize(void) +{ + return hash_estimate_size(MaxConnections, sizeof(AnserTokenEntry)); +} + +void +AnserAuthShmemInit(void) +{ + AnserInitializeTokenHash(); +} + +/* + * Is the token hash usable? Mirrors AnserInitialized() in anser.c for the + * state this file owns; AnserChannelLock guards the hash and is resolved in + * AnserShmemInit(). + */ +static bool +AnserAuthInitialized(void) +{ + return gp_anser_enable && AnserChannelLock != NULL && + AnserTokenHash != NULL; +} + +static void +AnserInitializeTokenHash(void) +{ + HASHCTL hctl; + + MemSet(&hctl, 0, sizeof(hctl)); + hctl.keysize = sizeof(AnserTokenTag); + hctl.entrysize = sizeof(AnserTokenEntry); + hctl.hash = tag_hash; + + /* One entry per concurrent session; removed when the session exits. */ + AnserTokenHash = ShmemInitHash(ANSER_TOKEN_HASH_NAME, + MaxConnections, + MaxConnections, + &hctl, + HASH_ELEM | HASH_FUNCTION); +} + +/* + * Drop this session's token entry at backend exit. Registered once by the + * first AnserGetOrCreateSessionToken() call in the backend. + */ +static void +AnserTokenSessionCleanup(int code, Datum arg) +{ + AnserTokenTag tag; + + if (AnserTokenHash == NULL) + return; + + tag.session_id = gp_session_id; + tag.user_id = DatumGetObjectId(arg); + + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + (void) hash_search(AnserTokenHash, &tag, HASH_REMOVE, NULL); + LWLockRelease(AnserChannelLock); +} + +/* + * AnserGetOrCreateSessionToken + * + * Return this session's token (palloc'd hex string), generating and + * registering it on first use. NULL when the subsystem is off, the user id + * is invalid, or the token hash is full -- callers fail open (connect without + * the token, i.e. fall back to pg_hba-driven authentication). + * + * user_id must be the *session* user: segment executors connect back to the + * QD as the session user (cdbconn passes MyProcPort->user_name), regardless + * of any SET ROLE in effect on the QD. + */ +char * +AnserGetOrCreateSessionToken(Oid user_id) +{ + AnserTokenTag tag; + AnserTokenEntry *entry; + bool found; + char token_hex[ANSER_TOKEN_HEX_LEN + 1]; + bool have_token = false; + + if (!AnserAuthInitialized() || !OidIsValid(user_id)) + return NULL; + + tag.session_id = gp_session_id; + tag.user_id = user_id; + + /* Copy the token into a stack buffer: no palloc while holding the lock. */ + LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); + entry = (AnserTokenEntry *) hash_search(AnserTokenHash, &tag, + HASH_ENTER, &found); + if (entry != NULL) + { + if (!found) + { + uint8 token[ANSER_TOKEN_BYTES]; + + if (!pg_strong_random(token, ANSER_TOKEN_BYTES)) + { + (void) hash_search(AnserTokenHash, &tag, HASH_REMOVE, NULL); + entry = NULL; + } + else + { + hex_encode((const char *) token, ANSER_TOKEN_BYTES, + entry->token_hex); + entry->token_hex[ANSER_TOKEN_HEX_LEN] = '\0'; + } + } + if (entry != NULL) + { + strlcpy(token_hex, entry->token_hex, sizeof(token_hex)); + have_token = true; + } + } + LWLockRelease(AnserChannelLock); + + if (!have_token) + return NULL; + + if (!anser_token_exit_registered) + { + anser_token_exit_registered = true; + before_shmem_exit(AnserTokenSessionCleanup, ObjectIdGetDatum(user_id)); + } + + return pstrdup(token_hex); +} + +/* + * AnserSessionTokenIsValid + * + * Token check behind AnserConnCheckPassword(): true iff some live session of + * this exact user registered this token. Runs before InitPostgres in the + * accepting backend; shared-memory pointers are inherited from the postmaster, + * so no attach is needed. + */ +bool +AnserSessionTokenIsValid(Oid user_id, const char *token_hex) +{ + HASH_SEQ_STATUS status; + AnserTokenEntry *entry; + bool valid = false; + + if (!AnserAuthInitialized() || !OidIsValid(user_id) || token_hex == NULL || + strlen(token_hex) != ANSER_TOKEN_HEX_LEN) + return false; + + LWLockAcquire(AnserChannelLock, LW_SHARED); + hash_seq_init(&status, AnserTokenHash); + while ((entry = (AnserTokenEntry *) hash_seq_search(&status)) != NULL) + { + if (entry->tag.user_id == user_id && + strcmp(entry->token_hex, token_hex) == 0) + { + valid = true; + hash_seq_term(&status); + break; + } + } + LWLockRelease(AnserChannelLock); + + return valid; +} + +/* + * CustomAuthClaims_hook: is this an Anser backward (segment -> QD) connection? + * + * The client marks the connection with anser.conn=true in its startup + * packet, either as a command-line option or as a GUC option, so both sources + * are checked -- the same pair of tests the parallel-retrieve-cursor path in + * libpq/auth.c makes for gp_retrieve_conn. + */ +bool +AnserConnClaims(Port *port) +{ + if (port == NULL) + return false; + + return AnserConnMarkedInCmdOptions(port->cmdline_options) || + AnserConnMarkedInGucOptions(port->guc_options); +} + +/* + * CustomAuthCheckPassword_hook: the password of an Anser connection is the + * per-session token the QD handed to the segment in the plan. The connecting + * user must be the session user that registered it. + */ +bool +AnserConnCheckPassword(Port *port, const char *passwd) +{ + Oid owner_uid; + + if (port == NULL || passwd == NULL) + return false; + + owner_uid = get_role_oid(port->user_name, false); + + return AnserSessionTokenIsValid(owner_uid, passwd); +} + +/* + * Return true if the command line contains anser.conn=true. Mirrors + * cmd_options_include_retrieve_conn() in libpq/auth.c. + */ +static bool +AnserConnMarkedInCmdOptions(char *cmd_options) +{ + char **av; + int maxac; + int ac; + int flag; + bool ret = false; + + if (!cmd_options) + return false; + + maxac = 2 + (strlen(cmd_options) + 1) / 2; + + av = (char **) palloc(maxac * sizeof(char *)); + ac = 0; + + av[ac++] = "dummy"; + + pg_split_opts(av, &ac, cmd_options); + + av[ac] = NULL; + +#ifdef HAVE_INT_OPTERR + opterr = 0; +#endif + + while ((flag = getopt(ac, av, "c:-:")) != -1) + { + switch (flag) + { + case 'c': + case '-': + { + char *name, + *value; + + ParseLongOption(optarg, &name, &value); + if (!value) + { + if (flag == '-') + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("--%s requires a value", + optarg))); + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("-c %s requires a value", + optarg))); + } + + if ((guc_name_compare(name, "anser.conn") == 0) && + !parse_bool(value, &ret)) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid value for guc anser.conn: \"%s\"", + value))); + } + + pfree(name); + pfree(value); + break; + } + + default: + break; + } + } + + /* + * Reset getopt(3) library so that it will work correctly in subprocesses + * or when this function is called a second time with another array. + */ + optind = 1; +#ifdef HAVE_INT_OPTRESET + optreset = 1; /* some systems need this too */ +#endif + + return ret; +} + +/* + * Return true if startup GUC options contain anser.conn=true. Mirrors + * guc_options_include_retrieve_conn() in libpq/auth.c. + */ +static bool +AnserConnMarkedInGucOptions(List *guc_options) +{ + ListCell *gucopts; + bool ret = false; + + gucopts = list_head(guc_options); + while (gucopts) + { + char *name; + char *value; + + name = lfirst(gucopts); + gucopts = lnext(guc_options, gucopts); + + value = lfirst(gucopts); + gucopts = lnext(guc_options, gucopts); + + if (guc_name_compare(name, "anser.conn") == 0) + { + /* Do not break in case there are more than one such option. */ + if (!parse_bool(value, &ret)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid value for guc anser.conn: \"%s\"", + value))); + } + } + + return ret; +} diff --git a/gpcontrib/anser/src/anserbloomconsume.c b/gpcontrib/anser/src/anserbloomconsume.c new file mode 100644 index 00000000000..06173f8250e --- /dev/null +++ b/gpcontrib/anser/src/anserbloomconsume.c @@ -0,0 +1,240 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserbloomconsume.c + * Standalone Anser Bloom filter consumer executor helper. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserbloomconsume.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserbloom.h" +#include "anserclient.h" +#include "anserfilter.h" +#include "cdb/cdbvars.h" + +/* + * State for one Bloom filter consumer. Consumes the merged payload for a + * channel exactly once (consumed), either from the coordinator's shared + * memory channel map or over libpq. token authenticates the libpq + * transport on segments and is NULL on the coordinator. cancelled records + * that the producer side aborted instead of delivering the payload. + */ +struct AnserBloomFilterConsumeState +{ + AnserChannelKey channel_key; + bloom_filter *filter; + char *token; /* QD session token for the libpq transport, or NULL */ + int64 total_elems; /* filter sizing, shared with the producer */ + Size max_payload_bytes; + uint64 seed; + uint32 expected_parts; + uint32 received_parts; + bool consumed; + bool cancelled; +}; + +static bool ExecAnserBloomFilterConsumeDirect(AnserBloomFilterConsumeState *state, + long registration_timeout_ms); +static bool ExecAnserBloomFilterConsumeClient(AnserBloomFilterConsumeState *state); + +AnserBloomFilterConsumeState * +ExecInitAnserBloomFilterConsume(const AnserChannelKey *channel_key, + int64 total_elems, Size max_payload_bytes, + uint32 expected_parts, + const char *token) +{ + AnserBloomFilterConsumeState *state; + + if (channel_key == NULL || expected_parts == 0) + return NULL; + + state = palloc0(sizeof(AnserBloomFilterConsumeState)); + state->channel_key = *channel_key; + state->total_elems = total_elems; + state->max_payload_bytes = max_payload_bytes; + state->seed = AnserBloomSeed(channel_key->condition_key); + state->expected_parts = expected_parts; + state->token = (token != NULL && token[0] != '\0') ? pstrdup(token) : NULL; + return state; +} + +bool +ExecAnserBloomFilterConsume(AnserBloomFilterConsumeState *state, + long registration_timeout_ms) +{ + if (state == NULL) + return false; + + if (state->consumed) + return state->filter != NULL; + + /* + * Coordinator-local consumers read the channel map directly; segment + * executors block on the send service over libpq to the QD. The signatures + * are identical -- only the transport differs. + */ + if (Gp_role == GP_ROLE_EXECUTE) + return ExecAnserBloomFilterConsumeClient(state); + + return ExecAnserBloomFilterConsumeDirect(state, registration_timeout_ms); +} + +/* + * Direct shared-memory consume path (coordinator): wait for producer + * registration, wait for READY, then copy the merged payload out of the + * channel map. + */ +static bool +ExecAnserBloomFilterConsumeDirect(AnserBloomFilterConsumeState *state, + long registration_timeout_ms) +{ + void *payload; + Size payload_len = 0; + bool cancelled = false; + bool ready; + + if (!AnserWaitProducersRegistered(&state->channel_key, + registration_timeout_ms)) + { + state->consumed = true; + return false; + } + + if (!AnserWaitReady(&state->channel_key, &cancelled)) + { + state->cancelled = cancelled; + state->consumed = true; + return false; + } + + payload = palloc((Size) gp_anser_max_info_size); + ready = AnserConsumeReady(&state->channel_key, + payload, + (Size) gp_anser_max_info_size, + &payload_len, + &cancelled); + if (!ready || cancelled) + { + pfree(payload); + state->cancelled = cancelled; + state->consumed = true; + return false; + } + + /* + * The coordinator has already unioned every segment's part into one merged + * part (see AnserStorePayloadDSM), so we deserialize a single chunk rather + * than unioning N. The merged header's total_parts records how many parts + * were folded, which we surface as the received count. + */ + { + uint32 part_index = 0; + uint32 folded = 0; + + state->filter = AnserBloomDeserializePart(payload, payload_len, + state->total_elems, + state->max_payload_bytes, + state->seed, + &part_index, &folded); + state->received_parts = (state->filter != NULL) ? folded : 0; + } + pfree(payload); + state->consumed = true; + return state->filter != NULL; +} + +/* + * Network consume path (segment). Blocks in the coordinator backend via libpq + * until the send service delivers the whole payload (or cancels this consumer); + * there is no registration/ready polling here -- the wait is unbounded and + * cancellation is the only backstop. + */ +static bool +ExecAnserBloomFilterConsumeClient(AnserBloomFilterConsumeState *state) +{ + void *payload = NULL; + Size payload_len = 0; + bool cancelled = false; + + if (!AnserClientConsumeWait(&state->channel_key, &payload, &payload_len, + &cancelled, state->token) || cancelled) + { + if (payload != NULL) + pfree(payload); + state->cancelled = cancelled; + state->consumed = true; + return false; + } + + /* + * The coordinator has already unioned every segment's part into one merged + * part (see AnserStorePayloadDSM), so we deserialize a single chunk rather + * than unioning N. The merged header's total_parts records how many parts + * were folded, which we surface as the received count. + */ + { + uint32 part_index = 0; + uint32 folded = 0; + + state->filter = AnserBloomDeserializePart(payload, payload_len, + state->total_elems, + state->max_payload_bytes, + state->seed, + &part_index, &folded); + state->received_parts = (state->filter != NULL) ? folded : 0; + } + if (payload != NULL) + pfree(payload); + state->consumed = true; + return state->filter != NULL; +} + +bloom_filter * +ExecAnserBloomFilterConsumerGetFilter(AnserBloomFilterConsumeState *state) +{ + return state != NULL ? state->filter : NULL; +} + +uint32 +ExecAnserBloomFilterConsumerReceivedParts(AnserBloomFilterConsumeState *state) +{ + return state != NULL ? state->received_parts : 0; +} + +bool +ExecAnserBloomFilterConsumerWasCancelled(AnserBloomFilterConsumeState *state) +{ + return state != NULL && state->cancelled; +} + +void +ExecEndAnserBloomFilterConsume(AnserBloomFilterConsumeState *state) +{ + if (state == NULL) + return; + + if (state->filter != NULL) + bloom_free(state->filter); + pfree(state); +} diff --git a/gpcontrib/anser/src/anserbloomproduce.c b/gpcontrib/anser/src/anserbloomproduce.c new file mode 100644 index 00000000000..04bc7b44e0c --- /dev/null +++ b/gpcontrib/anser/src/anserbloomproduce.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserbloomproduce.c + * Standalone Anser Bloom filter producer executor helper. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserbloomproduce.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserbloom.h" +#include "anserclient.h" +#include "anserfilter.h" +#include "cdb/cdbvars.h" + +/* + * State for a single bloom filter producer: the target channel, the filter + * being built, this producer's identity within total_parts, and the QD + * session token used by segments to publish over libpq. published and + * cancelled guard against double publication and drive teardown. + */ +struct AnserBloomFilterProduceState +{ + AnserChannelKey channel_key; + bloom_filter *filter; + char *token; /* QD session token for the libpq transport, or NULL */ + uint32 part_index; + uint32 total_parts; + bool published; + bool cancelled; +}; + +/* + * Publish one part, choosing the transport by role: coordinator-local callers + * touch the channel map directly (no self-connection), while segment executors + * go over libpq to the QD. total_parts doubles as expected_producers: each + * producer contributes exactly one part. + */ +static bool +AnserProducePublishPart(AnserBloomFilterProduceState *state, + const void *payload, Size payload_len, bool cancelled) +{ + if (Gp_role == GP_ROLE_EXECUTE) + return AnserClientPublish(&state->channel_key, state->total_parts, + payload, payload_len, cancelled, state->token); + + return AnserPublish(&state->channel_key, payload, payload_len, cancelled); +} + +AnserBloomFilterProduceState * +ExecInitAnserBloomFilterProduce(const AnserChannelKey *channel_key, + int64 total_elems, + Size max_payload_bytes, + uint32 part_index, + uint32 total_parts, + const char *token) +{ + AnserBloomFilterProduceState *state; + uint64 seed; + + if (channel_key == NULL || total_parts == 0 || part_index >= total_parts) + return NULL; + + state = palloc0(sizeof(AnserBloomFilterProduceState)); + state->channel_key = *channel_key; + state->part_index = part_index; + state->total_parts = total_parts; + state->token = (token != NULL && token[0] != '\0') ? pstrdup(token) : NULL; + seed = AnserBloomSeed(channel_key->condition_key); + state->filter = AnserBloomCreate(total_elems, max_payload_bytes, seed); + + return state; +} + +void +ExecAnserBloomFilterProduceAddDatum(AnserBloomFilterProduceState *state, + Datum value, bool isnull) +{ + if (state == NULL || state->published || isnull) + return; + + bloom_add_element(state->filter, (unsigned char *) &value, sizeof(Datum)); +} + +bool +ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state) +{ + Size payload_size; + Size payload_len = 0; + void *payload; + bool ok; + + if (state == NULL || state->published) + return false; + + if (state->cancelled) + { + state->published = true; + return AnserProducePublishPart(state, NULL, 0, true); + } + + /* + * Serialize as a self-contained single part (index 0 of 1). The coordinator + * stores the first part verbatim and OR-folds each later part, bumping the + * merged header's fold count, so the final count reflects how many parts were + * unioned. state->part_index / state->total_parts identify this producer to + * the channel (expected_producers), not the on-wire part layout. + */ + payload_size = AnserBloomSerializedSize(state->filter); + payload = palloc(payload_size); + ok = AnserBloomSerializePart(state->filter, + 0, + 1, + payload, + payload_size, + &payload_len); + if (ok) + ok = AnserProducePublishPart(state, payload, payload_len, false); + + pfree(payload); + state->published = true; + return ok; +} + +bool +ExecAnserBloomFilterProduceCancel(AnserBloomFilterProduceState *state) +{ + if (state == NULL || state->published) + return false; + + state->cancelled = true; + state->published = true; + return AnserProducePublishPart(state, NULL, 0, true); +} + +void +ExecEndAnserBloomFilterProduce(AnserBloomFilterProduceState *state) +{ + if (state == NULL) + return; + + if (!state->published) + (void) ExecAnserBloomFilterProduceCancel(state); + + if (state->filter != NULL) + bloom_free(state->filter); + pfree(state); +} diff --git a/gpcontrib/anser/src/anserclient.c b/gpcontrib/anser/src/anserclient.c new file mode 100644 index 00000000000..0b33cd4ec03 --- /dev/null +++ b/gpcontrib/anser/src/anserclient.c @@ -0,0 +1,453 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserclient.c + * libpq client helpers for the Anser network transport. + * + * A remote producer/consumer (running on a segment, Gp_role == GP_ROLE_EXECUTE) + * cannot touch the coordinator-resident channel map directly. Instead it opens + * an ordinary libpq connection to the QD -- discovered from gp_qd_hostname / + * gp_qd_port, which the dispatcher injects into every QE -- and calls the + * anser.* SQL functions. When the QD supplied a session token (carried + * in the plan), the connection authenticates with it via the anser.conn + * startup marker, bypassing pg_hba (the parallel-retrieve-cursor model); + * otherwise authentication falls back to pg_hba. Encryption and connection + * lifecycle are inherited from libpq; these helpers are the client edges only. + * + * Everything here is fail-open: a broken connection degrades to unfiltered + * execution, never to a wrong result or an error propagated into the query. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserclient.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq-fe.h" + +#include "anser.h" +#include "anserclient.h" +#include "cdb/cdbvars.h" +#include "commands/dbcommands.h" +#include "libpq/libpq-be.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/latch.h" +#include "utils/wait_event.h" + +/* Enough for the decimal form of any int32 argument. */ +#define ANSER_INT_STRLEN 12 + +static PGconn *anser_client_connect(const char *token); +static int anser_client_exec_bool(PGconn *conn, const char *sql, int nparams, + const char *const *values, const int *lengths, + const int *formats); +static int anser_client_producer_begin(PGconn *conn, + const AnserChannelKey *key, + uint32 expected_producers); +static int anser_client_publish_part(PGconn *conn, + const AnserChannelKey *key, + const void *payload, Size payload_len, + bool cancelled); +static PGresult *anser_client_wait_result(PGconn *conn, const char *sql, + int nparams, + const char *const *values, + const int *lengths, + const int *formats, + int result_format); + +/* + * Open a libpq connection to the QD postmaster, reusing the query's database and + * user. Returns NULL (never raises) on any failure so callers can fail open. + * + * When a session token is given, the connection carries the anser.conn=true + * startup marker and the token as password, which the QD authenticates against + * its session-token hash before pg_hba is consulted (see + * AnserConnCheckPassword, installed as the core custom-auth hook) -- so no + * pg_hba entry for the + * segment hosts is needed. Without a token the connection goes through + * ordinary pg_hba-driven authentication. + */ +static PGconn * +anser_client_connect(const char *token) +{ + const char *keywords[10]; + const char *values[10]; + int n = 0; + char portstr[ANSER_INT_STRLEN]; + const char *dbname; + const char *user; + PGconn *conn; + + if (qdHostname == NULL || qdHostname[0] == '\0' || qdPostmasterPort <= 0) + return NULL; + + snprintf(portstr, sizeof(portstr), "%d", qdPostmasterPort); + + if (MyProcPort != NULL && MyProcPort->database_name != NULL) + dbname = MyProcPort->database_name; + else if (OidIsValid(MyDatabaseId)) + dbname = get_database_name(MyDatabaseId); + else + dbname = NULL; + + if (MyProcPort != NULL && MyProcPort->user_name != NULL) + user = MyProcPort->user_name; + else + user = GetUserNameFromId(GetUserId(), true); + + if (dbname == NULL || user == NULL) + return NULL; + + keywords[n] = "host"; + values[n] = qdHostname; + n++; + keywords[n] = "port"; + values[n] = portstr; + n++; + keywords[n] = "dbname"; + values[n] = dbname; + n++; + keywords[n] = "user"; + values[n] = user; + n++; + keywords[n] = "client_encoding"; + values[n] = GetDatabaseEncodingName(); + n++; + keywords[n] = "connect_timeout"; + values[n] = "10"; + n++; + if (token != NULL && token[0] != '\0') + { + keywords[n] = "password"; + values[n] = token; + n++; + keywords[n] = "options"; + values[n] = "-c anser.conn=true"; + n++; + } + keywords[n] = "application_name"; + values[n] = "anser_rf"; + n++; + keywords[n] = NULL; + values[n] = NULL; + + conn = PQconnectdbParams(keywords, values, false); + if (conn == NULL) + return NULL; + if (PQstatus(conn) != CONNECTION_OK) + { + PQfinish(conn); + return NULL; + } + + return conn; +} + +/* + * Run a bool-returning anser.* function. Returns 1 (true), 0 (false), or -1 + * on any protocol error. + */ +static int +anser_client_exec_bool(PGconn *conn, const char *sql, int nparams, + const char *const *values, const int *lengths, + const int *formats) +{ + PGresult *res; + int ret; + + res = PQexecParams(conn, sql, nparams, NULL, values, lengths, formats, 0); + if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK || + PQntuples(res) != 1 || PQnfields(res) != 1) + { + if (res != NULL) + PQclear(res); + return -1; + } + + if (PQgetisnull(res, 0, 0)) + ret = 0; + else + ret = (strcmp(PQgetvalue(res, 0, 0), "t") == 0) ? 1 : 0; + + PQclear(res); + return ret; +} + +static int +anser_client_producer_begin(PGconn *conn, const AnserChannelKey *key, + uint32 expected_producers) +{ + const char *values[5]; + char ssid[ANSER_INT_STRLEN]; + char ccnt[ANSER_INT_STRLEN]; + char condid[ANSER_INT_STRLEN]; + char expected[ANSER_INT_STRLEN]; + + snprintf(ssid, sizeof(ssid), "%d", key->gp_session_id); + snprintf(ccnt, sizeof(ccnt), "%d", key->gp_command_count); + snprintf(condid, sizeof(condid), "%d", (int) key->condition_id); + snprintf(expected, sizeof(expected), "%u", expected_producers); + + values[0] = ssid; + values[1] = ccnt; + values[2] = condid; + values[3] = key->condition_key; + values[4] = expected; + + return anser_client_exec_bool(conn, + "SELECT anser.producer_begin($1::int4, $2::int4, $3::int4, $4::text, $5::int4)", + 5, values, NULL, NULL); +} + +static int +anser_client_publish_part(PGconn *conn, const AnserChannelKey *key, + const void *payload, Size payload_len, bool cancelled) +{ + const char *values[6]; + int lengths[6]; + int formats[6]; + char ssid[ANSER_INT_STRLEN]; + char ccnt[ANSER_INT_STRLEN]; + char condid[ANSER_INT_STRLEN]; + + snprintf(ssid, sizeof(ssid), "%d", key->gp_session_id); + snprintf(ccnt, sizeof(ccnt), "%d", key->gp_command_count); + snprintf(condid, sizeof(condid), "%d", (int) key->condition_id); + + memset(lengths, 0, sizeof(lengths)); + memset(formats, 0, sizeof(formats)); + + values[0] = ssid; + values[1] = ccnt; + values[2] = condid; + values[3] = key->condition_key; + + /* $5 payload: raw bytea in binary format (empty when cancelling). */ + if (!cancelled && payload != NULL && payload_len > 0) + { + values[4] = (const char *) payload; + lengths[4] = (int) payload_len; + } + else + { + values[4] = ""; + lengths[4] = 0; + } + formats[4] = 1; + + values[5] = cancelled ? "t" : "f"; + + return anser_client_exec_bool(conn, + "SELECT anser.publish($1::int4, $2::int4, $3::int4, $4::text, $5::bytea, $6::bool)", + 6, values, lengths, formats); +} + +bool +AnserClientPublish(const AnserChannelKey *channel_key, + uint32 expected_producers, const void *payload, + Size payload_len, bool cancelled, const char *token) +{ + PGconn *conn; + bool ok = false; + + if (channel_key == NULL) + return false; + + conn = anser_client_connect(token); + if (conn == NULL) + return false; /* fail open */ + + if (anser_client_producer_begin(conn, channel_key, expected_producers) == 1 && + anser_client_publish_part(conn, channel_key, payload, payload_len, + cancelled) == 1) + ok = true; + else if (!cancelled) + { + /* + * Something went wrong mid-publish. Best-effort cancel so the dataset + * dies cleanly rather than leaving consumers to time out. + */ + (void) anser_client_publish_part(conn, channel_key, NULL, 0, true); + } + + PQfinish(conn); + return ok; +} + +/* + * Issue a query and block interruptibly for its result. Unlike PQexecParams, + * this pumps the connection through WaitLatchOrSocket so the calling backend + * still honors query cancellation while the coordinator holds the consumer. On + * interrupt we forward a cancel to the QD backend and re-raise, so the blocked + * anser.consume_wait there unwinds and its wait slot is reaped. + */ +static PGresult * +anser_client_wait_result(PGconn *conn, const char *sql, int nparams, + const char *const *values, const int *lengths, + const int *formats, int result_format) +{ + PGresult *res = NULL; + PGresult *tmp; + bool failed = false; + + if (!PQsendQueryParams(conn, sql, nparams, NULL, values, lengths, formats, + result_format)) + return NULL; + + /* Never "return" from inside PG_TRY: flag failures and handle them after. */ + PG_TRY(); + { + for (;;) + { + CHECK_FOR_INTERRUPTS(); + + if (!PQconsumeInput(conn)) + { + failed = true; + break; + } + + if (!PQisBusy(conn)) + break; + + (void) WaitLatchOrSocket(MyLatch, + WL_LATCH_SET | WL_SOCKET_READABLE | + WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + PQsocket(conn), 1000L, + PG_WAIT_EXTENSION); + ResetLatch(MyLatch); + } + } + PG_CATCH(); + { + PGcancel *cancel = PQgetCancel(conn); + + if (cancel != NULL) + { + char errbuf[256]; + + (void) PQcancel(cancel, errbuf, sizeof(errbuf)); + PQfreeCancel(cancel); + } + PG_RE_THROW(); + } + PG_END_TRY(); + + if (failed) + return NULL; + + res = PQgetResult(conn); + /* Drain any trailing results so the connection is reusable/closable. */ + while ((tmp = PQgetResult(conn)) != NULL) + PQclear(tmp); + + return res; +} + +bool +AnserClientConsumeWait(const AnserChannelKey *channel_key, void **payload, + Size *payload_len, bool *cancelled, const char *token) +{ + PGconn *conn; + PGresult *res; + const char *values[4]; + char ssid[ANSER_INT_STRLEN]; + char ccnt[ANSER_INT_STRLEN]; + char condid[ANSER_INT_STRLEN]; + bool ok = false; + + if (payload != NULL) + *payload = NULL; + if (payload_len != NULL) + *payload_len = 0; + if (cancelled != NULL) + *cancelled = false; + + if (channel_key == NULL) + return false; + + conn = anser_client_connect(token); + if (conn == NULL) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + snprintf(ssid, sizeof(ssid), "%d", channel_key->gp_session_id); + snprintf(ccnt, sizeof(ccnt), "%d", channel_key->gp_command_count); + snprintf(condid, sizeof(condid), "%d", (int) channel_key->condition_id); + values[0] = ssid; + values[1] = ccnt; + values[2] = condid; + values[3] = channel_key->condition_key; + + PG_TRY(); + { + res = anser_client_wait_result(conn, + "SELECT anser.consume_wait($1::int4, $2::int4, $3::int4, $4::text)", + 4, values, NULL, NULL, 1); + } + PG_CATCH(); + { + PQfinish(conn); + PG_RE_THROW(); + } + PG_END_TRY(); + + if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK || + PQntuples(res) != 1 || PQnfields(res) != 1) + { + if (res != NULL) + PQclear(res); + PQfinish(conn); + if (cancelled != NULL) + *cancelled = true; + return false; + } + + if (PQgetisnull(res, 0, 0)) + { + if (cancelled != NULL) + *cancelled = true; + } + else + { + int len = PQgetlength(res, 0, 0); + char *val = PQgetvalue(res, 0, 0); + void *buf = NULL; + + if (len > 0) + { + buf = palloc(len); + memcpy(buf, val, len); + } + if (payload != NULL) + *payload = buf; + if (payload_len != NULL) + *payload_len = (Size) len; + ok = true; + } + + PQclear(res); + PQfinish(conn); + return ok; +} diff --git a/gpcontrib/anser/src/anserfilter.c b/gpcontrib/anser/src/anserfilter.c new file mode 100644 index 00000000000..4ea09952aaf --- /dev/null +++ b/gpcontrib/anser/src/anserfilter.c @@ -0,0 +1,285 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserfilter.c + * Bloom-filter payload helpers for Anser channels. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserfilter.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anserfilter.h" +#include "common/hashfn.h" +#include "port/pg_bitutils.h" + +static bool AnserBloomValidateHeader(const AnserBloomPartHeader *header, + Size payload_len); + +/* + * Map Anser's payload budget to bloom_create's work_mem (KB): the space left for + * the bitset after the part header. Callers must have checked that + * max_payload_bytes leaves room for a header. + */ +static int +AnserBloomWorkMemKb(Size max_payload_bytes) +{ + return (int) ((max_payload_bytes - sizeof(AnserBloomPartHeader)) / 1024); +} + +uint64 +AnserBloomSeed(const char *condition_key) +{ + if (condition_key == NULL) + return 0; + + return hash_bytes_extended((const unsigned char *) condition_key, + strlen(condition_key), 0); +} + +/* + * Build an empty bloom filter for a channel from the caller's parameters. + * + * The producer builds its filter here; the consumer rebuilds an identical one + * from the received bitset (AnserBloomDeserializePart -> bloom_create_from_bitset) + * using the SAME (total_elems, max_payload_bytes, seed) -- carried in the plan + * node's custom_private and derived from the shared condition key -- so every + * segment and the consumer realize a byte-for-byte identical filter shape. This + * is why the serialized part header does not need to carry the bitset parameters: + * the reconstructing side already knows them. + * + * We defer sizing to the standard bloom_create, which targets ~2 bytes per + * element, rounds the bitset down to a power of two, and floors it at 1 MB. + * max_payload_bytes bounds the bitset from above (minus header room), expressed + * as bloom_create's work_mem budget in KB. + */ +bloom_filter * +AnserBloomCreate(int64 total_elems, Size max_payload_bytes, uint64 seed) +{ + /* Internal callers always size the payload to hold a header + bitset. */ + Assert(max_payload_bytes > sizeof(AnserBloomPartHeader)); + + return bloom_create(total_elems, AnserBloomWorkMemKb(max_payload_bytes), seed); +} + +Size +AnserBloomSerializedSize(const bloom_filter *filter) +{ + if (filter == NULL) + return 0; + + return sizeof(AnserBloomPartHeader) + bloom_bitset_bytes(filter); +} + +/* + * Does this payload look like a serialized bloom part? Used by the in-place fold + * to confirm both the accumulator and the incoming payload are well-formed parts + * before OR-ing their bitsets. A false positive is effectively impossible: a + * part must carry the ABF1 magic, a known version, and sane part counts. + */ +bool +AnserBloomLooksLikePart(const void *payload, Size payload_len) +{ + if (payload == NULL || payload_len < sizeof(AnserBloomPartHeader)) + return false; + + return AnserBloomValidateHeader((const AnserBloomPartHeader *) payload, + payload_len); +} + +/* + * Fold an incoming part into an accumulator part IN PLACE. + * + * When a merged part and the incoming part are the same serialized size (they + * share bitset params derived from the condition key), the union is a pure + * bitwise OR of the two bitsets plus a bump of the merged header's fold count -- + * no reallocation. This mutates `acc` directly, so the caller must hold whatever + * lock guards the buffer (AnserChannelLock, for the channel payload). + * + * Returns true only when the in-place union applied. It returns false -- and + * leaves `acc` untouched (all checks run before any write) -- when the union + * cannot be done by raw OR: sizes differ, either side is not a valid part, or + * the filter parameters disagree. Since every part on a channel shares the same + * (condition-key-derived) parameters and therefore the same serialized size, the + * first part is stored verbatim and every later part folds in here; a false + * return means a malformed/mismatched payload and the caller cancels the channel. + */ +bool +AnserBloomFoldPartInPlace(void *acc, Size acc_len, + const void *part, Size part_len) +{ + AnserBloomPartHeader *ah; + unsigned char *abits; + const unsigned char *pbits; + Size bitset_bytes; + Size i; + + /* Only a same-sized, valid part-vs-part union can be done by raw OR. */ + if (acc_len != part_len || + !AnserBloomLooksLikePart(acc, acc_len) || + !AnserBloomLooksLikePart(part, part_len)) + return false; + + ah = (AnserBloomPartHeader *) acc; + + /* + * Equal serialized size is sufficient: every part on a channel is built by + * bloom_create from the same (condition-key-derived) parameters, so equal + * length implies an identical bitset shape. The bitset is whatever follows + * the header, so its length is the payload length minus the header. + */ + bitset_bytes = acc_len - sizeof(AnserBloomPartHeader); + abits = (unsigned char *) acc + sizeof(AnserBloomPartHeader); + pbits = (const unsigned char *) part + sizeof(AnserBloomPartHeader); + + for (i = 0; i < bitset_bytes; i++) + abits[i] |= pbits[i]; + + /* One more segment part folded into the running merged part. */ + ah->total_parts += 1; + + return true; +} + +/* + * Serialize one filter as a wire part: an AnserBloomPartHeader followed by the + * raw bitset. Fails (returns false) on bogus arguments or a too-small buffer. + */ +bool +AnserBloomSerializePart(const bloom_filter *filter, uint32 part_index, + uint32 total_parts, void *buffer, Size buffer_size, + Size *payload_len) +{ + AnserBloomPartHeader header; + Size bitset_bytes; + Size total_len; + + if (payload_len != NULL) + *payload_len = 0; + + if (filter == NULL || buffer == NULL || total_parts == 0 || + part_index >= total_parts) + return false; + + bitset_bytes = bloom_bitset_bytes(filter); + total_len = sizeof(AnserBloomPartHeader) + bitset_bytes; + if (buffer_size < total_len) + return false; + + MemSet(&header, 0, sizeof(header)); + header.magic = ANSER_BLOOM_PART_MAGIC; + header.version = ANSER_BLOOM_PART_VERSION; + header.part_index = part_index; + header.total_parts = total_parts; + + memcpy(buffer, &header, sizeof(header)); + memcpy((char *) buffer + sizeof(header), bloom_bitset_data(filter), + bitset_bytes); + + if (payload_len != NULL) + *payload_len = total_len; + return true; +} + +/* + * Rebuild the filter from a received merged part. + * + * The bitset parameters are NOT taken from the wire header: the caller passes + * the same (total_elems, max_payload_bytes, seed) used to produce the filter -- + * it holds them in the plan node, so both ends agree by construction. We rebuild + * the empty filter from those, then load the received bitset into it. The wire + * header is still validated (magic/version/counts) and, crucially, the received + * bitset length must exactly match the size the local parameters imply; any + * mismatch (version/parameter skew, truncation) returns NULL so the consumer + * fails open rather than loading a wrongly-shaped bitset. part_index/total_parts + * are surfaced from the header for diagnostics. + */ +bloom_filter * +AnserBloomDeserializePart(const void *payload, Size payload_len, + int64 total_elems, Size max_payload_bytes, uint64 seed, + uint32 *part_index, uint32 *total_parts) +{ + const AnserBloomPartHeader *header; + bloom_filter *filter; + + if (part_index != NULL) + *part_index = 0; + if (total_parts != NULL) + *total_parts = 0; + + if (payload == NULL || payload_len < sizeof(AnserBloomPartHeader)) + return NULL; + + header = (const AnserBloomPartHeader *) payload; + if (!AnserBloomValidateHeader(header, payload_len)) + return NULL; + + if (max_payload_bytes <= sizeof(AnserBloomPartHeader)) + return NULL; + if (total_elems < 1) + total_elems = 1; + + /* + * Build the filter straight from the received bitset, sized by our own + * parameters. bloom_create_from_bitset returns NULL unless the received + * length is exactly the size those parameters imply, so the fail-open + * described above happens here. A filter is thus only ever populated at + * construction and, from then on, only grown by add/union -- never re-set. + */ + filter = bloom_create_from_bitset(total_elems, + AnserBloomWorkMemKb(max_payload_bytes), + seed, + (const unsigned char *) payload + + sizeof(AnserBloomPartHeader), + payload_len - sizeof(AnserBloomPartHeader)); + if (filter == NULL) + return NULL; + + if (part_index != NULL) + *part_index = header->part_index; + if (total_parts != NULL) + *total_parts = header->total_parts; + return filter; +} + +/* + * Validate the wire framing of a part header. The bitset parameters are not + * carried on the wire (both ends rebuild the filter from the shared plan + * parameters), so this only checks the framing: magic/version, a sane fold + * count, and that the payload carries a header plus at least some bitset. The + * authoritative size check -- that the received bitset matches the size the local + * parameters imply -- is done in AnserBloomDeserializePart. + */ +static bool +AnserBloomValidateHeader(const AnserBloomPartHeader *header, Size payload_len) +{ + if (header == NULL) + return false; + + if (header->magic != ANSER_BLOOM_PART_MAGIC || + header->version != ANSER_BLOOM_PART_VERSION) + return false; + + if (header->total_parts == 0 || header->part_index >= header->total_parts) + return false; + + return payload_len > sizeof(AnserBloomPartHeader); +} diff --git a/gpcontrib/anser/src/anserfuncs.c b/gpcontrib/anser/src/anserfuncs.c new file mode 100644 index 00000000000..2f9b3a4f2f2 --- /dev/null +++ b/gpcontrib/anser/src/anserfuncs.c @@ -0,0 +1,200 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserfuncs.c + * SQL functions that expose the Anser network transport. + * + * These are the thin coordinator-side edges of the Anser data path. Remote + * (segment) producers and consumers reach the coordinator-resident channel map + * by opening an ordinary libpq connection to the QD and calling these functions + * (created by anser--1.0.sql in schema "anser"); all real work happens in the + * gather and send background services. A producer announces itself with + * anser.producer_begin(), streams parts with anser.publish(), and a consumer + * blocks in anser.consume_wait() until the send service delivers its payload + * (or cancels it). + * + * Access control: these functions are intentionally left with the default + * EXECUTE grant to PUBLIC. The network transport connects to the QD as the + * *query's own* role, so restricting them to superusers -- or revoking them + * from PUBLIC -- would silently disable runtime filtering for every + * non-superuser query (it would fail open to unfiltered execution). + * + * The (session id, command count, condition) key cannot be derived server-side: + * each call runs in a fresh coordinator backend the segment opened over + * libpq, with its own session -- not the originating query's -- so the key must + * travel in the call. Because it is caller-supplied, we bind every channel to + * the authenticated role that created it (see AnserChannelEntry.creator_role): + * a caller may only produce/consume on a channel its own role created, unless it + * is a superuser. That blocks the dangerous vector -- one role poisoning + * another role's bloom filter, which could drop matching rows -- and leaves only + * same-role/cross-command self-interference, which fails open, never to wrong + * results. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserfuncs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "varatt.h" + +PG_FUNCTION_INFO_V1(anser_producer_begin); +PG_FUNCTION_INFO_V1(anser_publish); +PG_FUNCTION_INFO_V1(anser_consume_wait); + +static bool anser_build_key(int32 gp_session_id, int32 gp_command_count, + int32 condition_id, text *condition_key_text, + AnserChannelKey *key); + +/* + * anser_producer_begin(ssid, ccnt, cond_id, cond_key, expected_producers) + * + * Register (idempotently) the channel and arm its produce deadline. This is the + * "a producer opened a connection" signal; if the channel does not become READY + * within anser.timeout_ms the gather service cancels the whole dataset. + */ +Datum +anser_producer_begin(PG_FUNCTION_ARGS) +{ + int32 gp_session_id = PG_GETARG_INT32(0); + int32 gp_command_count = PG_GETARG_INT32(1); + int32 condition_id = PG_GETARG_INT32(2); + text *condition_key = PG_GETARG_TEXT_PP(3); + int32 expected_producers = PG_GETARG_INT32(4); + AnserChannelKey key; + + if (expected_producers <= 0) + PG_RETURN_BOOL(false); + + if (!anser_build_key(gp_session_id, gp_command_count, condition_id, + condition_key, &key)) + PG_RETURN_BOOL(false); + + PG_RETURN_BOOL(AnserProducerBegin(&key, expected_producers, + GetUserId(), superuser())); +} + +/* + * anser_publish(ssid, ccnt, cond_id, cond_key, payload, cancelled) + * + * Hand one part to the gather service and block for its ACK. expected_producers + * is not repeated here: anser.producer_begin already stamped it on the + * channel, so we pass 0 to leave it unchanged. + */ +Datum +anser_publish(PG_FUNCTION_ARGS) +{ + int32 gp_session_id = PG_GETARG_INT32(0); + int32 gp_command_count = PG_GETARG_INT32(1); + int32 condition_id = PG_GETARG_INT32(2); + text *condition_key = PG_GETARG_TEXT_PP(3); + bytea *payload = PG_GETARG_BYTEA_PP(4); + bool cancelled = PG_GETARG_BOOL(5); + AnserChannelKey key; + + if (!anser_build_key(gp_session_id, gp_command_count, condition_id, + condition_key, &key)) + PG_RETURN_BOOL(false); + + PG_RETURN_BOOL(AnserProducerSubmit(&key, 0, + VARDATA_ANY(payload), + VARSIZE_ANY_EXHDR(payload), + cancelled, + GetUserId(), superuser())); +} + +/* + * anser_consume_wait(ssid, ccnt, cond_id, cond_key) -> bytea + * + * Subscribe, register a wait slot, and block on the proc latch until the send + * service delivers the payload or cancels this consumer. Returns the payload + * bytes on delivery, or NULL when the channel is cancelled/unreachable. Blocks + * the calling coordinator backend for the query's lifetime, per the "consumer + * waits, does not process further" semantics. + */ +Datum +anser_consume_wait(PG_FUNCTION_ARGS) +{ + int32 gp_session_id = PG_GETARG_INT32(0); + int32 gp_command_count = PG_GETARG_INT32(1); + int32 condition_id = PG_GETARG_INT32(2); + text *condition_key = PG_GETARG_TEXT_PP(3); + AnserChannelKey key; + void *payload = NULL; + Size payload_len = 0; + bool cancelled = false; + bytea *result; + + if (!anser_build_key(gp_session_id, gp_command_count, condition_id, + condition_key, &key)) + PG_RETURN_NULL(); + + if (!AnserConsumerWait(&key, &payload, &payload_len, &cancelled, + GetUserId(), superuser()) || + cancelled) + { + if (payload != NULL) + pfree(payload); + PG_RETURN_NULL(); + } + + result = (bytea *) palloc(VARHDRSZ + payload_len); + SET_VARSIZE(result, VARHDRSZ + payload_len); + if (payload_len > 0) + memcpy(VARDATA(result), payload, payload_len); + if (payload != NULL) + pfree(payload); + + PG_RETURN_BYTEA_P(result); +} + +/* + * anser_build_key(ssid, ccnt, cond_id, cond_key, key) + * + * Validate the caller-supplied channel key components and copy them into + * *key. Returns false (fail open) when a component is out of range. + */ +static bool +anser_build_key(int32 gp_session_id, int32 gp_command_count, + int32 condition_id, text *condition_key_text, + AnserChannelKey *key) +{ + char *condition_key = text_to_cstring(condition_key_text); + bool ok = true; + + if (condition_id < 0 || strlen(condition_key) >= ANSER_CONDITION_KEY_SIZE) + ok = false; + else + { + MemSet(key, 0, sizeof(AnserChannelKey)); + key->gp_session_id = gp_session_id; + key->gp_command_count = gp_command_count; + key->condition_id = (uint32) condition_id; + strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); + } + + pfree(condition_key); + return ok; +} diff --git a/gpcontrib/anser/src/anserinit.c b/gpcontrib/anser/src/anserinit.c new file mode 100644 index 00000000000..572e970f639 --- /dev/null +++ b/gpcontrib/anser/src/anserinit.c @@ -0,0 +1,282 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserinit.c + * Module entry point: GUCs, shared memory, background services, and the + * core hooks the Anser subsystem hangs off. + * + * Anser is a shared_preload_libraries extension. Everything it needs from the + * server is reached through an existing extensibility point: + * + * shmem_request_hook / shmem_startup_hook the channel map and its LWLocks + * RegisterBackgroundWorker the gather and send services + * planner_hook runtime-filter injection + * RegisterCustomScanMethods the injected plan nodes + * CustomAuth*_hook segment -> QD token connections + * + * IDENTIFICATION + * gpcontrib/anser/src/anserinit.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserplan.h" +#include "cdb/cdbvars.h" +#include "libpq/auth.h" +#include "miscadmin.h" +#include "optimizer/planner.h" +#include "postmaster/bgworker.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +static void anser_define_gucs(void); +static void anser_register_services(void); +static void anser_shmem_request(void); +static void anser_shmem_startup(void); +static PlannedStmt *anser_planner(Query *parse, const char *query_string, + int cursorOptions, ParamListInfo boundParams, + OptimizerOptions *optimizer_options); + +static shmem_request_hook_type prev_shmem_request_hook = NULL; +static shmem_startup_hook_type prev_shmem_startup_hook = NULL; +static planner_hook_type prev_planner_hook = NULL; + +void +_PG_init(void) +{ + anser_define_gucs(); + + /* + * Only a preloaded library can request shared memory, register background + * workers, or be relied on to have installed its hooks in every backend. + * Loaded any other way, Anser stays inert: the GUCs exist (so a stray + * setting is not an error) but nothing is wired up. + */ + if (!process_shared_preload_libraries_in_progress) + return; + + prev_shmem_request_hook = shmem_request_hook; + shmem_request_hook = anser_shmem_request; + prev_shmem_startup_hook = shmem_startup_hook; + shmem_startup_hook = anser_shmem_startup; + + prev_planner_hook = planner_hook; + planner_hook = anser_planner; + + /* + * The producer and consumer nodes travel to the segments inside dispatched + * plans, so every backend must be able to resolve their CustomScan methods + * by name. Registering here covers QD and QE alike. + */ + AnserRegisterRuntimeFilterMethods(); + + if (gp_anser_enable) + { + /* + * Own the authentication of incoming segment -> QD connections. With + * Anser disabled the hooks stay unset and such a connection is simply + * authenticated the ordinary way, through pg_hba. + */ + CustomAuthClaims_hook = AnserConnClaims; + CustomAuthCheckPassword_hook = AnserConnCheckPassword; + + anser_register_services(); + } +} + +/* + * The subsystem's GUCs. All are "anser.*"-qualified because they belong to a + * loadable module; the C variables keep their gp_anser_ names. + */ +static void +anser_define_gucs(void) +{ + DefineCustomBoolVariable("anser.enable", + "Enables the Anser adaptive information sharing subsystem.", + "When disabled, Anser does not allocate shared memory and its background services are not started.", + &gp_anser_enable, + false, + PGC_POSTMASTER, + 0, + NULL, NULL, NULL); + + DefineCustomBoolVariable("anser.runtime_filter", + "Enables injection of Anser runtime bloom filters into plans.", + "Requires anser.enable; the plan pass is a no-op otherwise.", + &gp_anser_runtime_filter, + false, + PGC_USERSET, + GUC_EXPLAIN, + NULL, NULL, NULL); + + DefineCustomBoolVariable("anser.conn", + "Specify this is a connection for the Anser runtime filter transport.", + NULL, + &gp_anser_conn, + false, + PGC_BACKEND, + GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_DISALLOW_IN_FILE, + NULL, NULL, NULL); + + DefineCustomIntVariable("anser.max_channels", + "Sets the maximum number of Anser channels.", + "This value sizes the fixed Anser shared-memory channel map at postmaster start. " + "0 (the default) auto-sizes it to max_connections * gp_max_slices, " + "falling back to a fixed per-connection budget when gp_max_slices is unbounded.", + &gp_anser_max_channels, + 0, 0, INT_MAX, + PGC_POSTMASTER, + 0, + NULL, NULL, NULL); + + DefineCustomIntVariable("anser.max_info_size", + "Sets the maximum byte size of one Anser information record.", + "Per-record DSM payload cap for Anser information. The default holds a full 64 MB bloom-filter bitset plus its serialized-part header.", + &gp_anser_max_info_size, + 64 * 1024 * 1024 + 1024 * 1024, 1, INT_MAX, + PGC_POSTMASTER, + 0, + NULL, NULL, NULL); + + DefineCustomIntVariable("anser.timeout_ms", + "Sets how long Anser consumers wait for producer registration.", + "After producer registration, consumers wait for data without this timeout and rely on query cancellation or channel cancellation.", + &gp_anser_timeout_ms, + 1000, 0, INT_MAX, + PGC_USERSET, + GUC_UNIT_MS, + NULL, NULL, NULL); + + DefineCustomIntVariable("anser.max_consumers_per_channel", + "Sets the maximum number of waiting Anser consumers per channel.", + "This value sizes the fixed Anser consumer wait table at postmaster start " + "(anser.max_channels * this). Each channel has one consumer per segment, " + "so it should be set to the number of primary segments; the plan pass injects " + "at most one consumer per channel. It cannot be auto-derived because the " + "segment count is a catalog value unavailable at postmaster start. Over-sizing " + "only wastes shared memory; under-sizing makes surplus consumers fail open " + "(unfiltered), never wrong results.", + &gp_anser_max_consumers_per_channel, + 64, 1, INT_MAX, + PGC_POSTMASTER, + 0, + NULL, NULL, NULL); + + MarkGUCPrefixReserved("anser"); +} + +/* + * Register the gather and send services. + * + * Both live on the coordinator only. The decision is made here rather than in + * a bgw_start_rule because the postmaster consults that field only for its own + * auxiliary process list, not for workers an extension registers. Gp_role is + * already settled at this point: the configuration files (which carry + * gp_contentid) are processed before shared_preload_libraries. + */ +static void +anser_register_services(void) +{ + BackgroundWorker worker; + int i; + + static const struct + { + const char *name; + const char *main_func; + } services[] = + { + {"anser gather service", "AnserGatherServiceMain"}, + {"anser send service", "AnserSendServiceMain"} + }; + + if (!AnserStartRule((Datum) 0)) + return; + + for (i = 0; i < lengthof(services); i++) + { + MemSet(&worker, 0, sizeof(worker)); + worker.bgw_flags = BGWORKER_SHMEM_ACCESS; + worker.bgw_start_time = BgWorkerStart_RecoveryFinished; + worker.bgw_restart_time = 1; + worker.bgw_notify_pid = 0; + snprintf(worker.bgw_name, BGW_MAXLEN, "%s", services[i].name); + snprintf(worker.bgw_type, BGW_MAXLEN, "%s", services[i].name); + snprintf(worker.bgw_library_name, BGW_MAXLEN, "anser"); + snprintf(worker.bgw_function_name, BGW_MAXLEN, "%s", + services[i].main_func); + + RegisterBackgroundWorker(&worker); + } +} + +static void +anser_shmem_request(void) +{ + if (prev_shmem_request_hook) + prev_shmem_request_hook(); + + if (!gp_anser_enable) + return; + + RequestAddinShmemSpace(AnserShmemSize()); + RequestNamedLWLockTranche(ANSER_LWLOCK_TRANCHE, ANSER_NUM_LWLOCKS); +} + +static void +anser_shmem_startup(void) +{ + if (prev_shmem_startup_hook) + prev_shmem_startup_hook(); + + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + AnserShmemInit(); + LWLockRelease(AddinShmemInitLock); +} + +/* + * Plan the query as usual, then hand the finished tree to the runtime-filter + * pass. Wrapping the hook this way covers both optimizers, because ORCA is + * dispatched from inside standard_planner(). + */ +static PlannedStmt * +anser_planner(Query *parse, const char *query_string, int cursorOptions, + ParamListInfo boundParams, OptimizerOptions *optimizer_options) +{ + PlannedStmt *result; + + if (prev_planner_hook) + result = prev_planner_hook(parse, query_string, cursorOptions, + boundParams, optimizer_options); + else + result = standard_planner(parse, query_string, cursorOptions, + boundParams, optimizer_options); + + AnserApplyRuntimeFilters(result); + + return result; +} diff --git a/gpcontrib/anser/src/anserplan.c b/gpcontrib/anser/src/anserplan.c new file mode 100644 index 00000000000..6f3a58d185a --- /dev/null +++ b/gpcontrib/anser/src/anserplan.c @@ -0,0 +1,451 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserplan.c + * Post-planning transformation that injects Anser runtime bloom-filter + * producer/consumer CustomScan nodes into a finished plan tree. + * + * The pass runs once from planner() (after both the Postgres planner and ORCA, + * and after set_plan_references / the cdbllize slice passes), recognizes one + * supported join shape, and inserts a producer on the hash build side and a + * consumer above the probe scan. See anserplan.h for the + * rationale. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserplan.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserplan.h" +#include "cdb/cdbvars.h" +#include "catalog/pg_type.h" +#include "commands/extension.h" +#include "nodes/nodeFuncs.h" +#include "nodes/pg_list.h" +#include "utils/acl.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" + +/* Runtime-filter bloom size bounds (realized bitset bytes). */ +#define ANSER_RF_MIN_BYTES (1024 * 1024) /* bloom_create's 1 MB floor */ +#define ANSER_RF_MAX_BYTES (64 * 1024 * 1024) +#define ANSER_RF_HEADER_ROOM 64 + +/* Per-statement state for the injection pass. */ +typedef struct AnserInjectCtx +{ + uint32 next_condition_id; + int next_plan_node_id; + List *consumer_keys; /* condition_keys already given a consumer node; + * enforces one consumer per channel (see + * anser_try_inject) */ + char *token; /* QD session token for the segment -> QD backward + * connections; lazily registered at the first + * injection, NULL when unavailable (fail open to + * pg_hba-driven authentication) */ +} AnserInjectCtx; + +static bool anser_transport_installed(void); +static int anser_max_plan_node_id(Plan *plan); +static bool anser_rf_size(double est_rows, int64 *total_elems, + int64 *max_payload, int64 *planned_bytes); +static bool anser_hashjoin_keys(HashJoin *hj, AttrNumber *inner_attno, + AttrNumber *outer_attno); +static bool anser_resolve_build_scan(Plan *hash, AttrNumber inner_attno, + Plan **parent_out, Plan **scan_out, + AttrNumber *attno_out); +static void anser_try_inject(HashJoin *hj, AnserInjectCtx *ctx); +static void anser_inject_walk(Plan *plan, AnserInjectCtx *ctx); + +void +AnserApplyRuntimeFilters(PlannedStmt *stmt) +{ + /* + * Opt-in and coordinator-only: the pass runs on the QD where the whole + * PlannedStmt is available, and only when the operator has enabled the + * Anser subsystem and the runtime-filter feature. Anything else is left + * completely untouched. + */ + if (!gp_anser_enable || !gp_anser_runtime_filter) + return; + if (Gp_role != GP_ROLE_DISPATCH) + return; + if (stmt == NULL || stmt->commandType != CMD_SELECT || stmt->planTree == NULL) + return; + + /* + * The injected nodes are only useful if the segments can call back into the + * transport functions in *this* database, which means the extension has to + * be installed here. Without it we would dispatch a plan whose producers + * and consumers all fail their libpq calls and fail open one by one; skip + * the injection instead. + */ + if (!anser_transport_installed()) + return; + + { + AnserInjectCtx ctx; + ListCell *lc; + int maxid; + + /* + * Injected nodes need plan_node_ids unique across the whole statement. + * set_plan_references already numbered every existing node, so continue + * past the current maximum (planTree + subplans). + * + * We walk the tree because neither planner's id counter survives to + * this hook (the Postgres planner counts in a standard_planner() + * local; ORCA counts inside its DXL translation context) and + * PlannedStmt carries no max-id field -- walking is the only + * planner-agnostic option, and cheap at this hook point. + * + * We take the max, not the node count: "count == next free id" + * assumes dense numbering, which ORCA's CIdGenerator and third-party + * planner_hooks do not promise. max + 1 is correct under any + * assignment scheme. + */ + maxid = anser_max_plan_node_id(stmt->planTree); + foreach(lc, stmt->subplans) + maxid = Max(maxid, anser_max_plan_node_id((Plan *) lfirst(lc))); + + ctx.next_condition_id = 0; + ctx.next_plan_node_id = maxid + 1; + ctx.consumer_keys = NIL; + + /* + * Segment executors connect back to the QD to publish/consume bloom + * parts; they authenticate with this session's token (the + * parallel-retrieve-cursor model) instead of relying on pg_hba entries + * for the segment hosts. Keyed by the session user because that is + * the identity the QEs connect with. NULL means unavailable -- the + * connection then falls back to ordinary pg_hba authentication. + */ + ctx.token = AnserGetOrCreateSessionToken(GetSessionUserId()); + + anser_inject_walk(stmt->planTree, &ctx); + } +} + +/* + * Is "CREATE EXTENSION anser" present in the current database? + * + * A pg_extension lookup, deliberately not a lookup of anser.publish() itself: + * resolving a schema-qualified function name checks USAGE on the schema and + * raises when the planning role lacks it, which would turn a missing privilege + * into a failed query instead of an unfiltered one. + * + * The lookup runs per planned statement -- only for statements that already + * passed the GUC tests -- rather than being cached, so CREATE EXTENSION takes + * effect immediately, without an invalidation callback. + */ +static bool +anser_transport_installed(void) +{ + return OidIsValid(get_extension_oid("anser", true)); +} + +/* + * Largest plan_node_id in a plan subtree. Recurses the spine plus CustomScan + * children; sufficient for the supported (simple) plan shape. + */ +static int +anser_max_plan_node_id(Plan *plan) +{ + int m; + + if (plan == NULL) + return 0; + + m = plan->plan_node_id; + m = Max(m, anser_max_plan_node_id(outerPlan(plan))); + m = Max(m, anser_max_plan_node_id(innerPlan(plan))); + if (IsA(plan, CustomScan)) + { + ListCell *lc; + + foreach(lc, ((CustomScan *) plan)->custom_plans) + m = Max(m, anser_max_plan_node_id((Plan *) lfirst(lc))); + } + + return m; +} + +/* + * Compute the bloom sizing to hand both the producer and consumer helpers, from + * the estimated build cardinality. Both call AnserBloomCreate (== bloom_create) + * with the SAME (total_elems, max_payload) so they realize an identical filter; + * we mirror bloom_create's own math here so `planned_bytes` (shown in EXPLAIN) + * equals the realized bitset: target ~2 bytes/element, floor at 1 MB, cap at the + * server payload budget, round DOWN to a power of two. + */ +static bool +anser_rf_size(double est_rows, int64 *total_elems, int64 *max_payload, + int64 *planned_bytes) +{ + int64 cap_bytes; + int64 elems; + int64 target_bytes; + int64 realized; + + /* Largest bitset that fits the server payload cap, and our own ceiling. */ + cap_bytes = Min((int64) ANSER_RF_MAX_BYTES, + (int64) gp_anser_max_info_size - ANSER_RF_HEADER_ROOM); + if (cap_bytes < ANSER_RF_MIN_BYTES) + return false; /* cap too small to hold even a floor-sized filter */ + + /* + * Clamp the element estimate so 2*elems never exceeds the cap; this also keeps + * total_elems within int range for custom_private (cap/2 <= 32M elements). + */ + elems = (est_rows > 0.0) ? (int64) est_rows : 1; + if (elems > cap_bytes / 2) + elems = cap_bytes / 2; + if (elems < 1) + elems = 1; + + target_bytes = Max((int64) ANSER_RF_MIN_BYTES, elems * 2); + realized = ANSER_RF_MIN_BYTES; + while ((realized << 1) <= target_bytes) + realized <<= 1; + + *total_elems = elems; + *max_payload = cap_bytes + ANSER_RF_HEADER_ROOM; + *planned_bytes = realized; + return true; +} + +/* + * Match a single-column equijoin over plain (by-value) Vars and return the + * inner (build) and outer (probe) key attnos. After set_plan_references the + * operands are INNER_VAR / OUTER_VAR references into the join's child tlists. + */ +static bool +anser_hashjoin_keys(HashJoin *hj, AttrNumber *inner_attno, AttrNumber *outer_attno) +{ + OpExpr *op; + Node *l; + Node *r; + Var *outer_var; + Var *inner_var; + + if (list_length(hj->hashclauses) != 1) + return false; + op = (OpExpr *) linitial(hj->hashclauses); + if (!IsA(op, OpExpr) || list_length(op->args) != 2) + return false; + + l = (Node *) linitial(op->args); + r = (Node *) lsecond(op->args); + while (l != NULL && IsA(l, RelabelType)) + l = (Node *) ((RelabelType *) l)->arg; + while (r != NULL && IsA(r, RelabelType)) + r = (Node *) ((RelabelType *) r)->arg; + if (l == NULL || r == NULL || !IsA(l, Var) || !IsA(r, Var)) + return false; + + if (((Var *) l)->varno == OUTER_VAR && ((Var *) r)->varno == INNER_VAR) + { + outer_var = (Var *) l; + inner_var = (Var *) r; + } + else if (((Var *) l)->varno == INNER_VAR && ((Var *) r)->varno == OUTER_VAR) + { + outer_var = (Var *) r; + inner_var = (Var *) l; + } + else + return false; + + /* + * Producer and consumer hash the raw Datum bytes, which is only correct + * when SQL equality coincides with bitwise Datum equality of the key. + * That requires the SAME by-value type on both sides: cross-type equijoins + * (e.g. float4 = float8, date = timestamp) hash different bit patterns for + * equal values, and floats are excluded even same-typed because -0.0 and + * 0.0 compare equal but are not bitwise equal. Anything looser could + * prune rows that actually join. + */ + if (inner_var->vartype != outer_var->vartype) + return false; + if (!get_typbyval(inner_var->vartype)) + return false; + if (inner_var->vartype == FLOAT4OID || inner_var->vartype == FLOAT8OID) + return false; + + *inner_attno = inner_var->varattno; + *outer_attno = outer_var->varattno; + return true; +} + +/* + * Follow the build side down from the Hash to the base SeqScan, mapping the key + * attno through each passthrough targetlist. Wrapping the base scan (rather than + * an intermediate Motion) keeps the injected CustomScan's custom_scan_tlist made + * of base-relation Vars, which (a) deparses cleanly in EXPLAIN and (b) is the + * proven-safe "leaf child" case for MPP slice/gang setup. Only plain single- + * child passthroughs (Hash, Motion) with Var targetlist entries are supported. + * On success *parent_out is the node whose outerPlan is the base scan. + */ +static bool +anser_resolve_build_scan(Plan *hash, AttrNumber inner_attno, Plan **parent_out, + Plan **scan_out, AttrNumber *attno_out) +{ + Plan *node = hash; + AttrNumber attno = inner_attno; + + for (;;) + { + TargetEntry *tle; + Var *var; + Plan *child; + + if (node == NULL || + attno < 1 || attno > list_length(node->targetlist)) + return false; + + tle = (TargetEntry *) list_nth(node->targetlist, attno - 1); + if (tle == NULL || !IsA(tle->expr, Var)) + return false; + var = (Var *) tle->expr; + if (var->varno != OUTER_VAR) /* single-child passthrough only */ + return false; + + child = outerPlan(node); + if (child == NULL) + return false; + + if (IsA(child, SeqScan)) + { + *parent_out = node; + *scan_out = child; + *attno_out = var->varattno; + return true; + } + if (!IsA(child, Hash) && !IsA(child, Motion)) + return false; + + node = child; + attno = var->varattno; + } +} + +/* + * If this HashJoin is the supported shape, inject a producer above the build + * base scan and a consumer above the probe scan. + */ +static void +anser_try_inject(HashJoin *hj, AnserInjectCtx *ctx) +{ + Plan *hash = innerPlan(hj); /* build side */ + Plan *probe = outerPlan(hj); /* probe side */ + Plan *build_parent; + Plan *build_scan; + AttrNumber inner_attno; + AttrNumber outer_attno; + AttrNumber build_attno; + int64 total_elems; + int64 max_payload; + int64 planned_bytes; + uint32 condition_id; + char condition_key[ANSER_CONDITION_KEY_SIZE]; + CustomScan *producer; + CustomScan *consumer; + ListCell *lc; + + if (hj->join.jointype != JOIN_INNER && hj->join.jointype != JOIN_RIGHT) + return; + if (hash == NULL || !IsA(hash, Hash)) + return; + if (probe == NULL || !IsA(probe, SeqScan)) + return; + + if (!anser_hashjoin_keys(hj, &inner_attno, &outer_attno)) + return; + if (!anser_resolve_build_scan(hash, inner_attno, &build_parent, &build_scan, + &build_attno)) + return; + if (!anser_rf_size(hash->plan_rows, &total_elems, &max_payload, &planned_bytes)) + return; + + condition_id = ctx->next_condition_id++; + snprintf(condition_key, sizeof(condition_key), "anser_rf_%u", condition_id); + + /* + * One consumer per channel. The consumer wait table budgets exactly + * anser.max_consumers_per_channel slots per channel, sized for one + * consumer instance per segment (nseg). A second consumer plan node on the + * same channel would need 2*nseg slots and could exhaust that budget, so we + * never inject one -- skip the whole join and fail open instead. + * + * Minting a unique condition_id per injection makes this hold by + * construction, so the check never fires. It stays as a guard against + * channel-key collisions if the key derivation ever changes (e.g. keys + * derived from the build's semantic identity, where two joins could share + * one channel). + */ + foreach(lc, ctx->consumer_keys) + { + if (strcmp((const char *) lfirst(lc), condition_key) == 0) + return; + } + + /* Producer wraps the build base scan; keyed by the mapped build attno. */ + producer = AnserBuildBloomProducerScan(build_scan, build_attno, condition_id, + condition_key, total_elems, max_payload, + planned_bytes, ctx->token); + producer->scan.plan.plan_node_id = ctx->next_plan_node_id++; + outerPlan(build_parent) = (Plan *) producer; + + /* Consumer wraps the probe scan; keyed by the outer (probe) attno. */ + consumer = AnserBuildBloomConsumerScan(probe, outer_attno, condition_id, + condition_key, total_elems, max_payload, + planned_bytes, ctx->token); + consumer->scan.plan.plan_node_id = ctx->next_plan_node_id++; + outerPlan(hj) = (Plan *) consumer; + + /* Record the channel so no later join can add a second consumer on it. */ + ctx->consumer_keys = lappend(ctx->consumer_keys, pstrdup(condition_key)); +} + +/* + * Manual in-place traversal (recurse spine + custom_plans). We do not use + * plan_tree_mutator, whose CustomScan arm does not descend into custom_plans. + */ +static void +anser_inject_walk(Plan *plan, AnserInjectCtx *ctx) +{ + if (plan == NULL) + return; + + if (IsA(plan, HashJoin)) + anser_try_inject((HashJoin *) plan, ctx); + + anser_inject_walk(outerPlan(plan), ctx); + anser_inject_walk(innerPlan(plan), ctx); + if (IsA(plan, CustomScan)) + { + ListCell *lc; + + foreach(lc, ((CustomScan *) plan)->custom_plans) + anser_inject_walk((Plan *) lfirst(lc), ctx); + } +} diff --git a/gpcontrib/anser/src/anserplanexec.c b/gpcontrib/anser/src/anserplanexec.c new file mode 100644 index 00000000000..69a57706452 --- /dev/null +++ b/gpcontrib/anser/src/anserplanexec.c @@ -0,0 +1,674 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserplanexec.c + * CustomScan providers that carry the Anser runtime bloom filter through + * the executor: a pass-through "producer" above the hash build input that + * observes the build join key and publishes a bloom filter, and a + * pass-through "consumer" above the probe scan that prunes rows whose key is + * definitely absent from the received (unioned) filter. + * + * Both are thin drivers over the helper library in + * anserbloom.h, which selects transport by role + * (segment -> libpq to QD; coordinator -> direct shmem) and unions parts. The + * plan-injection pass (anserplan.c) builds the nodes and stashes the parameters + * in CustomScan.custom_private using the layout in AnserRfPrivateIndex below. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserplanexec.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/skey.h" +#include "anser.h" +#include "anserbloom.h" +#include "anserfilter.h" +#include "anserplan.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "commands/explain.h" +#include "executor/executor.h" +#include "lib/bloomfilter.h" +#include "nodes/extensible.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "nodes/pg_list.h" +#include "nodes/value.h" +#include "utils/guc.h" + +/* + * Positional layout of CustomScan.custom_private shared with anserplan.c. + * Integer nodes except the last two, which are Strings. Both providers read + * the same list (each ignores fields it does not need). + */ +typedef enum AnserRfPrivateIndex +{ + ANSER_RF_PRIV_CONDITION_ID = 0, /* Integer: channel condition_id */ + ANSER_RF_PRIV_KEY_ATTNO, /* Integer: build/probe key attno */ + ANSER_RF_PRIV_TOTAL_ELEMS, /* Integer: bloom sizing (producer) */ + ANSER_RF_PRIV_MAX_PAYLOAD, /* Integer: bloom sizing (producer) */ + ANSER_RF_PRIV_PLANNED_BYTES, /* Integer: planned bitset bytes (EXPLAIN) */ + ANSER_RF_PRIV_CONDITION_KEY, /* String: channel condition_key */ + ANSER_RF_PRIV_TOKEN, /* String: QD session token (may be "") */ + ANSER_RF_PRIV__COUNT +} AnserRfPrivateIndex; + +/* Registration-timeout for the consumer's wait-for-registration phase. */ +#define ANSER_RF_REGISTRATION_TIMEOUT_MS ((long) gp_anser_timeout_ms) + +/* + * Producer scan state: feeds the build key of every child tuple into the + * filter and publishes this backend's part once the child is exhausted. + */ +typedef struct AnserBloomProduceScanState +{ + CustomScanState csstate; + AnserBloomFilterProduceState *produce; + AttrNumber key_attno; + int64 planned_bytes; + char *token; /* QD session token, NULL when none */ + bool published; +} AnserBloomProduceScanState; + +/* + * Consumer scan state: receives and unions the bloom parts on first use, + * then prunes probe rows whose key is definitely absent from the filter. + */ +typedef struct AnserBloomConsumeScanState +{ + CustomScanState csstate; + AnserBloomFilterConsumeState *consume; + bloom_filter *filter; /* NULL => fail open (pass everything) */ + AttrNumber key_attno; + int64 planned_bytes; + char *token; /* QD session token, NULL when none */ + bool received; /* have we run the receive/union yet? */ + bool pushed_down; /* filter handed to the child scan as an + * SK_BLOOM_FILTER scan key (the scan filters, + * we pass through) */ +} AnserBloomConsumeScanState; + +/* Provider callbacks. */ +static Node *anser_produce_create_state(CustomScan *cscan); +static void anser_produce_begin(CustomScanState *node, EState *estate, int eflags); +static TupleTableSlot *anser_produce_exec(CustomScanState *node); +static void anser_produce_end(CustomScanState *node); +static void anser_produce_rescan(CustomScanState *node); +static void anser_produce_explain(CustomScanState *node, List *ancestors, + ExplainState *es); + +static Node *anser_consume_create_state(CustomScan *cscan); +static void anser_consume_begin(CustomScanState *node, EState *estate, int eflags); +static TupleTableSlot *anser_consume_exec(CustomScanState *node); +static void anser_consume_end(CustomScanState *node); +static void anser_consume_rescan(CustomScanState *node); +static void anser_consume_explain(CustomScanState *node, List *ancestors, + ExplainState *es); + +static const CustomScanMethods anser_produce_scan_methods = +{ + .CustomName = "Anser Bloom Producer", + .CreateCustomScanState = anser_produce_create_state, +}; + +static const CustomScanMethods anser_consume_scan_methods = +{ + .CustomName = "Anser Bloom Consumer", + .CreateCustomScanState = anser_consume_create_state, +}; + +static const CustomExecMethods anser_produce_exec_methods = +{ + .CustomName = "Anser Bloom Producer", + .BeginCustomScan = anser_produce_begin, + .ExecCustomScan = anser_produce_exec, + .EndCustomScan = anser_produce_end, + .ReScanCustomScan = anser_produce_rescan, + .ExplainCustomScan = anser_produce_explain, +}; + +static const CustomExecMethods anser_consume_exec_methods = +{ + .CustomName = "Anser Bloom Consumer", + .BeginCustomScan = anser_consume_begin, + .ExecCustomScan = anser_consume_exec, + .EndCustomScan = anser_consume_end, + .ReScanCustomScan = anser_consume_rescan, + .ExplainCustomScan = anser_consume_explain, +}; + +void +AnserRegisterRuntimeFilterMethods(void) +{ + RegisterCustomScanMethods(&anser_produce_scan_methods); + RegisterCustomScanMethods(&anser_consume_scan_methods); +} + +/* ---- node builders (called from the injection pass in anserplan.c) ---- */ + +/* + * Identity output targetlist for a pass-through CustomScan: a Var per child + * column referencing the scan tuple via INDEX_VAR, so ExecScan projects the + * child tuple through unchanged. (Post-setrefs we build this by hand rather + * than relying on set_customscan_references.) + */ +static List * +anser_rf_identity_tlist(List *child_tlist) +{ + List *tlist = NIL; + ListCell *lc; + AttrNumber attno = 0; + + foreach(lc, child_tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Var *var; + + attno++; + var = makeVar(INDEX_VAR, attno, + exprType((Node *) tle->expr), + exprTypmod((Node *) tle->expr), + exprCollation((Node *) tle->expr), + 0); + tlist = lappend(tlist, + makeTargetEntry((Expr *) var, attno, + tle->resname ? pstrdup(tle->resname) : NULL, + tle->resjunk)); + } + + return tlist; +} + +static CustomScan * +anser_build_rf_scan(const CustomScanMethods *methods, Plan *child, + AttrNumber key_attno, uint32 condition_id, + const char *condition_key, int64 total_elems, + Size max_payload_bytes, int64 planned_bytes, + const char *token) +{ + CustomScan *cs = makeNode(CustomScan); + List *priv = NIL; + + /* custom_private, in AnserRfPrivateIndex order. */ + priv = lappend(priv, makeInteger((int) condition_id)); + priv = lappend(priv, makeInteger((int) key_attno)); + priv = lappend(priv, makeInteger((int) total_elems)); + priv = lappend(priv, makeInteger((int) max_payload_bytes)); + priv = lappend(priv, makeInteger((int) planned_bytes)); + priv = lappend(priv, makeString(pstrdup(condition_key))); + priv = lappend(priv, makeString(pstrdup(token != NULL ? token : ""))); + + cs->scan.plan.targetlist = anser_rf_identity_tlist(child->targetlist); + cs->scan.plan.qual = NIL; + cs->scan.plan.lefttree = NULL; + cs->scan.plan.righttree = NULL; + cs->scan.plan.startup_cost = child->startup_cost; + cs->scan.plan.total_cost = child->total_cost; + cs->scan.plan.plan_rows = child->plan_rows; + cs->scan.plan.plan_width = child->plan_width; + cs->scan.plan.parallel_aware = false; + cs->scan.plan.parallel_safe = child->parallel_safe; + cs->scan.plan.flow = (Flow *) copyObject(child->flow); + cs->scan.scanrelid = 0; + cs->flags = 0; + cs->custom_plans = list_make1(child); + cs->custom_exprs = NIL; + cs->custom_private = priv; + cs->custom_scan_tlist = copyObject(child->targetlist); + cs->methods = methods; + + return cs; +} + +CustomScan * +AnserBuildBloomProducerScan(Plan *child, AttrNumber key_attno, + uint32 condition_id, const char *condition_key, + int64 total_elems, Size max_payload_bytes, + int64 planned_bytes, const char *token) +{ + return anser_build_rf_scan(&anser_produce_scan_methods, child, key_attno, + condition_id, condition_key, total_elems, + max_payload_bytes, planned_bytes, token); +} + +CustomScan * +AnserBuildBloomConsumerScan(Plan *child, AttrNumber key_attno, + uint32 condition_id, const char *condition_key, + int64 total_elems, Size max_payload_bytes, + int64 planned_bytes, const char *token) +{ + return anser_build_rf_scan(&anser_consume_scan_methods, child, key_attno, + condition_id, condition_key, total_elems, + max_payload_bytes, planned_bytes, token); +} + +/* ---- shared helpers ---- */ + +static void +anser_rf_build_key(CustomScan *cscan, AnserChannelKey *key) +{ + List *priv = cscan->custom_private; + int condition_id = intVal(list_nth(priv, ANSER_RF_PRIV_CONDITION_ID)); + char *condition_key = strVal(list_nth(priv, ANSER_RF_PRIV_CONDITION_KEY)); + + MemSet(key, 0, sizeof(*key)); + key->gp_session_id = gp_session_id; + key->gp_command_count = gp_command_count; + key->condition_id = (uint32) condition_id; + strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); +} + +/* Session token carried in custom_private; NULL when absent/empty. */ +static char * +anser_rf_token(CustomScan *cscan) +{ + char *token = strVal(list_nth(cscan->custom_private, ANSER_RF_PRIV_TOKEN)); + + return token[0] != '\0' ? token : NULL; +} + +/* + * Number of producing segments for this slice, and this backend's part index. + * On a segment the slice runs on the whole gang; on the coordinator the filter + * is produced locally as a single part. + */ +static void +anser_rf_part_info(uint32 *part_index, uint32 *total_parts) +{ + if (Gp_role == GP_ROLE_EXECUTE) + { + *part_index = (uint32) GpIdentity.segindex; + *total_parts = (uint32) getgpsegmentCount(); + } + else + { + *part_index = 0; + *total_parts = 1; + } +} + +/* Move the child tuple into our scan slot (positional pass-through). */ +static TupleTableSlot * +anser_rf_child_slot(CustomScanState *node, TupleTableSlot *childslot) +{ + ExecCopySlot(node->ss.ss_ScanTupleSlot, childslot); + return node->ss.ss_ScanTupleSlot; +} + +static bool +anser_rf_recheck(CustomScanState *node, TupleTableSlot *slot) +{ + return true; +} + +/* ---- producer ---- */ + +static Node * +anser_produce_create_state(CustomScan *cscan) +{ + AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) + newNode(sizeof(AnserBloomProduceScanState), T_CustomScanState); + + st->csstate.methods = &anser_produce_exec_methods; + return (Node *) st; +} + +static void +anser_produce_begin(CustomScanState *node, EState *estate, int eflags) +{ + AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) node; + CustomScan *cscan = (CustomScan *) node->ss.ps.plan; + List *priv = cscan->custom_private; + Plan *child = (Plan *) linitial(cscan->custom_plans); + AnserChannelKey key; + uint32 part_index; + uint32 total_parts; + int64 total_elems = intVal(list_nth(priv, ANSER_RF_PRIV_TOTAL_ELEMS)); + Size max_payload = (Size) intVal(list_nth(priv, ANSER_RF_PRIV_MAX_PAYLOAD)); + + st->key_attno = (AttrNumber) intVal(list_nth(priv, ANSER_RF_PRIV_KEY_ATTNO)); + st->planned_bytes = intVal(list_nth(priv, ANSER_RF_PRIV_PLANNED_BYTES)); + st->token = anser_rf_token(cscan); + st->published = false; + + anser_rf_build_key(cscan, &key); + anser_rf_part_info(&part_index, &total_parts); + + st->produce = ExecInitAnserBloomFilterProduce(&key, total_elems, max_payload, + part_index, total_parts, + st->token); + + node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); +} + +/* + * Pass-through that feeds the build key of every child tuple into the filter, + * and publishes once the child is exhausted. + */ +static TupleTableSlot * +anser_produce_next(CustomScanState *node) +{ + AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) node; + PlanState *child = (PlanState *) linitial(node->custom_ps); + TupleTableSlot *slot = ExecProcNode(child); + + if (TupIsNull(slot)) + { + if (!st->published) + { + (void) ExecAnserBloomFilterProducePublish(st->produce); + st->published = true; + } + return NULL; + } + + if (st->produce != NULL) + { + bool isnull; + Datum value = slot_getattr(slot, st->key_attno, &isnull); + + ExecAnserBloomFilterProduceAddDatum(st->produce, value, isnull); + } + + return anser_rf_child_slot(node, slot); +} + +static TupleTableSlot * +anser_produce_exec(CustomScanState *node) +{ + return ExecScan(&node->ss, + (ExecScanAccessMtd) anser_produce_next, + (ExecScanRecheckMtd) anser_rf_recheck); +} + +static void +anser_produce_end(CustomScanState *node) +{ + AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) node; + + if (st->produce != NULL) + ExecEndAnserBloomFilterProduce(st->produce); + st->produce = NULL; + if (node->custom_ps != NIL) + ExecEndNode((PlanState *) linitial(node->custom_ps)); +} + +static void +anser_produce_rescan(CustomScanState *node) +{ + if (node->custom_ps != NIL) + ExecReScan((PlanState *) linitial(node->custom_ps)); +} + +static void +anser_produce_explain(CustomScanState *node, List *ancestors, ExplainState *es) +{ + AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) node; + char buf[64]; + + ExplainPropertyInteger("Bloom Filter Size", "bytes", st->planned_bytes, es); + + /* + * The realized filter size is planned_bytes by construction: producer and + * consumer build identical filters from the same plan parameters (see + * anser_rf_size in anserplan.c), so no execution stats are needed. + */ + snprintf(buf, sizeof(buf), "memory=" INT64_FORMAT "kB", + st->planned_bytes / 1024); + ExplainPropertyText("Bloom Filter Stats", buf, es); +} + +/* ---- consumer ---- */ + +static Node * +anser_consume_create_state(CustomScan *cscan) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) + newNode(sizeof(AnserBloomConsumeScanState), T_CustomScanState); + + st->csstate.methods = &anser_consume_exec_methods; + return (Node *) st; +} + +static void +anser_consume_begin(CustomScanState *node, EState *estate, int eflags) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + CustomScan *cscan = (CustomScan *) node->ss.ps.plan; + List *priv = cscan->custom_private; + Plan *child = (Plan *) linitial(cscan->custom_plans); + AnserChannelKey key; + uint32 part_index; + uint32 expected_parts; + int64 total_elems = intVal(list_nth(priv, ANSER_RF_PRIV_TOTAL_ELEMS)); + Size max_payload = (Size) intVal(list_nth(priv, ANSER_RF_PRIV_MAX_PAYLOAD)); + + st->key_attno = (AttrNumber) intVal(list_nth(priv, ANSER_RF_PRIV_KEY_ATTNO)); + st->planned_bytes = intVal(list_nth(priv, ANSER_RF_PRIV_PLANNED_BYTES)); + st->token = anser_rf_token(cscan); + st->filter = NULL; + st->received = false; + st->pushed_down = false; + + anser_rf_build_key(cscan, &key); + anser_rf_part_info(&part_index, &expected_parts); + + st->consume = ExecInitAnserBloomFilterConsume(&key, total_elems, max_payload, + expected_parts, st->token); + + node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); +} + +/* + * On first call, block to receive and union the bloom parts. A NULL filter + * (channel cancelled / unreachable / feature degraded) means fail open: pass + * every row through unfiltered. + */ +static void +anser_consume_receive(AnserBloomConsumeScanState *st) +{ + if (st->received) + return; + st->received = true; + + if (st->consume == NULL) + return; + + if (ExecAnserBloomFilterConsume(st->consume, ANSER_RF_REGISTRATION_TIMEOUT_MS)) + st->filter = ExecAnserBloomFilterConsumerGetFilter(st->consume); + else + st->filter = NULL; + + /* + * Diagnostic: a NULL filter means we fail open (no pruning). Log why -- + * cancelled delivery vs. too few bloom parts unioned -- so runtime-filter + * misbehavior is visible in the server log. + */ + if (st->filter == NULL) + elog(LOG, + "anser bloom consumer: no filter, failing open (cancelled=%d, received_parts=%u)", + ExecAnserBloomFilterConsumerWasCancelled(st->consume), + ExecAnserBloomFilterConsumerReceivedParts(st->consume)); +} + +/* + * Hand the received filter to the child SeqScan as an SK_BLOOM_FILTER scan + * key -- the exact structure PassByBloomFilter consumes (nodeSeqscan.c), so + * the scan does the pruning itself and a table AM that supports + * SCAN_SUPPORT_RUNTIME_FILTER may additionally use the key for block-level + * pruning. Runs before the child's first ExecProcNode, so the key is in + * node->filters when the scan descriptor is created. The filter stays owned + * by us; anser_consume_end ends the child before freeing it. + */ +static void +anser_consume_pushdown(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + PlanState *child = (PlanState *) linitial(node->custom_ps); + SeqScanState *sss; + ScanKey sk; + MemoryContext oldcxt; + + if (!gp_enable_runtime_filter_pushdown || !IsA(child, SeqScanState)) + return; + sss = (SeqScanState *) child; + if (!sss->filter_in_seqscan) + return; + + oldcxt = MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt); + sk = (ScanKey) palloc0(sizeof(ScanKeyData)); + sk->sk_attno = st->key_attno; + sk->sk_flags = SK_BLOOM_FILTER; + sk->sk_argument = PointerGetDatum(st->filter); + sss->filters = lappend(sss->filters, sk); + MemoryContextSwitchTo(oldcxt); + + st->pushed_down = true; +} + +static TupleTableSlot * +anser_consume_next(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + PlanState *child = (PlanState *) linitial(node->custom_ps); + + anser_consume_receive(st); + + /* + * When the pushdown machinery is enabled, hand the filter to the scan and + * pass tuples through untouched; otherwise probe the filter here. + */ + if (st->filter != NULL && !st->pushed_down) + anser_consume_pushdown(node); + + for (;;) + { + TupleTableSlot *slot = ExecProcNode(child); + bool isnull; + Datum value; + + if (TupIsNull(slot)) + return NULL; + + /* + * Fail open (no usable filter) or pushed down (the scan filters via + * PassByBloomFilter): pass everything. + */ + if (st->filter == NULL || st->pushed_down) + return anser_rf_child_slot(node, slot); + + value = slot_getattr(slot, st->key_attno, &isnull); + + /* NULLs never match an equijoin key; let the join handle them. */ + if (!isnull) + { + /* + * Count lookups/prunes via instrumentation (nfiltered1/nfiltered2), + * not local state: these fields ride the CdbExplain_StatInst wire + * format back to the QD, so EXPLAIN ANALYZE shows them for + * segment-executed nodes too. + */ + InstrCountFiltered1(node, 1); + if (bloom_lacks_element(st->filter, (unsigned char *) &value, + sizeof(Datum))) + { + /* Definitely absent from the build side -> prune. */ + InstrCountFiltered2(node, 1); + continue; + } + } + + return anser_rf_child_slot(node, slot); + } +} + +static TupleTableSlot * +anser_consume_exec(CustomScanState *node) +{ + return ExecScan(&node->ss, + (ExecScanAccessMtd) anser_consume_next, + (ExecScanRecheckMtd) anser_rf_recheck); +} + +static void +anser_consume_end(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + + /* + * End the child first: when the filter was pushed down, the child's scan + * key references it, so the filter must outlive the child node. + */ + if (node->custom_ps != NIL) + ExecEndNode((PlanState *) linitial(node->custom_ps)); + if (st->consume != NULL) + ExecEndAnserBloomFilterConsume(st->consume); + st->consume = NULL; + st->filter = NULL; +} + +static void +anser_consume_rescan(CustomScanState *node) +{ + if (node->custom_ps != NIL) + ExecReScan((PlanState *) linitial(node->custom_ps)); +} + +static void +anser_consume_explain(CustomScanState *node, List *ancestors, ExplainState *es) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + + ExplainPropertyInteger("Bloom Filter Size", "bytes", st->planned_bytes, es); + + if (es->analyze && node->ss.ps.instrument != NULL) + { + char buf[128]; + double nloops = node->ss.ps.instrument->nloops; + double nfiltered = node->ss.ps.instrument->nfiltered2; + double nchecked = node->ss.ps.instrument->nfiltered1; + + /* + * checked/rejected come from nfiltered1/nfiltered2, which cdbexplain + * transports from the segments and deposits into this node's + * instrument. As with all per-node EXPLAIN ANALYZE stats in MPP, + * these are the winning segment's values (max ntuples/nloops), not a + * cluster-wide sum. memory is planned_bytes: the unioned filter's + * size is identical on every segment by construction (anser_rf_size). + * When the filter was pushed into the child scan, pruning happens + * there instead (its "Rows Removed by Pushdown Runtime Filter" line), + * so our counters stay zero. + */ + if (st->pushed_down) + snprintf(buf, sizeof(buf), + "memory=" INT64_FORMAT "kB (pushed down to Seq Scan)", + st->planned_bytes / 1024); + else + snprintf(buf, sizeof(buf), + "memory=" INT64_FORMAT "kB checked=%.0f rejected=%.0f", + st->planned_bytes / 1024, nchecked, nfiltered); + ExplainPropertyText("Bloom Filter Stats", buf, es); + + if (!st->pushed_down && nloops > 0) + ExplainPropertyFloat("Rows Removed by Bloom Filter", NULL, + nfiltered / nloops, 0, es); + } +} diff --git a/gpcontrib/anser/src/anserservice.c b/gpcontrib/anser/src/anserservice.c new file mode 100644 index 00000000000..980583fa3c4 --- /dev/null +++ b/gpcontrib/anser/src/anserservice.c @@ -0,0 +1,203 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserservice.c + * Coordinator-local Anser background services. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserservice.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "cdb/cdbvars.h" +#include "libpq/pqsignal.h" +#include "miscadmin.h" +#include "postmaster/bgworker.h" +#include "storage/fd.h" +#include "storage/ipc.h" +#include "storage/latch.h" +#include "storage/lwlock.h" +#include "utils/guc.h" +#include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/ps_status.h" +#include "utils/resowner.h" +#include "utils/wait_event.h" + +static volatile sig_atomic_t anser_service_got_sigterm = false; +static volatile sig_atomic_t anser_service_got_sighup = false; + +static void AnserServiceLoop(const char *service_name, bool gather_service); +static void AnserServiceSigHup(SIGNAL_ARGS); +static void AnserServiceSigTerm(SIGNAL_ARGS); + +bool +AnserStartRule(Datum main_arg) +{ + return gp_anser_enable && Gp_role == GP_ROLE_DISPATCH; +} + +void +AnserGatherServiceMain(Datum main_arg) +{ + AnserServiceLoop("anser gather service", true); +} + +void +AnserSendServiceMain(Datum main_arg) +{ + AnserServiceLoop("anser send service", false); +} + +static void +AnserServiceLoop(const char *service_name, bool gather_service) +{ + sigjmp_buf local_sigjmp_buf; + MemoryContext service_ctx; + + pqsignal(SIGHUP, AnserServiceSigHup); + pqsignal(SIGTERM, AnserServiceSigTerm); + BackgroundWorkerUnblockSignals(); + + init_ps_display(service_name); + ereport(LOG, + (errmsg_internal("%s started", service_name))); + + /* + * Do all per-cycle work in a dedicated context so error recovery can reset + * it, and under a resource owner so a failed cycle's attached/created DSM + * segments are reclaimed rather than leaked. + */ + service_ctx = AllocSetContextCreate(TopMemoryContext, "Anser service", + ALLOCSET_DEFAULT_SIZES); + MemoryContextSwitchTo(service_ctx); + if (CurrentResourceOwner == NULL) + CurrentResourceOwner = ResourceOwnerCreate(NULL, service_name); + + AnserAttachServiceLatch(gather_service); + + /* + * If a cycle raises an error, resume here: log it, drop whatever the cycle + * held, and carry on rather than terminating the worker. Modeled on the + * shmem-only auxiliary processes (see bgwriter.c); the leftmost sigsetjmp + * stays active so we can even survive an error during recovery. + */ + if (sigsetjmp(local_sigjmp_buf, 1) != 0) + { + /* Not using PG_TRY, so reset the error stack by hand. */ + error_context_stack = NULL; + + HOLD_INTERRUPTS(); + + EmitErrorReport(); + + /* Minimal subset of AbortTransaction() for a shmem-only worker. */ + LWLockReleaseAll(); + if (CurrentResourceOwner != NULL) + { + ResourceOwnerRelease(CurrentResourceOwner, + RESOURCE_RELEASE_BEFORE_LOCKS, false, false); + ResourceOwnerRelease(CurrentResourceOwner, + RESOURCE_RELEASE_LOCKS, false, false); + ResourceOwnerRelease(CurrentResourceOwner, + RESOURCE_RELEASE_AFTER_LOCKS, false, false); + } + + /* + * Release any hash_seq_search scan and temp files abandoned when the + * error interrupted a cycle mid-scan. Missing the hash-table reset here + * leaks dynahash scan registrations across errors until hash_seq_init + * itself fails, permanently wedging the service (it could then never + * scan the channel map to deliver to consumers). + */ + AtEOXact_Files(false); + AtEOXact_HashTables(false); + + MemoryContextSwitchTo(service_ctx); + FlushErrorState(); + MemoryContextResetAndDeleteChildren(service_ctx); + + RESUME_INTERRUPTS(); + + /* Do not spin on a persistent error. */ + pg_usleep(1000000L); + } + + /* We can now handle ereport(ERROR). */ + PG_exception_stack = &local_sigjmp_buf; + + while (!anser_service_got_sigterm) + { + if (anser_service_got_sighup) + { + anser_service_got_sighup = false; + ProcessConfigFile(PGC_SIGHUP); + } + + /* + * Run this service's data-path pass, then the shared orphan sweep. + * The gather service drains producer submissions and enforces the + * produce timeout; the send service delivers ready/cancelled channels + * to waiting consumers. The timed wakeup bounds how long a stale + * COLLECTING channel or a dead-backend slot lingers between latches. + */ + if (gather_service) + AnserGatherServiceCycle(); + else + AnserSendServiceCycle(); + + AnserServiceMaintenance(); + AnserWaitServiceLatch(gather_service, ANSER_SERVICE_WAKEUP_INTERVAL_MS); + + /* Reclaim any transient allocations made during this cycle. */ + MemoryContextReset(service_ctx); + } + + PG_exception_stack = NULL; + AnserDetachServiceLatch(gather_service); + + proc_exit(0); +} + +static void +AnserServiceSigHup(SIGNAL_ARGS) +{ + int save_errno = errno; + + anser_service_got_sighup = true; + AnserWakeServiceLatch(true); + AnserWakeServiceLatch(false); + SetLatch(MyLatch); + errno = save_errno; +} + +static void +AnserServiceSigTerm(SIGNAL_ARGS) +{ + int save_errno = errno; + + anser_service_got_sigterm = true; + AnserWakeServiceLatch(true); + AnserWakeServiceLatch(false); + SetLatch(MyLatch); + errno = save_errno; +} From f0682a89d59e50da8a08419989cce34257ed1cb5 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Thu, 3 Sep 2026 22:31:26 +0300 Subject: [PATCH 03/15] Feature: Add Anser regression tests and CI coverage anser_test is a second control file over the same library (its functions live in anser.so, so they act on the same shared state the services do) exposing the internal C API to the tests. It is test-only and superuser-gated; do not create it in production. Two regression files. anser_test covers the channel map and the network path end to end: producer/consumer accounting, the bloom part protocol and its in-place union, the produce timeout, the SQL functions driven through the live gather and send services, the libpq client helpers over loopback, session-token registration and rejection, multi-consumer partial delivery, and payload-DSM lifetime in the success, timeout and cancel cases. anser_runtime_filter covers the plan pass: the injected nodes appear (and disappear with the GUC off), and results are identical with the filter on and off under both optimizers, with pushdown, and for the datatype cases injection must refuse. The tests need the services live, which means postmaster-context settings, so installcheck first ensures shared_preload_libraries contains anser (appending, not overwriting) and anser.enable=on, then restarts the cluster. The CI matrix entry drives that through one target. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-cloudberry.yml | 14 + gpcontrib/anser/Makefile | 48 +- gpcontrib/anser/anser_test--1.0.sql | 152 ++ gpcontrib/anser/anser_test.control | 23 + .../anser/expected/anser_runtime_filter.out | 227 +++ gpcontrib/anser/expected/anser_test.out | 336 ++++ gpcontrib/anser/sql/anser_runtime_filter.sql | 147 ++ gpcontrib/anser/sql/anser_test.sql | 117 ++ gpcontrib/anser/src/anser_test.c | 1415 +++++++++++++++++ 9 files changed, 2475 insertions(+), 4 deletions(-) create mode 100644 gpcontrib/anser/anser_test--1.0.sql create mode 100644 gpcontrib/anser/anser_test.control create mode 100644 gpcontrib/anser/expected/anser_runtime_filter.out create mode 100644 gpcontrib/anser/expected/anser_test.out create mode 100644 gpcontrib/anser/sql/anser_runtime_filter.sql create mode 100644 gpcontrib/anser/sql/anser_test.sql create mode 100644 gpcontrib/anser/src/anser_test.c diff --git a/.github/workflows/build-cloudberry.yml b/.github/workflows/build-cloudberry.yml index b790d3411ec..6ec2f52ecba 100644 --- a/.github/workflows/build-cloudberry.yml +++ b/.github/workflows/build-cloudberry.yml @@ -243,6 +243,11 @@ jobs: "make_configs":["src/test/regress:installcheck-good"], "pg_settings":{"optimizer":"on"} }, + {"test":"ic-anser", + "make_configs":["gpcontrib/anser:installcheck-with-install"], + "shared_preload_libraries":"anser", + "postgres_conf_addons":"anser.enable=on" + }, {"test":"pax-ic-good-opt-off", "make_configs":[ "contrib/pax_storage/:pax-test", @@ -1500,6 +1505,15 @@ jobs: echo "Adding shared_preload_libraries: ${{ matrix.shared_preload_libraries }}" fi + # Append extra postgresql.conf settings requested by the matrix + # entry (e.g. postmaster-context GUCs a test module requires). + # BLDWRAP_POSTGRES_CONF_ADDONS is a pipe-separated list of + # key=value pairs (see gpAux/gpdemo/demo_cluster.sh). + if [[ -n "${{ matrix.postgres_conf_addons }}" ]]; then + EXTRA_CONF="${EXTRA_CONF}${EXTRA_CONF:+|}${{ matrix.postgres_conf_addons }}" + echo "Adding postgres_conf_addons: ${{ matrix.postgres_conf_addons }}" + fi + if ! time su - gpadmin -c "cd ${SRC_DIR} && NUM_PRIMARY_MIRROR_PAIRS='${{ matrix.num_primary_mirror_pairs }}' BLDWRAP_POSTGRES_CONF_ADDONS=\"${EXTRA_CONF}\" SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/create-cloudberry-demo-cluster.sh"; then echo "::error::Demo cluster creation failed" exit 1 diff --git a/gpcontrib/anser/Makefile b/gpcontrib/anser/Makefile index 6893ea4e772..5163c5c20c7 100644 --- a/gpcontrib/anser/Makefile +++ b/gpcontrib/anser/Makefile @@ -37,14 +37,21 @@ OBJS = \ src/anserinit.o \ src/anserplan.o \ src/anserplanexec.o \ - src/anserservice.o + src/anserservice.o \ + src/anser_test.o PGFILEDESC = "anser - adaptive information sharing runtime filters" -EXTENSION = anser -DATA = anser--1.0.sql +# anser_test exposes the internal C API to the regression tests; its functions +# live in the same library, so it is a second control file over $libdir/anser +# rather than a separate module. +EXTENSION = anser anser_test +DATA = anser--1.0.sql anser_test--1.0.sql -# src/anserclient.c opens libpq connections back to the coordinator. +REGRESS = anser_test anser_runtime_filter + +# src/anserclient.c opens libpq connections back to the coordinator, and +# src/anser_test.c drives loopback connections of its own. PG_CPPFLAGS = -I$(srcdir)/include -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) SHLIB_PREREQS = submake-libpq @@ -59,3 +66,36 @@ top_builddir = ../.. include $(top_builddir)/src/Makefile.global include $(top_srcdir)/contrib/contrib-global.mk endif + +# The tests need the live gather/send services, which only exist when the +# library is preloaded and anser.enable is on -- both postmaster-context +# settings. Ensure them (and restart) before installcheck, as +# gp_relsizes_stats does for its worker. +installcheck: preload-anser + +.PHONY: preload-anser +preload-anser: + @if [ -z "$$COORDINATOR_DATA_DIRECTORY$$MASTER_DATA_DIRECTORY" ]; then \ + echo "ERROR: COORDINATOR_DATA_DIRECTORY (or MASTER_DATA_DIRECTORY) is not set;" >&2; \ + echo " source cloudberry-env.sh and gpdemo-env.sh before running installcheck." >&2; \ + exit 1; \ + fi + @current=`psql -d postgres -At -c "SHOW shared_preload_libraries;" | tr -d ' '`; \ + case ",$$current," in \ + *,anser,*) \ + echo "==> shared_preload_libraries already contains anser" ;; \ + *) \ + if [ -z "$$current" ]; then new=anser; else new="$$current,anser"; fi; \ + echo "==> Adding anser to shared_preload_libraries ($$new)"; \ + gpconfig -c shared_preload_libraries -v "'$$new'" --skipvalidation ;; \ + esac + gpconfig -c anser.enable -v on --skipvalidation + gpstop -ra + psql -d postgres -c "SHOW shared_preload_libraries;" + +# CI runs a single make target per test config, so offer one target that +# installs the extension, arms the cluster, and runs the tests. +.PHONY: installcheck-with-install +installcheck-with-install: + $(MAKE) install + $(MAKE) installcheck diff --git a/gpcontrib/anser/anser_test--1.0.sql b/gpcontrib/anser/anser_test--1.0.sql new file mode 100644 index 00000000000..de8b54457a2 --- /dev/null +++ b/gpcontrib/anser/anser_test--1.0.sql @@ -0,0 +1,152 @@ +/* gpcontrib/anser/anser_test--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION anser_test" to load this file. \quit + +CREATE FUNCTION anser_test_register_condition( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + expected_producers int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_subscribe( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_publish( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + payload bytea, + cancelled bool) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_publish_value( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_consume( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + timeout_ms int4) +RETURNS bytea +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_consume_has( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text, + value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_state( + gp_session_id int4, + gp_command_count int4, + condition_id int4, + condition_key text) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_cancel_query( + gp_session_id int4, + gp_command_count int4) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_bloom_roundtrip( + condition_key text, + value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_bloom_fold_inplace() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_bloom_rejects_mismatch() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_node_roundtrip(value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_client_roundtrip(value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_token_roundtrip() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_multi_consumer(value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_abandoned_consumer_recycles(value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_dsm_free_on_success() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_dsm_free_on_timeout() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_dsm_free_on_cancel() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_set_sweep(enabled bool) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_sweep() +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C; + +CREATE FUNCTION anser_test_max_channels_stable_across_slices() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C; diff --git a/gpcontrib/anser/anser_test.control b/gpcontrib/anser/anser_test.control new file mode 100644 index 00000000000..e07633dd15d --- /dev/null +++ b/gpcontrib/anser/anser_test.control @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# anser_test extension +comment = 'test helpers for the anser extension' +default_version = '1.0' +module_pathname = '$libdir/anser' +relocatable = true +requires = 'anser' diff --git a/gpcontrib/anser/expected/anser_runtime_filter.out b/gpcontrib/anser/expected/anser_runtime_filter.out new file mode 100644 index 00000000000..21e1fc61ce7 --- /dev/null +++ b/gpcontrib/anser/expected/anser_runtime_filter.out @@ -0,0 +1,227 @@ +-- Anser runtime bloom filter: plan-tree integration (PR4). +-- +-- Requires the cluster to run with anser.enable=on so the gather/send +-- services are live. On a multi-segment cluster the build side redistributes, +-- exercising the Motion-directly-under-CustomScan case; on a single segment it +-- degrades to the leaf case. Either way the feature must (a) inject the +-- producer/consumer nodes and (b) never change query results. +-- The plan pass only injects when the extension is installed in this +-- database, since that is what makes the segments' callbacks resolvable. +CREATE EXTENSION anser; +-- Deterministic plan shape: force a hash join. +SET enable_nestloop = off; +SET enable_mergejoin = off; +-- build is the smaller (hashed / preserved) side, distributed so the join key +-- must be redistributed; probe is the larger side distributed by the join key. +CREATE TABLE anser_rf_build (id int, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe (id int, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build; +ANALYZE anser_rf_probe; +-- Use the Postgres planner for a deterministic plan shape. +SET optimizer = off; +-- With the filter on the plan carries an "Anser Bloom Producer" under the Hash +-- and an "Anser Bloom Consumer" above the probe scan (with the planned bloom +-- size); with it off neither node appears. (COSTS OFF keeps the output stable.) +SET anser.runtime_filter = on; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + QUERY PLAN +------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Right Join + Hash Cond: (p.id = b.id) + -> Custom Scan (Anser Bloom Consumer) + Bloom Filter Size: 1048576 bytes + -> Seq Scan on anser_rf_probe p + -> Hash + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: b.id + -> Custom Scan (Anser Bloom Producer) + Bloom Filter Size: 1048576 bytes + Bloom Filter Stats: memory=1024kB + -> Seq Scan on anser_rf_build b + Optimizer: Postgres query optimizer +(14 rows) + +SET anser.runtime_filter = off; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + QUERY PLAN +------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Right Join + Hash Cond: (p.id = b.id) + -> Seq Scan on anser_rf_probe p + -> Hash + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: b.id + -> Seq Scan on anser_rf_build b + Optimizer: Postgres query optimizer +(9 rows) + +-- Correctness: identical results with the filter on vs off (Postgres planner). +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_on AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_r_off AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS rows_on FROM anser_rf_r_on; + rows_on +--------- + 200 +(1 row) + +SELECT count(*) AS rows_off FROM anser_rf_r_off; + rows_off +---------- + 200 +(1 row) + +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_r_on EXCEPT ALL SELECT * FROM anser_rf_r_off) d; + only_on +--------- + 0 +(1 row) + +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_on) d; + only_off +---------- + 0 +(1 row) + +-- Correctness must also hold under ORCA (whether or not it injects the nodes). +SET optimizer = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_orca AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_orca + FROM (SELECT * FROM anser_rf_r_orca EXCEPT ALL SELECT * FROM anser_rf_r_off) d; + only_orca +----------- + 0 +(1 row) + +SELECT count(*) AS only_off_orca + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_orca) d; + only_off_orca +--------------- + 0 +(1 row) + +-- Pushdown mix: with gp_enable_runtime_filter_pushdown on, the consumer hands +-- the unioned filter to the probe SeqScan as an SK_BLOOM_FILTER scan key and +-- the scan (or the table AM) does the pruning. Results must still match the +-- filter-off run. +SET optimizer = off; +SET gp_enable_runtime_filter_pushdown = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_push AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_push + FROM (SELECT * FROM anser_rf_r_push EXCEPT ALL SELECT * FROM anser_rf_r_off) d; + only_push +----------- + 0 +(1 row) + +SELECT count(*) AS only_off_push + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_push) d; + only_off_push +--------------- + 0 +(1 row) + +RESET gp_enable_runtime_filter_pushdown; +DROP TABLE anser_rf_r_push; +DROP TABLE anser_rf_r_on, anser_rf_r_off, anser_rf_r_orca; +DROP TABLE anser_rf_build, anser_rf_probe; +-- Datatype guard: producer and consumer hash the raw Datum bytes, so injection +-- is restricted to keys where SQL equality is bitwise Datum equality. A +-- cross-type join (float4 vs float8 Datums for equal values differ) must not +-- be injected; results must match the filter-off run either way. +CREATE TABLE anser_rf_build_f (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_f (id float4, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_f SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe_f SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build_f; +ANALYZE anser_rf_probe_f; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_f_on AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_f_off AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS rows_on FROM anser_rf_f_on; + rows_on +--------- + 200 +(1 row) + +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_f_on EXCEPT ALL SELECT * FROM anser_rf_f_off) d; + only_on +--------- + 0 +(1 row) + +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_f_off EXCEPT ALL SELECT * FROM anser_rf_f_on) d; + only_off +---------- + 0 +(1 row) + +DROP TABLE anser_rf_f_on, anser_rf_f_off, anser_rf_build_f, anser_rf_probe_f; +-- Same-typed float keys are also excluded: -0.0 and 0.0 compare equal in SQL +-- but are not bitwise equal, so hashing raw Datums could prune a joinable row. +CREATE TABLE anser_rf_build_z (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_z (id float8, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_z VALUES (1, 'b1'), (-0.0, 'bz'); +INSERT INTO anser_rf_probe_z VALUES (1, 'p1'), (0.0, 'pz'), (2, 'p2'); +ANALYZE anser_rf_build_z; +ANALYZE anser_rf_probe_z; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_z_on AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_z_off AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS rows_on FROM anser_rf_z_on; + rows_on +--------- + 2 +(1 row) + +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_z_on EXCEPT ALL SELECT * FROM anser_rf_z_off) d; + only_on +--------- + 0 +(1 row) + +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_z_off EXCEPT ALL SELECT * FROM anser_rf_z_on) d; + only_off +---------- + 0 +(1 row) + +DROP TABLE anser_rf_z_on, anser_rf_z_off, anser_rf_build_z, anser_rf_probe_z; +RESET anser.runtime_filter; +RESET optimizer; +RESET enable_nestloop; +RESET enable_mergejoin; +DROP EXTENSION anser; diff --git a/gpcontrib/anser/expected/anser_test.out b/gpcontrib/anser/expected/anser_test.out new file mode 100644 index 00000000000..784e11df8d4 --- /dev/null +++ b/gpcontrib/anser/expected/anser_test.out @@ -0,0 +1,336 @@ +CREATE EXTENSION anser_test CASCADE; +NOTICE: installing required extension "anser" +-- Pause the background maintenance sweep so terminal (CANCELLED/CONSUMED) +-- channels stay observable and the state assertions below are deterministic +-- rather than racing the live gather/send services. Re-enabled at the end, +-- where we prove the sweep actually reclaims them. +SELECT anser_test_set_sweep(false); + anser_test_set_sweep +---------------------- + +(1 row) + +-- Happy path: one condition, two producers, two consumers. Each producer +-- publishes a real bloom part (multi-payload combine is bloom-only now); the two +-- parts union on the coordinator, so each consumer's received filter contains +-- both producers' values. +SELECT anser_test_register_condition(1, 1, 1, 'join_a', 2); + anser_test_register_condition +------------------------------- + t +(1 row) + +SELECT anser_test_subscribe(1, 1, 1, 'join_a'); + anser_test_subscribe +---------------------- + t +(1 row) + +SELECT anser_test_subscribe(1, 1, 1, 'join_a'); + anser_test_subscribe +---------------------- + t +(1 row) + +SELECT anser_test_publish_value(1, 1, 1, 'join_a', 10); + anser_test_publish_value +-------------------------- + t +(1 row) + +SELECT anser_test_state(1, 1, 1, 'join_a'); + anser_test_state +------------------ + COLLECTING +(1 row) + +SELECT anser_test_publish_value(1, 1, 1, 'join_a', 20); + anser_test_publish_value +-------------------------- + t +(1 row) + +SELECT anser_test_state(1, 1, 1, 'join_a'); + anser_test_state +------------------ + READY +(1 row) + +SELECT anser_test_consume_has(1, 1, 1, 'join_a', 10); + anser_test_consume_has +------------------------ + t +(1 row) + +SELECT anser_test_consume_has(1, 1, 1, 'join_a', 20); + anser_test_consume_has +------------------------ + t +(1 row) + +SELECT anser_test_state(1, 1, 1, 'join_a'); + anser_test_state +------------------ + CONSUMED +(1 row) + +-- Timeout/cancel path: no producer publishes. +SELECT anser_test_register_condition(1, 1, 2, 'join_timeout', 1); + anser_test_register_condition +------------------------------- + t +(1 row) + +SELECT anser_test_subscribe(1, 1, 2, 'join_timeout'); + anser_test_subscribe +---------------------- + t +(1 row) + +SELECT anser_test_consume(1, 1, 2, 'join_timeout', 1) IS NULL; + ?column? +---------- + t +(1 row) + +SELECT anser_test_state(1, 1, 2, 'join_timeout'); + anser_test_state +------------------ + CANCELLED +(1 row) + +-- Input validation: negative IDs/counts and overlong condition keys fail. +SELECT anser_test_register_condition(1, 1, -1, 'bad_id', 1); + anser_test_register_condition +------------------------------- + f +(1 row) + +SELECT anser_test_register_condition(1, 1, 3, 'bad_count', 0); + anser_test_register_condition +------------------------------- + f +(1 row) + +SELECT anser_test_register_condition(1, 1, 3, repeat('x', 64), 1); + anser_test_register_condition +------------------------------- + f +(1 row) + +SELECT anser_test_subscribe(1, 1, -1, 'bad_id'); + anser_test_subscribe +---------------------- + f +(1 row) + +SELECT anser_test_subscribe(1, 1, 3, repeat('x', 64)); + anser_test_subscribe +---------------------- + f +(1 row) + +-- Query-level cancellation touches all channels for the command. +SELECT anser_test_register_condition(1, 2, 1, 'join_b', 1); + anser_test_register_condition +------------------------------- + t +(1 row) + +SELECT anser_test_register_condition(1, 2, 2, 'join_c', 1); + anser_test_register_condition +------------------------------- + t +(1 row) + +SELECT anser_test_cancel_query(1, 2); + anser_test_cancel_query +------------------------- + +(1 row) + +SELECT anser_test_state(1, 2, 1, 'join_b'); + anser_test_state +------------------ + CANCELLED +(1 row) + +SELECT anser_test_state(1, 2, 2, 'join_c'); + anser_test_state +------------------ + CANCELLED +(1 row) + +-- GUC sizing guard: AnserMaxChannels() is memoized at postmaster start, so a +-- per-session SET gp_max_slices must not change the reported channel-map size +-- (otherwise the shared arrays and the Len functions would disagree). +SELECT anser_test_max_channels_stable_across_slices() AS max_channels_stable; + max_channels_stable +--------------------- + t +(1 row) + +-- Bloom payload protocol and standalone producer/consumer helpers. +SELECT anser_test_bloom_roundtrip('bf_roundtrip', 42); + anser_test_bloom_roundtrip +---------------------------- + t +(1 row) + +-- In-place fold: same-size union mutates the buffer; a mismatched size is +-- rejected. This is the coordinator's only combine path (first part is stored +-- verbatim, every later part folds in here). +SELECT anser_test_bloom_fold_inplace() AS fold_inplace_ok; + fold_inplace_ok +----------------- + t +(1 row) + +SELECT anser_test_node_roundtrip(168); + anser_test_node_roundtrip +--------------------------- + t +(1 row) + +-- Safety regression: the consumer rebuilds the filter from its own parameters and +-- requires the received bitset to be exactly the expected size (and the header +-- magic to match); truncated/oversized/corrupt parts are rejected (fail open). +SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; + reject_mismatch +----------------- + t +(1 row) + +-- SQL-function round trip through the live services: producer_begin -> publish +-- (gather service appends, channel goes READY) -> consume_wait (send service +-- delivers) returns the payload. Proves the full producer -> gather -> send -> +-- consumer chain, not just the map. +SELECT anser.producer_begin(10, 1, 1, 'svc_roundtrip', 1) AS begin_ok; + begin_ok +---------- + t +(1 row) + +SELECT anser.publish(10, 1, 1, 'svc_roundtrip', '\x6162'::bytea, false) AS publish_ok; + publish_ok +------------ + t +(1 row) + +SELECT encode(anser.consume_wait(10, 1, 1, 'svc_roundtrip'), 'escape') AS payload; + payload +--------- + ab +(1 row) + +-- Producer-begin timeout: begin arms the produce deadline (COLLECTING) but no +-- producer publishes; the gather maintenance pass cancels the whole dataset +-- after anser.timeout_ms, so the waiting consumer is delivered a cancel and +-- consume_wait returns NULL. +SELECT anser.producer_begin(11, 1, 1, 'svc_timeout', 1) AS begin_ok; + begin_ok +---------- + t +(1 row) + +SELECT anser_test_state(11, 1, 1, 'svc_timeout') AS state_after_begin; + state_after_begin +------------------- + COLLECTING +(1 row) + +SELECT anser.consume_wait(11, 1, 1, 'svc_timeout') IS NULL AS consume_cancelled; + consume_cancelled +------------------- + t +(1 row) + +-- Client helper loopback: drive AnserClientPublish / AnserClientConsumeWait +-- against the local coordinator over libpq, proving the client transport +-- end-to-end without a multi-node cluster. +SELECT anser_test_client_roundtrip(4242) AS client_ok; + client_ok +----------- + t +(1 row) + +-- Session token: registration, validation, and rejection of bogus token/user +-- (the token authenticates the segment -> QD backward connection). +SELECT anser_test_token_roundtrip() AS token_ok; + token_ok +---------- + t +(1 row) + +-- Multi-consumer partial delivery: two consumers block concurrently on one +-- channel (loopback libpq); one is cancelled mid-wait while the other still +-- receives the intact payload. Proves delivery is per-consumer. +SELECT anser_test_multi_consumer(24680) AS partial_delivery_ok; + partial_delivery_ok +--------------------- + t +(1 row) + +-- Regression guard (abandoned-consumer recycle): a consumer cancelled mid-wait +-- must stop counting toward the channel's expected consumers, so the channel +-- still recycles to CONSUMED after the surviving consumer is delivered instead +-- of lingering forever in READY with stale data. +SELECT anser_test_abandoned_consumer_recycles(13579) AS recycled_no_stale_data; + recycled_no_stale_data +------------------------ + t +(1 row) + +-- Clearing works: with the sweep paused, the cancelled channel above is still +-- present as CANCELLED. Re-enable the sweep and run one synchronously; the +-- terminal channel is then reclaimed (NOT_FOUND), proving maintenance clears it. +SELECT anser_test_state(1, 1, 2, 'join_timeout') AS before_clear; + before_clear +-------------- + CANCELLED +(1 row) + +SELECT anser_test_set_sweep(true); + anser_test_set_sweep +---------------------- + +(1 row) + +SELECT anser_test_sweep(); + anser_test_sweep +------------------ + +(1 row) + +SELECT anser_test_state(1, 1, 2, 'join_timeout') AS after_clear; + after_clear +------------- + NOT_FOUND +(1 row) + +-- Payload-DSM lifetime (run last: these toggle the sweep and reclaim terminal +-- channels). Each proves the shared channel payload DSM is freed at the right +-- moment: (1) success -> freed by the sweep after the last consume; (2) only +-- 3/5 producers -> freed when the produce timeout cancels the channel; (3) +-- cancelled with consumers attached -> freed by the sweep only after every +-- consumer slot has drained (not eagerly at cancel). +SELECT anser_test_dsm_free_on_success() AS dsm_free_success; + dsm_free_success +------------------ + t +(1 row) + +SELECT anser_test_dsm_free_on_timeout() AS dsm_free_timeout; + dsm_free_timeout +------------------ + t +(1 row) + +SELECT anser_test_dsm_free_on_cancel() AS dsm_free_cancel; + dsm_free_cancel +----------------- + t +(1 row) + +DROP EXTENSION anser_test; +DROP EXTENSION anser; diff --git a/gpcontrib/anser/sql/anser_runtime_filter.sql b/gpcontrib/anser/sql/anser_runtime_filter.sql new file mode 100644 index 00000000000..8e82779e2bb --- /dev/null +++ b/gpcontrib/anser/sql/anser_runtime_filter.sql @@ -0,0 +1,147 @@ +-- Anser runtime bloom filter: plan-tree integration (PR4). +-- +-- Requires the cluster to run with anser.enable=on so the gather/send +-- services are live. On a multi-segment cluster the build side redistributes, +-- exercising the Motion-directly-under-CustomScan case; on a single segment it +-- degrades to the leaf case. Either way the feature must (a) inject the +-- producer/consumer nodes and (b) never change query results. + +-- The plan pass only injects when the extension is installed in this +-- database, since that is what makes the segments' callbacks resolvable. +CREATE EXTENSION anser; + +-- Deterministic plan shape: force a hash join. +SET enable_nestloop = off; +SET enable_mergejoin = off; + +-- build is the smaller (hashed / preserved) side, distributed so the join key +-- must be redistributed; probe is the larger side distributed by the join key. +CREATE TABLE anser_rf_build (id int, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe (id int, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build; +ANALYZE anser_rf_probe; + +-- Use the Postgres planner for a deterministic plan shape. +SET optimizer = off; + +-- With the filter on the plan carries an "Anser Bloom Producer" under the Hash +-- and an "Anser Bloom Consumer" above the probe scan (with the planned bloom +-- size); with it off neither node appears. (COSTS OFF keeps the output stable.) +SET anser.runtime_filter = on; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + +SET anser.runtime_filter = off; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + +-- Correctness: identical results with the filter on vs off (Postgres planner). +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_on AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_r_off AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); + +SELECT count(*) AS rows_on FROM anser_rf_r_on; +SELECT count(*) AS rows_off FROM anser_rf_r_off; +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_r_on EXCEPT ALL SELECT * FROM anser_rf_r_off) d; +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_on) d; + +-- Correctness must also hold under ORCA (whether or not it injects the nodes). +SET optimizer = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_orca AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_orca + FROM (SELECT * FROM anser_rf_r_orca EXCEPT ALL SELECT * FROM anser_rf_r_off) d; +SELECT count(*) AS only_off_orca + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_orca) d; + +-- Pushdown mix: with gp_enable_runtime_filter_pushdown on, the consumer hands +-- the unioned filter to the probe SeqScan as an SK_BLOOM_FILTER scan key and +-- the scan (or the table AM) does the pruning. Results must still match the +-- filter-off run. +SET optimizer = off; +SET gp_enable_runtime_filter_pushdown = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_push AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_push + FROM (SELECT * FROM anser_rf_r_push EXCEPT ALL SELECT * FROM anser_rf_r_off) d; +SELECT count(*) AS only_off_push + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_push) d; +RESET gp_enable_runtime_filter_pushdown; + +DROP TABLE anser_rf_r_push; +DROP TABLE anser_rf_r_on, anser_rf_r_off, anser_rf_r_orca; +DROP TABLE anser_rf_build, anser_rf_probe; + +-- Datatype guard: producer and consumer hash the raw Datum bytes, so injection +-- is restricted to keys where SQL equality is bitwise Datum equality. A +-- cross-type join (float4 vs float8 Datums for equal values differ) must not +-- be injected; results must match the filter-off run either way. +CREATE TABLE anser_rf_build_f (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_f (id float4, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_f SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe_f SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build_f; +ANALYZE anser_rf_probe_f; + +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_f_on AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_f_off AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); + +SELECT count(*) AS rows_on FROM anser_rf_f_on; +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_f_on EXCEPT ALL SELECT * FROM anser_rf_f_off) d; +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_f_off EXCEPT ALL SELECT * FROM anser_rf_f_on) d; + +DROP TABLE anser_rf_f_on, anser_rf_f_off, anser_rf_build_f, anser_rf_probe_f; + +-- Same-typed float keys are also excluded: -0.0 and 0.0 compare equal in SQL +-- but are not bitwise equal, so hashing raw Datums could prune a joinable row. +CREATE TABLE anser_rf_build_z (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_z (id float8, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_z VALUES (1, 'b1'), (-0.0, 'bz'); +INSERT INTO anser_rf_probe_z VALUES (1, 'p1'), (0.0, 'pz'), (2, 'p2'); +ANALYZE anser_rf_build_z; +ANALYZE anser_rf_probe_z; + +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_z_on AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_z_off AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); + +SELECT count(*) AS rows_on FROM anser_rf_z_on; +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_z_on EXCEPT ALL SELECT * FROM anser_rf_z_off) d; +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_z_off EXCEPT ALL SELECT * FROM anser_rf_z_on) d; + +DROP TABLE anser_rf_z_on, anser_rf_z_off, anser_rf_build_z, anser_rf_probe_z; + +RESET anser.runtime_filter; +RESET optimizer; +RESET enable_nestloop; +RESET enable_mergejoin; + +DROP EXTENSION anser; diff --git a/gpcontrib/anser/sql/anser_test.sql b/gpcontrib/anser/sql/anser_test.sql new file mode 100644 index 00000000000..47a7f98a9b3 --- /dev/null +++ b/gpcontrib/anser/sql/anser_test.sql @@ -0,0 +1,117 @@ +CREATE EXTENSION anser_test CASCADE; + +-- Pause the background maintenance sweep so terminal (CANCELLED/CONSUMED) +-- channels stay observable and the state assertions below are deterministic +-- rather than racing the live gather/send services. Re-enabled at the end, +-- where we prove the sweep actually reclaims them. +SELECT anser_test_set_sweep(false); + +-- Happy path: one condition, two producers, two consumers. Each producer +-- publishes a real bloom part (multi-payload combine is bloom-only now); the two +-- parts union on the coordinator, so each consumer's received filter contains +-- both producers' values. +SELECT anser_test_register_condition(1, 1, 1, 'join_a', 2); +SELECT anser_test_subscribe(1, 1, 1, 'join_a'); +SELECT anser_test_subscribe(1, 1, 1, 'join_a'); +SELECT anser_test_publish_value(1, 1, 1, 'join_a', 10); +SELECT anser_test_state(1, 1, 1, 'join_a'); +SELECT anser_test_publish_value(1, 1, 1, 'join_a', 20); +SELECT anser_test_state(1, 1, 1, 'join_a'); +SELECT anser_test_consume_has(1, 1, 1, 'join_a', 10); +SELECT anser_test_consume_has(1, 1, 1, 'join_a', 20); +SELECT anser_test_state(1, 1, 1, 'join_a'); + +-- Timeout/cancel path: no producer publishes. +SELECT anser_test_register_condition(1, 1, 2, 'join_timeout', 1); +SELECT anser_test_subscribe(1, 1, 2, 'join_timeout'); +SELECT anser_test_consume(1, 1, 2, 'join_timeout', 1) IS NULL; +SELECT anser_test_state(1, 1, 2, 'join_timeout'); + +-- Input validation: negative IDs/counts and overlong condition keys fail. +SELECT anser_test_register_condition(1, 1, -1, 'bad_id', 1); +SELECT anser_test_register_condition(1, 1, 3, 'bad_count', 0); +SELECT anser_test_register_condition(1, 1, 3, repeat('x', 64), 1); +SELECT anser_test_subscribe(1, 1, -1, 'bad_id'); +SELECT anser_test_subscribe(1, 1, 3, repeat('x', 64)); + +-- Query-level cancellation touches all channels for the command. +SELECT anser_test_register_condition(1, 2, 1, 'join_b', 1); +SELECT anser_test_register_condition(1, 2, 2, 'join_c', 1); +SELECT anser_test_cancel_query(1, 2); +SELECT anser_test_state(1, 2, 1, 'join_b'); +SELECT anser_test_state(1, 2, 2, 'join_c'); + +-- GUC sizing guard: AnserMaxChannels() is memoized at postmaster start, so a +-- per-session SET gp_max_slices must not change the reported channel-map size +-- (otherwise the shared arrays and the Len functions would disagree). +SELECT anser_test_max_channels_stable_across_slices() AS max_channels_stable; + +-- Bloom payload protocol and standalone producer/consumer helpers. +SELECT anser_test_bloom_roundtrip('bf_roundtrip', 42); +-- In-place fold: same-size union mutates the buffer; a mismatched size is +-- rejected. This is the coordinator's only combine path (first part is stored +-- verbatim, every later part folds in here). +SELECT anser_test_bloom_fold_inplace() AS fold_inplace_ok; +SELECT anser_test_node_roundtrip(168); + +-- Safety regression: the consumer rebuilds the filter from its own parameters and +-- requires the received bitset to be exactly the expected size (and the header +-- magic to match); truncated/oversized/corrupt parts are rejected (fail open). +SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; + +-- SQL-function round trip through the live services: producer_begin -> publish +-- (gather service appends, channel goes READY) -> consume_wait (send service +-- delivers) returns the payload. Proves the full producer -> gather -> send -> +-- consumer chain, not just the map. +SELECT anser.producer_begin(10, 1, 1, 'svc_roundtrip', 1) AS begin_ok; +SELECT anser.publish(10, 1, 1, 'svc_roundtrip', '\x6162'::bytea, false) AS publish_ok; +SELECT encode(anser.consume_wait(10, 1, 1, 'svc_roundtrip'), 'escape') AS payload; + +-- Producer-begin timeout: begin arms the produce deadline (COLLECTING) but no +-- producer publishes; the gather maintenance pass cancels the whole dataset +-- after anser.timeout_ms, so the waiting consumer is delivered a cancel and +-- consume_wait returns NULL. +SELECT anser.producer_begin(11, 1, 1, 'svc_timeout', 1) AS begin_ok; +SELECT anser_test_state(11, 1, 1, 'svc_timeout') AS state_after_begin; +SELECT anser.consume_wait(11, 1, 1, 'svc_timeout') IS NULL AS consume_cancelled; + +-- Client helper loopback: drive AnserClientPublish / AnserClientConsumeWait +-- against the local coordinator over libpq, proving the client transport +-- end-to-end without a multi-node cluster. +SELECT anser_test_client_roundtrip(4242) AS client_ok; + +-- Session token: registration, validation, and rejection of bogus token/user +-- (the token authenticates the segment -> QD backward connection). +SELECT anser_test_token_roundtrip() AS token_ok; + +-- Multi-consumer partial delivery: two consumers block concurrently on one +-- channel (loopback libpq); one is cancelled mid-wait while the other still +-- receives the intact payload. Proves delivery is per-consumer. +SELECT anser_test_multi_consumer(24680) AS partial_delivery_ok; + +-- Regression guard (abandoned-consumer recycle): a consumer cancelled mid-wait +-- must stop counting toward the channel's expected consumers, so the channel +-- still recycles to CONSUMED after the surviving consumer is delivered instead +-- of lingering forever in READY with stale data. +SELECT anser_test_abandoned_consumer_recycles(13579) AS recycled_no_stale_data; + +-- Clearing works: with the sweep paused, the cancelled channel above is still +-- present as CANCELLED. Re-enable the sweep and run one synchronously; the +-- terminal channel is then reclaimed (NOT_FOUND), proving maintenance clears it. +SELECT anser_test_state(1, 1, 2, 'join_timeout') AS before_clear; +SELECT anser_test_set_sweep(true); +SELECT anser_test_sweep(); +SELECT anser_test_state(1, 1, 2, 'join_timeout') AS after_clear; + +-- Payload-DSM lifetime (run last: these toggle the sweep and reclaim terminal +-- channels). Each proves the shared channel payload DSM is freed at the right +-- moment: (1) success -> freed by the sweep after the last consume; (2) only +-- 3/5 producers -> freed when the produce timeout cancels the channel; (3) +-- cancelled with consumers attached -> freed by the sweep only after every +-- consumer slot has drained (not eagerly at cancel). +SELECT anser_test_dsm_free_on_success() AS dsm_free_success; +SELECT anser_test_dsm_free_on_timeout() AS dsm_free_timeout; +SELECT anser_test_dsm_free_on_cancel() AS dsm_free_cancel; + +DROP EXTENSION anser_test; +DROP EXTENSION anser; diff --git a/gpcontrib/anser/src/anser_test.c b/gpcontrib/anser/src/anser_test.c new file mode 100644 index 00000000000..bb33ef454d8 --- /dev/null +++ b/gpcontrib/anser/src/anser_test.c @@ -0,0 +1,1415 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anser_test.c + * SQL-callable test helpers for the Anser subsystem. + * + * IDENTIFICATION + * gpcontrib/anser/src/anser_test.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq-fe.h" + +#include "anser.h" +#include "anserbloom.h" +#include "anserclient.h" +#include "anserfilter.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "commands/dbcommands.h" +#include "fmgr.h" +#include "lib/bloomfilter.h" +#include "miscadmin.h" +#include "postmaster/postmaster.h" +#include "storage/latch.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/wait_event.h" +#include "varatt.h" + +/* + * Bloom sizing used by the test helpers. bloom_create floors every filter at + * 1 MB, so these are the smallest filters we can build; producer and consumer + * sides must pass the identical pair (that is the whole point of carrying the + * parameters in the node rather than on the wire). + */ +#define ANSER_TEST_ELEMS 32 +#define ANSER_TEST_MAX_PAYLOAD (1024 * 1024) + +PG_FUNCTION_INFO_V1(anser_test_register_condition); +PG_FUNCTION_INFO_V1(anser_test_subscribe); +PG_FUNCTION_INFO_V1(anser_test_publish); +PG_FUNCTION_INFO_V1(anser_test_publish_value); +PG_FUNCTION_INFO_V1(anser_test_consume); +PG_FUNCTION_INFO_V1(anser_test_consume_has); +PG_FUNCTION_INFO_V1(anser_test_state); +PG_FUNCTION_INFO_V1(anser_test_cancel_query); +PG_FUNCTION_INFO_V1(anser_test_bloom_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_bloom_fold_inplace); +PG_FUNCTION_INFO_V1(anser_test_bloom_rejects_mismatch); +PG_FUNCTION_INFO_V1(anser_test_node_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_client_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_token_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_multi_consumer); +PG_FUNCTION_INFO_V1(anser_test_abandoned_consumer_recycles); +PG_FUNCTION_INFO_V1(anser_test_dsm_free_on_success); +PG_FUNCTION_INFO_V1(anser_test_dsm_free_on_timeout); +PG_FUNCTION_INFO_V1(anser_test_dsm_free_on_cancel); +PG_FUNCTION_INFO_V1(anser_test_set_sweep); +PG_FUNCTION_INFO_V1(anser_test_sweep); +PG_FUNCTION_INFO_V1(anser_test_max_channels_stable_across_slices); + +static bool build_test_key(FunctionCallInfo fcinfo, AnserChannelKey *key); +static char *anser_make_test_part(const char *condition_key, int32 value, + Size *len_out); +static const char *state_to_string(AnserChannelState state); +static char *anser_loopback_host(void); +static PGconn *anser_open_consumer(const AnserChannelKey *key); +static bool anser_wait_consumer_count(const AnserChannelKey *key, int target); +static void anser_cancel_conn(PGconn *conn); +static bool anser_drain_until_idle(PGconn *conn); +static bool anser_consumer_got_payload(PGconn *conn, const unsigned char *expected, + Size expected_len); +static bool anser_consumer_returned_row(PGconn *conn); +static bool anser_wait_channel_consumed(const AnserChannelKey *key); + +Datum +anser_test_register_condition(PG_FUNCTION_ARGS) +{ + int32 gp_session_id = PG_GETARG_INT32(0); + int32 gp_command_count = PG_GETARG_INT32(1); + int32 condition_id_arg = PG_GETARG_INT32(2); + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); + int32 expected_producers_arg = PG_GETARG_INT32(4); + AnserChannelKey key; + + if (condition_id_arg < 0 || expected_producers_arg <= 0) + PG_RETURN_BOOL(false); + + if (strlen(condition_key) >= ANSER_CONDITION_KEY_SIZE) + PG_RETURN_BOOL(false); + + /* + * Drive the production registration entry point (AnserProducerBegin) rather + * than a test-only variant, so the state machine we exercise below is the + * one real producers use. + */ + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = gp_session_id; + key.gp_command_count = gp_command_count; + key.condition_id = (uint32) condition_id_arg; + strlcpy(key.condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); + + PG_RETURN_BOOL(AnserProducerBegin(&key, expected_producers_arg, + GetUserId(), superuser())); +} + +Datum +anser_test_subscribe(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + + if (!build_test_key(fcinfo, &key)) + PG_RETURN_BOOL(false); + + PG_RETURN_BOOL(AnserSubscribe(&key)); +} + +Datum +anser_test_publish(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + bytea *payload = PG_GETARG_BYTEA_PP(4); + bool cancelled = PG_GETARG_BOOL(5); + + if (!build_test_key(fcinfo, &key)) + PG_RETURN_BOOL(false); + + PG_RETURN_BOOL(AnserPublish(&key, + VARDATA_ANY(payload), + VARSIZE_ANY_EXHDR(payload), + cancelled)); +} + +/* + * Publish a real serialized bloom part carrying a single int value. Multiple + * producers on one channel each call this; the coordinator stores the first part + * and OR-folds the rest (all same size), so the merged filter contains every + * published value. Needed by the state-machine test that drives >1 producer: + * the coordinator only combines serialized bloom parts, which a raw SQL bytea + * literal cannot express. + */ +Datum +anser_test_publish_value(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + int32 value = PG_GETARG_INT32(4); + char *part; + Size len = 0; + bool ok; + + if (!build_test_key(fcinfo, &key)) + PG_RETURN_BOOL(false); + + part = anser_make_test_part(key.condition_key, value, &len); + if (part == NULL) + PG_RETURN_BOOL(false); + + ok = AnserPublish(&key, part, len, false); + pfree(part); + PG_RETURN_BOOL(ok); +} + +Datum +anser_test_consume(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + int32 timeout_arg = PG_GETARG_INT32(4); + char *buffer; + Size payload_len = 0; + bool cancelled = false; + bytea *result; + + if (timeout_arg < 0) + PG_RETURN_NULL(); + + if (!build_test_key(fcinfo, &key)) + PG_RETURN_NULL(); + + /* + * Consume through the production path -- AnserWaitReady + AnserConsumeReady, + * the same pair the executor's bloom consumer uses. The timeout argument is + * advisory here: a channel that never becomes READY is cancelled by the + * gather service's stale-channel sweep after anser.timeout_ms, which wakes + * this wait with cancelled = true (so a "timeout" returns NULL). + */ + if (!AnserWaitReady(&key, &cancelled) || cancelled) + PG_RETURN_NULL(); + + buffer = (char *) palloc((Size) gp_anser_max_info_size); + if (!AnserConsumeReady(&key, buffer, (Size) gp_anser_max_info_size, + &payload_len, &cancelled) || cancelled) + PG_RETURN_NULL(); + + result = (bytea *) palloc(VARHDRSZ + payload_len); + SET_VARSIZE(result, VARHDRSZ + payload_len); + if (payload_len > 0) + memcpy(VARDATA(result), buffer, payload_len); + + PG_RETURN_BYTEA_P(result); +} + +/* + * Consume the merged bloom payload and test membership of a single value. Like + * anser_test_consume, but rebuilds the filter from the shared (ANSER_TEST_ELEMS, + * ANSER_TEST_MAX_PAYLOAD, key-derived seed) parameters -- exactly how the real + * consumer node reconstructs it, with the parameters carried by the node rather + * than the wire. Returns true iff the value is present in the received filter. + */ +Datum +anser_test_consume_has(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + int32 value = PG_GETARG_INT32(4); + Datum d = Int32GetDatum(value); + char *buffer; + Size payload_len = 0; + bool cancelled = false; + bloom_filter *filter; + bool has; + + if (!build_test_key(fcinfo, &key)) + PG_RETURN_BOOL(false); + + if (!AnserWaitReady(&key, &cancelled) || cancelled) + PG_RETURN_BOOL(false); + + buffer = (char *) palloc((Size) gp_anser_max_info_size); + if (!AnserConsumeReady(&key, buffer, (Size) gp_anser_max_info_size, + &payload_len, &cancelled) || cancelled) + { + pfree(buffer); + PG_RETURN_BOOL(false); + } + + filter = AnserBloomDeserializePart(buffer, payload_len, + ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, + AnserBloomSeed(key.condition_key), + NULL, NULL); + pfree(buffer); + if (filter == NULL) + PG_RETURN_BOOL(false); + + has = !bloom_lacks_element(filter, (unsigned char *) &d, sizeof(Datum)); + bloom_free(filter); + PG_RETURN_BOOL(has); +} + +Datum +anser_test_state(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + bool found = false; + AnserChannelState state; + + if (!build_test_key(fcinfo, &key)) + PG_RETURN_TEXT_P(cstring_to_text("NOT_FOUND")); + + state = AnserChannelGetState(&key, &found); + if (!found) + PG_RETURN_TEXT_P(cstring_to_text("NOT_FOUND")); + + PG_RETURN_TEXT_P(cstring_to_text(state_to_string(state))); +} + +Datum +anser_test_cancel_query(PG_FUNCTION_ARGS) +{ + int32 gp_session_id = PG_GETARG_INT32(0); + int32 gp_command_count = PG_GETARG_INT32(1); + + AnserCancelQuery(gp_session_id, gp_command_count); + PG_RETURN_VOID(); +} + +Datum +anser_test_bloom_roundtrip(PG_FUNCTION_ARGS) +{ + char *key = text_to_cstring(PG_GETARG_TEXT_PP(0)); + int32 value_arg = PG_GETARG_INT32(1); + Datum value = Int32GetDatum(value_arg); + uint64 seed = AnserBloomSeed(key); + bloom_filter *filter; + bloom_filter *roundtrip; + char *payload; + Size payload_size; + Size payload_len = 0; + uint32 part_index = 0; + uint32 total_parts = 0; + bool lacks; + + filter = AnserBloomCreate(32, 1024 * 1024, seed); + if (filter == NULL) + PG_RETURN_BOOL(false); + + bloom_add_element(filter, (unsigned char *) &value, sizeof(Datum)); + payload_size = AnserBloomSerializedSize(filter); + payload = palloc(payload_size); + if (!AnserBloomSerializePart(filter, 0, 1, payload, payload_size, + &payload_len)) + PG_RETURN_BOOL(false); + + roundtrip = AnserBloomDeserializePart(payload, payload_len, + 32, 1024 * 1024, seed, + &part_index, &total_parts); + if (roundtrip == NULL) + PG_RETURN_BOOL(false); + + lacks = bloom_lacks_element(roundtrip, (unsigned char *) &value, + sizeof(Datum)); + bloom_free(filter); + bloom_free(roundtrip); + PG_RETURN_BOOL(!lacks && part_index == 0 && total_parts == 1); +} + +/* + * In-place fold: folding an equally-sized part into a merged part is a bitwise + * OR of the bitset plus a fold-count bump, mutating the buffer without realloc. + * This is the coordinator's only combine path: the first part is stored + * verbatim, every later part folds in here. A differently-sized part is + * rejected and leaves the accumulator untouched. + */ +Datum +anser_test_bloom_fold_inplace(PG_FUNCTION_ARGS) +{ + uint64 seed = AnserBloomSeed("inplace_bloom"); + bloom_filter *left; + bloom_filter *right; + bloom_filter *big; + bloom_filter *merged; + Datum left_value = Int32GetDatum(7); + Datum right_value = Int32GetDatum(9); + char *acc; + char *part; + char *big_part; + Size acc_size; + Size part_size; + Size big_size; + Size acc_len = 0; + Size part_len = 0; + Size big_len = 0; + uint32 part_index = 0; + uint32 total_parts = 0; + uint32 tp_before = 0; + uint32 tp_after = 0; + bool same_ok; + bool mismatch_rejected; + + /* Two same-parameter parts: acc is the running merged part, part folds in. */ + left = AnserBloomCreate(32, 1024 * 1024, seed); + right = AnserBloomCreate(32, 1024 * 1024, seed); + if (left == NULL || right == NULL) + PG_RETURN_BOOL(false); + bloom_add_element(left, (unsigned char *) &left_value, sizeof(Datum)); + bloom_add_element(right, (unsigned char *) &right_value, sizeof(Datum)); + acc_size = AnserBloomSerializedSize(left); + part_size = AnserBloomSerializedSize(right); + acc = palloc(acc_size); + part = palloc(part_size); + if (!AnserBloomSerializePart(left, 0, 1, acc, acc_size, &acc_len) || + !AnserBloomSerializePart(right, 0, 1, part, part_size, &part_len)) + { + bloom_free(left); + bloom_free(right); + PG_RETURN_BOOL(false); + } + bloom_free(left); + bloom_free(right); + + same_ok = AnserBloomFoldPartInPlace(acc, acc_len, part, part_len); + merged = same_ok ? + AnserBloomDeserializePart(acc, acc_len, 32, 1024 * 1024, seed, + &part_index, &total_parts) : NULL; + same_ok = same_ok && + acc_len == acc_size && /* size unchanged, folded in place */ + merged != NULL && + part_index == 0 && + total_parts == 2 && /* one more part folded */ + !bloom_lacks_element(merged, (unsigned char *) &left_value, + sizeof(Datum)) && + !bloom_lacks_element(merged, (unsigned char *) &right_value, + sizeof(Datum)); + if (merged != NULL) + bloom_free(merged); + + /* + * A differently-sized part must be rejected and leave acc untouched. Since + * bloom_create floors every filter at 1 MB, we need a genuinely larger + * cardinality/budget to get a bigger (2 MB) bitset than the 1 MB acc. + */ + big = AnserBloomCreate(1500000, 4 * 1024 * 1024, seed); + if (big == NULL) + PG_RETURN_BOOL(false); + big_size = AnserBloomSerializedSize(big); + big_part = palloc(big_size); + if (!AnserBloomSerializePart(big, 0, 1, big_part, big_size, &big_len)) + { + bloom_free(big); + PG_RETURN_BOOL(false); + } + bloom_free(big); + + tp_before = ((const AnserBloomPartHeader *) acc)->total_parts; + mismatch_rejected = big_len != acc_len && + !AnserBloomFoldPartInPlace(acc, acc_len, big_part, big_len); + tp_after = ((const AnserBloomPartHeader *) acc)->total_parts; + mismatch_rejected = mismatch_rejected && tp_before == tp_after; + + PG_RETURN_BOOL(same_ok && mismatch_rejected); +} + +/* + * Safety regression for the size/format check in AnserBloomDeserializePart. + * + * The consumer rebuilds the filter from its OWN (total_elems, max_payload, seed) + * parameters, then requires the received bitset to be exactly the size those + * parameters imply and the wire header to carry the expected magic. A + * well-formed part must load; a truncated one, an oversized one, and one with a + * corrupted magic must all be rejected (NULL) so the consumer fails open rather + * than loading a wrongly-shaped bitset. Returns true iff the good part loads and + * every bad one is rejected. + */ +Datum +anser_test_bloom_rejects_mismatch(PG_FUNCTION_ARGS) +{ + uint64 seed = AnserBloomSeed("reject_mismatch"); + bloom_filter *filter; + char *good; + Size good_size; + Size good_len = 0; + bloom_filter *ok_load; + bloom_filter *short_load; + bloom_filter *long_load; + bloom_filter *magic_load; + AnserBloomPartHeader *hdr; + uint32 saved_magic; + bool ok; + + filter = AnserBloomCreate(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, seed); + if (filter == NULL) + PG_RETURN_BOOL(false); + + good_size = AnserBloomSerializedSize(filter); + good = palloc(good_size); + if (!AnserBloomSerializePart(filter, 0, 1, good, good_size, &good_len)) + { + bloom_free(filter); + PG_RETURN_BOOL(false); + } + bloom_free(filter); + + /* Well-formed: loads. */ + ok_load = AnserBloomDeserializePart(good, good_len, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, seed, NULL, NULL); + + /* One byte short of the expected bitset: rejected. */ + short_load = AnserBloomDeserializePart(good, good_len - 1, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, seed, NULL, NULL); + + /* Claiming more bytes than the expected bitset: rejected. */ + long_load = AnserBloomDeserializePart(good, good_len + 1, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, seed, NULL, NULL); + + /* Corrupted wire magic: rejected before the size check. */ + hdr = (AnserBloomPartHeader *) good; + saved_magic = hdr->magic; + hdr->magic = saved_magic ^ 0xFFFFFFFFU; + magic_load = AnserBloomDeserializePart(good, good_len, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, seed, NULL, NULL); + hdr->magic = saved_magic; + + ok = ok_load != NULL && short_load == NULL && long_load == NULL && + magic_load == NULL; + + if (ok_load != NULL) + bloom_free(ok_load); + if (short_load != NULL) + bloom_free(short_load); + if (long_load != NULL) + bloom_free(long_load); + if (magic_load != NULL) + bloom_free(magic_load); + pfree(good); + + PG_RETURN_BOOL(ok); +} + +Datum +anser_test_node_roundtrip(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + AnserBloomFilterProduceState *producer; + AnserBloomFilterConsumeState *consumer; + int32 value_arg = PG_GETARG_INT32(0); + Datum value = Int32GetDatum(value_arg); + bool ok; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 99; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "node_roundtrip", ANSER_CONDITION_KEY_SIZE); + if (!AnserProducerBegin(&key, 1, GetUserId(), superuser())) + PG_RETURN_BOOL(false); + if (!AnserSubscribe(&key)) + PG_RETURN_BOOL(false); + + producer = ExecInitAnserBloomFilterProduce(&key, 32, 1024 * 1024, 0, 1, + NULL); + if (producer == NULL) + PG_RETURN_BOOL(false); + ExecAnserBloomFilterProduceAddDatum(producer, value, false); + ok = ExecAnserBloomFilterProducePublish(producer); + ExecEndAnserBloomFilterProduce(producer); + if (!ok) + PG_RETURN_BOOL(false); + + consumer = ExecInitAnserBloomFilterConsume(&key, 32, 1024 * 1024, 1, + NULL); + if (consumer == NULL) + PG_RETURN_BOOL(false); + ok = ExecAnserBloomFilterConsume(consumer, 1000) && + ExecAnserBloomFilterConsumerGetFilter(consumer) != NULL && + ExecAnserBloomFilterConsumerReceivedParts(consumer) == 1 && + !ExecAnserBloomFilterConsumerWasCancelled(consumer) && + !bloom_lacks_element(ExecAnserBloomFilterConsumerGetFilter(consumer), + (unsigned char *) &value, sizeof(Datum)); + ExecEndAnserBloomFilterConsume(consumer); + PG_RETURN_BOOL(ok); +} + +/* + * Drive the libpq client helpers against our own coordinator (loopback), proving + * the AnserClient* path end-to-end without a multi-node cluster. The helpers + * read the QD address from qdHostname/qdPostmasterPort, which are blank on the + * coordinator itself, so we point them at the local postmaster for the duration + * of the call and restore them afterward. + */ +Datum +anser_test_client_roundtrip(PG_FUNCTION_ARGS) +{ + int32 value_arg = PG_GETARG_INT32(0); + char *saved_host = qdHostname; + int saved_port = qdPostmasterPort; + AnserChannelKey key; + unsigned char payload[sizeof(int32)]; + void *out = NULL; + Size out_len = 0; + bool cancelled = false; + bool ok = false; + + qdHostname = anser_loopback_host(); + qdPostmasterPort = PostPortNumber; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 20; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "client_loopback", ANSER_CONDITION_KEY_SIZE); + + memcpy(payload, &value_arg, sizeof(payload)); + + PG_TRY(); + { + if (AnserClientPublish(&key, 1, payload, sizeof(payload), false, NULL) && + AnserClientConsumeWait(&key, &out, &out_len, &cancelled, NULL) && + !cancelled && + out_len == sizeof(payload) && + memcmp(out, payload, out_len) == 0) + ok = true; + } + PG_FINALLY(); + { + qdHostname = saved_host; + qdPostmasterPort = saved_port; + } + PG_END_TRY(); + + if (out != NULL) + pfree(out); + + PG_RETURN_BOOL(ok); +} + +/* + * Session-token round trip: register this session's token, prove it validates + * for this session user, that a bogus token and a bogus user are rejected, and + * that a second call returns the same token (one token per session). + */ +Datum +anser_test_token_roundtrip(PG_FUNCTION_ARGS) +{ + Oid user = GetSessionUserId(); + char *token = AnserGetOrCreateSessionToken(user); + char *again; + bool ok; + + if (token == NULL) + PG_RETURN_BOOL(false); + + ok = AnserSessionTokenIsValid(user, token) && + !AnserSessionTokenIsValid(user, "00000000000000000000000000000000") && + !AnserSessionTokenIsValid(InvalidOid, token); + + again = AnserGetOrCreateSessionToken(user); + ok = ok && again != NULL && strcmp(again, token) == 0; + + PG_RETURN_BOOL(ok); +} + +/* + * Best loopback target for a libpq connection to our own postmaster: the first + * configured Unix-socket directory when available (avoids TCP/hba surprises), + * otherwise "localhost". + */ +static char * +anser_loopback_host(void) +{ + const char *sockdirs = GetConfigOption("unix_socket_directories", true, false); + + if (sockdirs != NULL && sockdirs[0] == '/') + { + const char *comma = strchr(sockdirs, ','); + Size len = comma != NULL ? (Size) (comma - sockdirs) : strlen(sockdirs); + + return pnstrdup(sockdirs, len); + } + + return pstrdup("localhost"); +} + +/* + * Multi-consumer partial delivery. + * + * Two consumers block concurrently on the same channel (real libpq loopback + * connections to our own coordinator). One is cancelled mid-wait, standing in + * for a broken consumer connection; the other keeps waiting. We then publish + * the payload and assert that the survivor receives it intact while the + * cancelled consumer got no data -- proving delivery is per-consumer, not + * all-or-nothing across consumers. + */ +Datum +anser_test_multi_consumer(PG_FUNCTION_ARGS) +{ + int32 value_arg = PG_GETARG_INT32(0); + char *saved_host = qdHostname; + int saved_port = qdPostmasterPort; + AnserChannelKey key; + unsigned char payload[sizeof(int32)]; + PGconn *keep = NULL; + PGconn *lost = NULL; + bool ok = false; + + qdHostname = anser_loopback_host(); + qdPostmasterPort = PostPortNumber; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 31; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "multi_consumer", ANSER_CONDITION_KEY_SIZE); + + memcpy(payload, &value_arg, sizeof(payload)); + + PG_TRY(); + { + /* Producer announces the channel (one producer expected). */ + if (AnserProducerBegin(&key, 1, GetUserId(), superuser())) + { + keep = anser_open_consumer(&key); + lost = anser_open_consumer(&key); + + /* Publish only once both consumers have registered wait slots. */ + if (keep != NULL && lost != NULL && + anser_wait_consumer_count(&key, 2)) + { + bool lost_failed; + + /* + * Break the "lost" consumer mid-wait and let its cancel fully + * resolve before publishing, so the send service can never race + * a delivery into it. + */ + anser_cancel_conn(lost); + lost_failed = !anser_consumer_returned_row(lost); + + if (lost_failed && + AnserPublish(&key, payload, sizeof(payload), false)) + ok = anser_consumer_got_payload(keep, payload, + sizeof(payload)); + } + } + } + PG_FINALLY(); + { + if (keep != NULL) + PQfinish(keep); + if (lost != NULL) + PQfinish(lost); + qdHostname = saved_host; + qdPostmasterPort = saved_port; + } + PG_END_TRY(); + + PG_RETURN_BOOL(ok); +} + +/* + * Regression guard: an abandoned consumer must not block channel recycling. + * + * Same shape as anser_test_multi_consumer, but the assertion is specifically + * that the channel does NOT leave stale data behind: after one consumer is + * cancelled mid-wait and the surviving consumers are delivered, the channel + * must recycle to CONSUMED. The cancelled consumer must not count toward the + * expected consumer total, or done_consumers would never catch up and the + * channel would wedge in READY forever (never reclaimable); this helper would + * then time out waiting for CONSUMED and return false. + */ +Datum +anser_test_abandoned_consumer_recycles(PG_FUNCTION_ARGS) +{ + int32 value_arg = PG_GETARG_INT32(0); + char *saved_host = qdHostname; + int saved_port = qdPostmasterPort; + AnserChannelKey key; + unsigned char payload[sizeof(int32)]; + int nseg = getgpsegmentCount(); + PGconn **keep; + PGconn *lost = NULL; + int i; + bool ok = false; + + if (nseg < 1) + nseg = 1; + + qdHostname = anser_loopback_host(); + qdPostmasterPort = PostPortNumber; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 32; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "abandon_recycle", ANSER_CONDITION_KEY_SIZE); + + memcpy(payload, &value_arg, sizeof(payload)); + + /* + * A channel recycles to CONSUMED once expected_consumers (== segment count, + * one consumer per segment) have been delivered. Open exactly that many + * surviving consumers plus one that abandons mid-wait: the abandoned one must + * neither receive data nor block the recycle once the survivors are served. + */ + keep = (PGconn **) palloc0(sizeof(PGconn *) * nseg); + + PG_TRY(); + { + bool all_open = true; + + if (AnserProducerBegin(&key, 1, GetUserId(), superuser())) + { + for (i = 0; i < nseg; i++) + { + keep[i] = anser_open_consumer(&key); + if (keep[i] == NULL) + all_open = false; + } + lost = anser_open_consumer(&key); + + if (all_open && lost != NULL && + anser_wait_consumer_count(&key, nseg + 1)) + { + anser_cancel_conn(lost); + (void) anser_consumer_returned_row(lost); + + if (AnserPublish(&key, payload, sizeof(payload), false)) + { + bool all_got = true; + + for (i = 0; i < nseg; i++) + { + if (!anser_consumer_got_payload(keep[i], payload, + sizeof(payload))) + all_got = false; + } + if (all_got) + ok = anser_wait_channel_consumed(&key); + } + } + } + } + PG_FINALLY(); + { + for (i = 0; i < nseg; i++) + if (keep[i] != NULL) + PQfinish(keep[i]); + if (lost != NULL) + PQfinish(lost); + qdHostname = saved_host; + qdPostmasterPort = saved_port; + } + PG_END_TRY(); + + PG_RETURN_BOOL(ok); +} + +/* + * Payload-DSM lifetime, scenario (1): 5 producers, N (= segment count) consumers, + * successful delivery. The shared payload DSM must survive past the last consume + * (the recycle to CONSUMED does not free it) and be released only when the sweep + * reclaims the drained channel. We keep the sweep paused to observe the deferred + * state, then sweep explicitly. + */ +Datum +anser_test_dsm_free_on_success(PG_FUNCTION_ARGS) +{ + char *saved_host = qdHostname; + int saved_port = qdPostmasterPort; + AnserChannelKey key; + char *part; + Size part_len = 0; + int nseg = getgpsegmentCount(); + PGconn **cons; + int i; + bool all_read = true; + bool present_after_consume = false; + bool gone_after_sweep = false; + + if (nseg < 1) + nseg = 1; + + AnserSetSweepEnabled(false); /* observe the deferred free ourselves */ + qdHostname = anser_loopback_host(); + qdPostmasterPort = PostPortNumber; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 40; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "dsm_success", ANSER_CONDITION_KEY_SIZE); + part = anser_make_test_part(key.condition_key, 7, &part_len); + + cons = (PGconn **) palloc0(sizeof(PGconn *) * nseg); + + PG_TRY(); + { + bool ready = false; + + /* 5 producers publish until the channel is READY (payload allocated). */ + if (part != NULL && AnserProducerBegin(&key, 5, GetUserId(), superuser())) + { + int p; + + ready = true; + for (p = 0; p < 5; p++) + if (!AnserPublish(&key, part, part_len, false)) + ready = false; + } + + if (ready) + { + bool all_open = true; + + for (i = 0; i < nseg; i++) + { + cons[i] = anser_open_consumer(&key); + if (cons[i] == NULL) + all_open = false; + } + + if (all_open && anser_wait_consumer_count(&key, nseg)) + { + /* Every consumer receives and copies out the shared payload. */ + for (i = 0; i < nseg; i++) + if (!anser_consumer_returned_row(cons[i])) + all_read = false; + + /* Consumed, but the payload DSM is still pinned (freed by sweep). */ + present_after_consume = AnserChannelPayloadBytes(&key) > 0; + + AnserSetSweepEnabled(true); + AnserServiceMaintenance(); + AnserSetSweepEnabled(false); + + gone_after_sweep = AnserChannelPayloadBytes(&key) < 0; + } + } + } + PG_FINALLY(); + { + /* sweep_enabled is shared postmaster-wide state: always restore it. */ + AnserSetSweepEnabled(true); + for (i = 0; i < nseg; i++) + if (cons[i] != NULL) + PQfinish(cons[i]); + qdHostname = saved_host; + qdPostmasterPort = saved_port; + } + PG_END_TRY(); + + PG_RETURN_BOOL(all_read && present_after_consume && gone_after_sweep); +} + +/* + * Payload-DSM lifetime, scenario (2): only 3 of 5 producers publish, so the + * channel never reaches READY. It stays COLLECTING with a partial payload until + * the produce deadline elapses, at which point the gather maintenance cancels it + * and frees the payload. (Consumers are omitted: a COLLECTING channel is never + * delivered, so nothing borrows the payload -- the free needs no consumers.) + */ +Datum +anser_test_dsm_free_on_timeout(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + char *part; + Size part_len = 0; + char saved_timeout[32]; + bool present_collecting = false; + bool freed_after_timeout = false; + + AnserSetSweepEnabled(false); + snprintf(saved_timeout, sizeof(saved_timeout), "%d", gp_anser_timeout_ms); + SetConfigOption("anser.timeout_ms", "100", PGC_USERSET, PGC_S_SESSION); + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 41; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "dsm_timeout", ANSER_CONDITION_KEY_SIZE); + part = anser_make_test_part(key.condition_key, 5, &part_len); + + PG_TRY(); + { + int p; + + if (part != NULL && AnserProducerBegin(&key, 5, GetUserId(), superuser())) + { + for (p = 0; p < 3; p++) + (void) AnserPublish(&key, part, part_len, false); + + present_collecting = AnserChannelPayloadBytes(&key) > 0; + + /* + * Past the produce deadline the gather maintenance cancels the + * still-COLLECTING channel and frees its partial payload. Drive one + * gather cycle after the timeout so this is deterministic. + */ + pg_usleep(200000L); /* 200 ms > anser.timeout_ms (100 ms) */ + AnserGatherServiceCycle(); + + freed_after_timeout = AnserChannelPayloadBytes(&key) <= 0; + } + } + PG_FINALLY(); + { + SetConfigOption("anser.timeout_ms", saved_timeout, + PGC_USERSET, PGC_S_SESSION); + /* sweep_enabled is shared postmaster-wide state: always restore it. */ + AnserSetSweepEnabled(true); + AnserServiceMaintenance(); /* reclaim the cancelled entry */ + } + PG_END_TRY(); + + PG_RETURN_BOOL(present_collecting && freed_after_timeout); +} + +/* + * Payload-DSM lifetime, scenario (3): 5 producers, N consumers, then the query is + * cancelled while consumers are attached. The cancel must NOT free the payload + * DSM (a consumer may still be borrowing it); it is released only after every + * consumer slot has drained and the sweep reclaims the channel. + */ +Datum +anser_test_dsm_free_on_cancel(PG_FUNCTION_ARGS) +{ + char *saved_host = qdHostname; + int saved_port = qdPostmasterPort; + AnserChannelKey key; + char *part; + Size part_len = 0; + int nseg = getgpsegmentCount(); + PGconn **cons; + int i; + bool present_after_cancel = false; + bool gone_after_sweep = false; + + if (nseg < 1) + nseg = 1; + + AnserSetSweepEnabled(false); + qdHostname = anser_loopback_host(); + qdPostmasterPort = PostPortNumber; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = 42; + key.gp_command_count = 1; + key.condition_id = 1; + strlcpy(key.condition_key, "dsm_cancel", ANSER_CONDITION_KEY_SIZE); + part = anser_make_test_part(key.condition_key, 9, &part_len); + + cons = (PGconn **) palloc0(sizeof(PGconn *) * nseg); + + PG_TRY(); + { + bool ready = false; + + if (part != NULL && AnserProducerBegin(&key, 5, GetUserId(), superuser())) + { + int p; + + ready = true; + for (p = 0; p < 5; p++) + if (!AnserPublish(&key, part, part_len, false)) + ready = false; + } + + if (ready) + { + bool all_open = true; + + for (i = 0; i < nseg; i++) + { + cons[i] = anser_open_consumer(&key); + if (cons[i] == NULL) + all_open = false; + } + + if (all_open && anser_wait_consumer_count(&key, nseg)) + { + /* + * Cancel with consumers attached. This marks the channel + * cancelled but must leave the payload DSM pinned -- a consumer + * may still be borrowing it -- so it is still present right after. + */ + AnserCancelQuery(key.gp_session_id, key.gp_command_count); + present_after_cancel = AnserChannelPayloadBytes(&key) > 0; + + /* Drain every consumer (each reads its copy or gets cancelled). */ + for (i = 0; i < nseg; i++) + (void) anser_consumer_returned_row(cons[i]); + + /* Slots drained: the sweep may now reclaim and free the payload. */ + AnserSetSweepEnabled(true); + AnserServiceMaintenance(); + AnserSetSweepEnabled(false); + + gone_after_sweep = AnserChannelPayloadBytes(&key) < 0; + } + } + } + PG_FINALLY(); + { + /* sweep_enabled is shared postmaster-wide state: always restore it. */ + AnserSetSweepEnabled(true); + for (i = 0; i < nseg; i++) + if (cons[i] != NULL) + PQfinish(cons[i]); + qdHostname = saved_host; + qdPostmasterPort = saved_port; + } + PG_END_TRY(); + + PG_RETURN_BOOL(present_after_cancel && gone_after_sweep); +} + +/* + * Open a loopback connection to our coordinator and fire anser.consume_wait + * asynchronously (binary result), leaving the connection blocked server-side. + */ +static PGconn * +anser_open_consumer(const AnserChannelKey *key) +{ + const char *keywords[5]; + const char *values[5]; + const char *params[4]; + char portstr[12]; + char ssid[12]; + char ccnt[12]; + char condid[12]; + PGconn *conn; + int n = 0; + + snprintf(portstr, sizeof(portstr), "%d", qdPostmasterPort); + + keywords[n] = "host"; + values[n] = qdHostname; + n++; + keywords[n] = "port"; + values[n] = portstr; + n++; + keywords[n] = "dbname"; + values[n] = get_database_name(MyDatabaseId); + n++; + keywords[n] = "user"; + values[n] = GetUserNameFromId(GetUserId(), false); + n++; + keywords[n] = NULL; + values[n] = NULL; + + conn = PQconnectdbParams(keywords, values, false); + if (conn == NULL) + return NULL; + if (PQstatus(conn) != CONNECTION_OK) + { + PQfinish(conn); + return NULL; + } + + snprintf(ssid, sizeof(ssid), "%d", key->gp_session_id); + snprintf(ccnt, sizeof(ccnt), "%d", key->gp_command_count); + snprintf(condid, sizeof(condid), "%d", (int) key->condition_id); + params[0] = ssid; + params[1] = ccnt; + params[2] = condid; + params[3] = key->condition_key; + + if (!PQsendQueryParams(conn, + "SELECT anser.consume_wait($1::int4, $2::int4, $3::int4, $4::text)", + 4, NULL, params, NULL, NULL, 1)) + { + PQfinish(conn); + return NULL; + } + + return conn; +} + +/* + * Poll the shared channel map until at least `target` consumers have subscribed + * (or a bounded timeout elapses). We share the coordinator's shmem, so we read + * the count directly rather than through the connections. + */ +static bool +anser_wait_consumer_count(const AnserChannelKey *key, int target) +{ + int i; + + for (i = 0; i < 1000; i++) /* up to ~10s */ + { + CHECK_FOR_INTERRUPTS(); + if (AnserChannelConsumerCount(key) >= target) + return true; + pg_usleep(10000); /* 10ms */ + } + + return false; +} + +/* Send a cancel request for conn's in-flight query (best effort). */ +static void +anser_cancel_conn(PGconn *conn) +{ + PGcancel *cancel = PQgetCancel(conn); + + if (cancel != NULL) + { + char errbuf[256]; + + (void) PQcancel(cancel, errbuf, sizeof(errbuf)); + PQfreeCancel(cancel); + } +} + +/* + * Pump a connection until its outstanding query stops being busy (result ready) + * or a bounded timeout elapses. Returns false on connection loss/timeout. + */ +static bool +anser_drain_until_idle(PGconn *conn) +{ + int i; + + for (i = 0; i < 1000; i++) /* up to ~100s worst case; resolves in ms */ + { + CHECK_FOR_INTERRUPTS(); + if (!PQconsumeInput(conn)) + return false; + if (!PQisBusy(conn)) + return true; + + (void) WaitLatchOrSocket(MyLatch, + WL_LATCH_SET | WL_SOCKET_READABLE | + WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + PQsocket(conn), 100L, PG_WAIT_EXTENSION); + ResetLatch(MyLatch); + } + + return false; +} + +/* True iff the consumer returned exactly the expected payload bytes. */ +static bool +anser_consumer_got_payload(PGconn *conn, const unsigned char *expected, + Size expected_len) +{ + PGresult *res; + PGresult *tmp; + bool ok = false; + + if (!anser_drain_until_idle(conn)) + return false; + + res = PQgetResult(conn); + if (res != NULL && PQresultStatus(res) == PGRES_TUPLES_OK && + PQntuples(res) == 1 && !PQgetisnull(res, 0, 0) && + (Size) PQgetlength(res, 0, 0) == expected_len && + memcmp(PQgetvalue(res, 0, 0), expected, expected_len) == 0) + ok = true; + + if (res != NULL) + PQclear(res); + while ((tmp = PQgetResult(conn)) != NULL) + PQclear(tmp); + + return ok; +} + +/* True iff the consumer returned a non-null data row (it should not have). */ +static bool +anser_consumer_returned_row(PGconn *conn) +{ + PGresult *res; + bool got_row = false; + + if (!anser_drain_until_idle(conn)) + return false; + + while ((res = PQgetResult(conn)) != NULL) + { + if (PQresultStatus(res) == PGRES_TUPLES_OK && + PQntuples(res) >= 1 && !PQgetisnull(res, 0, 0)) + got_row = true; + PQclear(res); + } + + return got_row; +} + +/* + * Poll (bounded) until the channel recycles to CONSUMED, or has already been + * reclaimed entirely. Either outcome means it did not leave stale data behind; + * a channel wedged in READY never reaches this and the poll times out. + */ +static bool +anser_wait_channel_consumed(const AnserChannelKey *key) +{ + int i; + + for (i = 0; i < 1000; i++) /* up to ~10s */ + { + bool found = false; + AnserChannelState state = AnserChannelGetState(key, &found); + + if (!found || state == ANSER_CHANNEL_CONSUMED) + return true; + + CHECK_FOR_INTERRUPTS(); + pg_usleep(10000); /* 10ms */ + } + + return false; +} + +/* + * Pause or resume the background maintenance sweep. With it paused, terminal + * (CANCELLED/CONSUMED) channels stay in the map so tests can assert their state + * without racing the gather/send services. + */ +Datum +anser_test_set_sweep(PG_FUNCTION_ARGS) +{ + bool enabled = PG_GETARG_BOOL(0); + + AnserSetSweepEnabled(enabled); + PG_RETURN_VOID(); +} + +/* + * Guard regression for AnserMaxChannels()'s memoization. + * + * The channel map is sized once at postmaster start; AnserMaxChannels() caches + * that result so a later per-session SET gp_max_slices cannot report a size that + * disagrees with the shared memory actually allocated (which would let the Len + * functions index past the arrays). Read the effective size, change + * gp_max_slices to a value that -- absent the cache -- would grow the auto-sized + * map by orders of magnitude, read again, and assert it did not budge. Runs + * entirely inside this one backend so the two reads bracket the SET. + */ +Datum +anser_test_max_channels_stable_across_slices(PG_FUNCTION_ARGS) +{ + int before = AnserMaxChannels(); + char saved[32]; + bool stable; + + /* Preserve the session value so the test leaves no residue behind. */ + snprintf(saved, sizeof(saved), "%d", gp_max_slices); + + /* MaxConnections * 1000000 would dwarf any real map if recomputed live. */ + SetConfigOption("gp_max_slices", "1000000", PGC_USERSET, PGC_S_SESSION); + + stable = (AnserMaxChannels() == before && before > 0); + + SetConfigOption("gp_max_slices", saved, PGC_USERSET, PGC_S_SESSION); + + PG_RETURN_BOOL(stable); +} + +/* + * Run one maintenance sweep synchronously in this backend (respects the enable + * flag), so a test can prove that reclamation clears terminal channels. + */ +Datum +anser_test_sweep(PG_FUNCTION_ARGS) +{ + AnserServiceMaintenance(); + PG_RETURN_VOID(); +} + +/* + * Build a serialized single bloom part (index 0 of 1) carrying one int value, + * seeded from condition_key so every part on the same channel is byte-identical + * in size and parameters (letting the coordinator OR-fold them in place). The + * caller frees the returned buffer; *len_out gets the serialized length. + */ +static char * +anser_make_test_part(const char *condition_key, int32 value, Size *len_out) +{ + uint64 seed = AnserBloomSeed(condition_key); + bloom_filter *filter = AnserBloomCreate(ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, seed); + Datum d = Int32GetDatum(value); + Size sz; + Size len = 0; + char *buf; + + bloom_add_element(filter, (unsigned char *) &d, sizeof(Datum)); + sz = AnserBloomSerializedSize(filter); + buf = palloc(sz); + if (!AnserBloomSerializePart(filter, 0, 1, buf, sz, &len)) + { + bloom_free(filter); + pfree(buf); + return NULL; + } + bloom_free(filter); + *len_out = len; + return buf; +} + +/* + * Fill key from the common leading args (session id, command count, condition + * id, condition key) shared by most test functions. Returns false on invalid + * input. + */ +static bool +build_test_key(FunctionCallInfo fcinfo, AnserChannelKey *key) +{ + int32 gp_session_id = PG_GETARG_INT32(0); + int32 gp_command_count = PG_GETARG_INT32(1); + int32 condition_id_arg = PG_GETARG_INT32(2); + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); + + if (key == NULL || condition_id_arg < 0) + return false; + + if (strlen(condition_key) >= ANSER_CONDITION_KEY_SIZE) + return false; + + MemSet(key, 0, sizeof(AnserChannelKey)); + key->gp_session_id = gp_session_id; + key->gp_command_count = gp_command_count; + key->condition_id = (uint32) condition_id_arg; + strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); + return true; +} + +/* Printable name for a channel state ("UNKNOWN" when out of range). */ +static const char * +state_to_string(AnserChannelState state) +{ + switch (state) + { + case ANSER_CHANNEL_PENDING: + return "PENDING"; + case ANSER_CHANNEL_COLLECTING: + return "COLLECTING"; + case ANSER_CHANNEL_READY: + return "READY"; + case ANSER_CHANNEL_CANCELLED: + return "CANCELLED"; + case ANSER_CHANNEL_CONSUMED: + return "CONSUMED"; + } + + return "UNKNOWN"; +} From 71c8ae49ecfc272d7673c912297eca327dece891 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Thu, 3 Sep 2026 23:02:30 +0300 Subject: [PATCH 04/15] Fix RAT check gpcontrib/anser Add control files to pom.xml since they do not contain APACHE header files --- pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pom.xml b/pom.xml index cb2c25c20c4..be2742ae4a1 100644 --- a/pom.xml +++ b/pom.xml @@ -1284,6 +1284,9 @@ code or new licensing patterns. gpcontrib/reject_partition_fullscan/Makefile gpcontrib/reject_partition_fullscan/reject_partition_fullscan.control + gpcontrib/anser/anser.control + gpcontrib/anser/anser_test.control + From 7dd4c04c65dd2aa6e77f32199f334a0f7315eb88 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Sun, 6 Sep 2026 01:07:44 +0300 Subject: [PATCH 05/15] Rewrite Anser to use existing QD <> QE transport --- gpcontrib/anser/Makefile | 24 +- gpcontrib/anser/README.md | 512 +---- gpcontrib/anser/anser--1.0.sql | 57 - gpcontrib/anser/anser.control | 24 - gpcontrib/anser/anser_test--1.0.sql | 130 +- gpcontrib/anser/anser_test.control | 1 - .../anser/expected/anser_runtime_filter.out | 4 - gpcontrib/anser/expected/anser_test.out | 327 +-- gpcontrib/anser/include/anser.h | 218 +- gpcontrib/anser/include/anserbloom.h | 7 +- gpcontrib/anser/include/anserclient.h | 60 - gpcontrib/anser/include/anserplan.h | 10 +- gpcontrib/anser/include/ansersideband.h | 112 + gpcontrib/anser/sql/anser_runtime_filter.sql | 4 - gpcontrib/anser/sql/anser_test.sql | 121 +- gpcontrib/anser/src/anser.c | 2041 ----------------- gpcontrib/anser/src/anser_test.c | 1157 +--------- gpcontrib/anser/src/anserauth.c | 430 ---- gpcontrib/anser/src/anserbloomconsume.c | 145 +- gpcontrib/anser/src/anserbloomproduce.c | 31 +- gpcontrib/anser/src/anserclient.c | 453 ---- gpcontrib/anser/src/anserdispatch.c | 507 ++++ gpcontrib/anser/src/anserfuncs.c | 200 -- gpcontrib/anser/src/anserinit.c | 223 +- gpcontrib/anser/src/anserplan.c | 48 +- gpcontrib/anser/src/anserplanexec.c | 31 +- gpcontrib/anser/src/anserservice.c | 203 -- gpcontrib/anser/src/ansersideband.c | 425 ++++ src/backend/cdb/dispatcher/cdbdisp_async.c | 11 + src/backend/libpq/auth.c | 45 - src/backend/tcop/postgres.c | 17 + src/include/cdb/cdbdisp.h | 23 + src/include/cdb/cdbvars.h | 13 + src/include/libpq/auth.h | 16 - 34 files changed, 1475 insertions(+), 6155 deletions(-) delete mode 100644 gpcontrib/anser/anser--1.0.sql delete mode 100644 gpcontrib/anser/anser.control delete mode 100644 gpcontrib/anser/include/anserclient.h create mode 100644 gpcontrib/anser/include/ansersideband.h delete mode 100644 gpcontrib/anser/src/anser.c delete mode 100644 gpcontrib/anser/src/anserauth.c delete mode 100644 gpcontrib/anser/src/anserclient.c create mode 100644 gpcontrib/anser/src/anserdispatch.c delete mode 100644 gpcontrib/anser/src/anserfuncs.c delete mode 100644 gpcontrib/anser/src/anserservice.c create mode 100644 gpcontrib/anser/src/ansersideband.c diff --git a/gpcontrib/anser/Makefile b/gpcontrib/anser/Makefile index 5163c5c20c7..9f9250870a7 100644 --- a/gpcontrib/anser/Makefile +++ b/gpcontrib/anser/Makefile @@ -27,33 +27,31 @@ MODULE_big = anser OBJS = \ $(WIN32RES) \ - src/anser.o \ - src/anserauth.o \ src/anserbloomconsume.o \ src/anserbloomproduce.o \ - src/anserclient.o \ + src/anserdispatch.o \ src/anserfilter.o \ - src/anserfuncs.o \ src/anserinit.o \ src/anserplan.o \ src/anserplanexec.o \ - src/anserservice.o \ + src/ansersideband.o \ src/anser_test.o PGFILEDESC = "anser - adaptive information sharing runtime filters" -# anser_test exposes the internal C API to the regression tests; its functions -# live in the same library, so it is a second control file over $libdir/anser -# rather than a separate module. -EXTENSION = anser anser_test -DATA = anser--1.0.sql anser_test--1.0.sql +# The subsystem itself needs no catalog objects -- it is configured entirely by +# GUCs and travels over the dispatch connection. The only extension here is +# anser_test, which exposes the internal C API to the regression tests from +# inside the same library. +EXTENSION = anser_test +DATA = anser_test--1.0.sql REGRESS = anser_test anser_runtime_filter -# src/anserclient.c opens libpq connections back to the coordinator, and -# src/anser_test.c drives loopback connections of its own. +# src/anserdispatch.c writes sideband messages onto the dispatcher's own libpq +# connections, so it needs libpq's internal headers (as the dispatcher itself +# does); the symbols resolve against the backend. PG_CPPFLAGS = -I$(srcdir)/include -I$(libpq_srcdir) -SHLIB_LINK_INTERNAL = $(libpq) SHLIB_PREREQS = submake-libpq ifdef USE_PGXS diff --git a/gpcontrib/anser/README.md b/gpcontrib/anser/README.md index 8b7d7ba1888..beee1d61e44 100644 --- a/gpcontrib/anser/README.md +++ b/gpcontrib/anser/README.md @@ -1,310 +1,127 @@ # Anser — adaptive information sharing Anser is a runtime pub/sub facility for MPP query execution. Producers on the -segments publish a small piece of information about a query (today: a bloom -filter over a join-build key), the coordinator unions the per-segment parts into -one global payload, and consumers on the segments receive it and use it to prune -work (today: skip probe rows that cannot join). The shared state lives in a -fixed coordinator-resident shared-memory **channel map**, serviced by two -background workers (gather + send). - -This document covers installation, the architecture, the segment→coordinator -network transport and its token authentication, the configuration surface, what -a *channel* is, and the *channel state machine*. For the plan-tree integration +segments publish a small piece of information about a running query (today: a +bloom filter over a join-build key), the coordinator combines the per-segment +parts into one, and consumers on the segments receive it and use it to prune +work (today: skip probe rows that cannot join). + +Everything travels over the **dispatch connection the coordinator already holds +open to every segment**. There is no shared memory, no background worker, no +second connection and nothing extra to authenticate: a channel exists only in +the coordinator backend running the query, for exactly as long as that query +runs. + +This document covers installation, the architecture, the wire protocol, the +configuration surface, and what a *channel* is. For the plan-tree integration see `anserplan.c`; for the payload/bloom protocol see `anserfilter.c` and `lib/bloomfilter.c`. ## Installation -Anser is an extension, but it is not a plain `CREATE EXTENSION` extension: -shared memory, background workers and planner/authentication hooks can only be -set up by a **preloaded** library, and the transport additionally needs three -SQL functions to exist in each database it is used in. Three steps, all -required: - -1. **Preload the library on every host** — the whole subsystem hangs off - `_PG_init` (`anserinit.c`), which only wires anything up when the library is - preloaded: - - ``` - gpconfig -c shared_preload_libraries -v "'anser'" --skipvalidation - gpconfig -c anser.enable -v on - gpstop -ra - ``` - - `anser.enable` is `PGC_POSTMASTER`: with it off, no shared memory is - requested and the two services are not registered. - -2. **Create the extension in each database that should use runtime filters**: - - ```sql - CREATE EXTENSION anser; - ``` - - This is what makes `anser.producer_begin` / `anser.publish` / - `anser.consume_wait` resolvable — segment executors call them **by name** - over libpq, so they must be present in that database's catalog. GUCs cannot - substitute: they do not create catalog entries. Install it in `template1` - to have new databases inherit it. Without the extension the plan pass skips - injection entirely (`anser_transport_installed()` in `anserplan.c`), so - queries run exactly as they would with the feature off rather than paying - for filters the segments cannot deliver. - -3. **Turn the filter on** where you want it — `anser.runtime_filter` is - `PGC_USERSET`, so per session, per role, or cluster-wide. - -Two further operational notes: - -- The gather and send services are ordinary background workers registered by - `_PG_init`, so they consume two `max_worker_processes` slots on the - coordinator. If the slots are exhausted the services never start; producers - then hit the produce deadline and every consumer fails open (unfiltered - execution, correct results). -- `anser_test` is a second control file over the same library, exposing the - internal C API to the regression tests. It is test-only and requires - superuser; do not create it in production databases. +Anser is configured entirely by GUCs — it creates no catalog objects, so there +is nothing to `CREATE EXTENSION` in each database. + +``` +gpconfig -c shared_preload_libraries -v "'anser'" --skipvalidation +gpconfig -c anser.enable -v on +gpstop -ra +``` + +The library **must** be preloaded: a segment backend deserializing a dispatched +plan has no opportunity to load it on demand, and its CustomScan providers have +to be registered before that happens. + +Then turn the filter on where you want it — `anser.runtime_filter` is +`PGC_USERSET`, so per session, per role, or cluster-wide. + +`anser_test` is a separate control file over the same library, exposing the +internal C API to the regression tests. It is test-only; do not create it in +production databases. ## Architecture -All Anser state lives in fixed **coordinator shared memory**, allocated once at -postmaster start. Producers and consumers are ordinary query backends -(coordinator-resident, or on segments reaching the coordinator over libpq); they -never talk to each other directly and never own the shared state — they only -hand work to, or wait on, two **background workers** that do. - -Three shared structures, two hand-off points: - -- **Channel map** — the hash of channels (one per runtime condition per query), - holding each channel's state, accounting, and payload. The single source of - truth. -- **Submission queue** — the producer → gather hand-off. A producer copies its - serialized part into a free queue entry, signals the gather worker, and blocks - for an ACK; it never touches the channel payload itself. -- **Wait table** — the send → consumer hand-off, an array of **slots**. A *slot* - is one consumer's reservation on a channel: it records the consumer's key, a - pointer to that backend's latch, and a place for the send worker to stamp the - delivered payload (or a cancel). A blocked consumer owns one slot and sleeps on - its latch until the send worker flips it. - -The two **background workers** exist because the shared state has to keep moving -independent of any one transient/blocked backend: - -- **Gather service** — drains the submission queue: for each part it folds - (bitwise-OR unions) the data into the target channel's single payload, advances - the channel toward `READY`, and ACKs the producer. It also runs periodic - maintenance: time out stragglers (`anser.timeout_ms`) and sweep terminal or - orphaned channels. -- **Send service** — delivers: once a channel is `READY` it copies the combined - payload into every waiting slot and wakes those consumers' latches; when all - expected consumers are served it recycles the channel to `CONSUMED`. - -Both workers sleep on a latch and wake on demand — a producer's submission sets -the gather latch, a publish/registration sets the send latch — plus a periodic -timeout so maintenance runs even when idle. Concurrency is guarded by two -LWLocks, always taken in the order `AnserChannelLock` → `AnserRingLock`. - -The pay-off of this split: a producer can publish and leave, a consumer can block -without pinning anything, and the coordinator still unions once and fans the -result out — see the data-flow section below. +A **channel** is one rendezvous point between the producers and consumers of a +single piece of runtime information, for a single query: + +``` +AnserChannelKey = { gp_session_id, gp_command_count, condition_id, condition_key[64] } +``` + +- `gp_session_id` + `gp_command_count` scope the channel to one query execution, + so keys never collide across sessions or across statements in a session. +- `condition_id` distinguishes multiple filters within the same query. +- `condition_key` is an opaque string describing the filtered condition (today a + synthetic `rf:.=.` string). Both sides derive it + independently and must agree — it is what makes a producer and a consumer meet + on the same channel. + +Channels live in a hash in the coordinator backend, created on first use and +dropped at `ExecutorEnd` (or on transaction abort). Since the merge and the +delivery both happen in that one process, the accumulator is an ordinary +`palloc`'d buffer. ### How it attaches to the server -Everything is reached through existing extensibility points, so the server +Everything is reached through an existing extensibility point, so the server carries no Anser-specific code (`anserinit.c`): | Hook | Used for | | --- | --- | -| `shmem_request_hook` | `RequestAddinShmemSpace` for the three shared structures, plus `RequestNamedLWLockTranche("anser", 2)` | -| `shmem_startup_hook` | `AnserShmemInit()`, which also resolves `AnserChannelLock` / `AnserRingLock` from the tranche | -| `RegisterBackgroundWorker` | the gather and send services (coordinator only — `AnserStartRule`) | -| `planner_hook` | runs the injection pass on the finished plan; wrapping the hook covers ORCA too, since it is dispatched from inside `standard_planner()` | +| `planner_hook` | the injection pass, on the finished plan; wrapping the hook covers ORCA too, since it is dispatched from inside `standard_planner()` | | `RegisterCustomScanMethods` | the producer/consumer nodes, so their methods resolve by name in every backend that deserializes a dispatched plan | +| `cdbdisp_notify_hook` | parts and subscriptions arriving from segments | +| `ExecutorEnd_hook` | dropping a query's channels | | `DefineCustom*Variable` | the `anser.*` GUCs below | -| `CustomAuthClaims_hook` / `CustomAuthCheckPassword_hook` | token authentication for segment→QD connections (see the transport section) | - -The last pair is the only hook Anser added to the server; the others were -already there. - -### Gather-service wakeup cycle - -The gather worker owns the **gather latch** and sleeps on it between passes. -Setting that latch is the "producer work is pending" signal — raised by -`AnserRegisterCondition`, `AnserProducerBegin`, `AnserPublish`, and (the common -one) `AnserEnqueueSubmission` when a remote producer drops a part into a free -submission-queue slot and blocks for its ACK. - -``` -producer backend gather worker (looping) -──────────────── ─────────────────────── -enqueue part → slot = PENDING -SetLatch(gather_latch) ───────────────► WaitLatch(gather_latch) returns -block on own latch ResetLatch(gather_latch) - │ AnserGatherServiceCycle(): - │ for each PENDING slot: - │ AnserGatherApply() ← fold/union part, - │ advance channel toward READY - │ slot = ACCEPTED/REJECTED - ▼ SetLatch(producer_latch) ─┐ -wake, read ACK, free slot ◄───────────────────────────────────────── ┘ - AnserCancelStaleChannels() (timeouts) - AnserReapSubmissionSlots() - AnserServiceMaintenance() (orphan sweep) - SetLatch(send_latch) on READY ─► send worker - WaitLatch(gather_latch) … (sleep again) -``` -Key properties: +The first two already existed; `cdbdisp_notify_hook` and the +`GP_SIDEBAND_MESSAGE` tolerance in the QE command loop are the only additions +Anser needed in the server, and neither mentions Anser. -- **No lost wakeups.** If the latch is set while the worker is mid-pass (not yet - waiting), it stays set and the next `WaitLatch` returns immediately. -- **Two hand-offs.** The cycle wakes each producer via *its own* latch (the ACK), - and wakes the **send** worker via the send latch once a channel reaches `READY` - — the gather worker never delivers to consumers itself. -- **Timed fallback.** The same cycle also runs every - `ANSER_SERVICE_WAKEUP_INTERVAL_MS` even with no latch set, so stale - `COLLECTING` channels time out and orphaned channels get swept while idle. +## Wire protocol -### Send-service wakeup cycle +The two directions are deliberately asymmetric, because the constraints differ. -The send worker owns the **send latch** and sleeps on it. Setting that latch -means "a channel is now deliverable or cancellable, or a consumer is now -waiting" — raised by the gather worker when a channel reaches `READY` -(`AnserGatherApply`), by the publish/cancel/timeout paths when a channel is -cancelled, and by a consumer when it subscribes (`AnserConsumerWait`) or abandons -its wait (`AnserAbandonWaitSlot`). +**Segment → coordinator** is a `NOTIFY` on channel `anser_rf`, the model +`nextval()` uses (`cdb_sequence_nextval_qe` in `commands/sequence.c`). The QD is +a libpq *client*, and libpq rejects message types it does not know, so this +direction has to be a message type libpq already understands. `NotifyMyFrontEnd` +imposes no length limit of its own — the ~8 KB `NOTIFY_PAYLOAD_MAX_LENGTH` +applies to the SQL-level `NOTIFY`, which must fit a queue page — but it delivers +through `pq_sendstring`, so the payload must be a NUL-free string: ``` -gather worker / canceller / send worker (looping) consumer backend -consumer subscribe ───────────────────── ──────────────── -────────────────────────── subscribe: slot = WAITING -channel → READY | CANCELLED | CONSUMED -SetLatch(send_latch) ───────────────────► WaitLatch(send_latch) returns - ResetLatch(send_latch) - AnserSendServiceCycle(): - for each READY/terminal channel: - for each WAITING slot on it: - READY → copy payload → slot, - slot = DELIVERED ────► wake, read payload, - terminal → slot = CANCELLED ───► (or cancel → fail open), - SetLatch(consumer_latch) free slot - done_consumers++ - all served → recycle CONSUMED - AnserReapWaitSlots() - WaitLatch(send_latch) … (sleep again) +anser1 \n + ``` -Key properties: - -- **Per-consumer delivery.** Each `WAITING` slot gets its *own* pinned copy of the - merged payload, so delivery is per-consumer — one consumer's cancel (or a DSM - shortage that cancels just it) never affects another's delivery. -- **Straggler safety.** A consumer that registers on an already-terminal channel - is handed a cancel and fails open, instead of blocking on a channel the sweep - would otherwise never reclaim. -- **Recycle.** Once `done_consumers` reaches `expected_consumers` (one per - segment) the channel becomes `CONSUMED` and its payload is freed. -- **No lost wakeups / timed fallback.** Like the gather worker: a latch set - mid-pass is honored next loop, and the same cycle runs every - `ANSER_SERVICE_WAKEUP_INTERVAL_MS` so straggler cancels and recycling still - happen while idle. - -## Network transport: how segments connect and authenticate - -Coordinator-resident producers/consumers touch the channel map directly. -Segment executors cannot — the map lives in the coordinator's shared memory — so -they open an **ordinary libpq connection back to the QD** and drive the -`anser.producer_begin` / `anser.publish` / `anser.consume_wait` SQL -functions from that backend (`anserclient.c`). The QD address comes from -`gp_qd_hostname` / `gp_qd_port`, which the dispatcher injects into every QE; the -connection reuses the query's database and the **session user** -(`MyProcPort->user_name` — the authenticated login role, unaffected by -`SET ROLE`), and sets `application_name=anser_rf` so these backends are -identifiable on the coordinator. - -### Authentication: per-session token (the parallel-retrieve-cursor model) - -The backward connection must not depend on `pg_hba.conf`: a stock -`gpinitsystem` cluster grants `trust` to coordinator IPs on the *segments* (that -is what makes QD→QE dispatch connections work), but never adds segment hosts to -the *coordinator's* pg_hba — so a segment→QD connection would be rejected by -default. Anser therefore authenticates these connections the same way -`PARALLEL RETRIEVE CURSOR` retrieve sessions do (`retrieve_conn_authentication` -in `libpq/auth.c`): - -1. **Token registration (QD, plan time).** When the planner pass injects a - runtime filter into a query, the QD registers a **per-session token**: - 128 bits of `pg_strong_random`, hex-encoded, stored in the shared-memory - *session token hash* keyed by `(gp_session_id, session user)` - (`AnserGetOrCreateSessionToken` in `anser.c`). One token per session; the - entry is removed when the session exits. -2. **Delivery to segments.** The token travels inside the dispatched plan (a - `String` in the producer/consumer `CustomScan.custom_private`), so it only - crosses the already-trusted QD→QE dispatch channel. -3. **Connection (segment).** The segment executor connects with the startup - marker `anser.conn=true` (passed via the libpq `options` keyword) and the - token as the connection `password`. -4. **Verification (QD, auth time).** `ClientAuthentication` checks the marker - **before** pg_hba is consulted and calls the extension's hooks - (`AnserConnClaims` / `AnserConnCheckPassword`): it - requests the password, resolves `user_name` to a role OID, and calls - `AnserSessionTokenIsValid`, which scans the token hash for a matching - `(user, token)` pair. On match the connection becomes an ordinary backend - for that user (`FakeClientAuthentication`); on mismatch it is rejected with - `FATAL`. +`kind` is `P` (a producer's part) or `S` (a consumer subscribing). The header +holds only numbers and one character, so it cannot contain the newline that ends +it; key and body are taken by length, so neither needs escaping. -``` -segment executor coordinator -──────────────── ─────────── -libpq connect: user=, - options="-c anser.conn=true", - password= - ── startup ────────► ClientAuthentication: - marker seen → skip pg_hba - ◄── AUTH_REQ_PASSWORD -token ─────────────────────────────► AnserSessionTokenIsValid(user, token) - scans session token hash (shmem) - ◄── OK / FATAL -SELECT anser.producer_begin(...) ──► ... runs as the session user -``` +**Coordinator → segment** is a `GP_SIDEBAND_MESSAGE`, written with `pqPutnchar`, +which performs no conversion — so the merged filter travels as **raw binary**, +with no base64 tax. That is the direction that matters most, since the merged +payload is sent once *per consumer* while each part is sent once. -Properties and limits of this model: - -- **No pg_hba change needed** on the coordinator for segment hosts; no password - of the user ever leaves the client. -- The token is **per session, not per query**, and grants a *full* SQL backend - as that user (unlike retrieve sessions, which are utility-mode and - `RETRIEVE`-only). Anyone who learns a live session's token can connect as its - user — the token never leaves the trusted dispatch channel, but it does - appear in debug-level plan dumps (`debug_print_plan`), so treat those logs as - sensitive. -- **Channel-level access control is unchanged and independent**: a channel is - bound to the role that created it (`AnserChannelEntry.creator_role`), so even - an authenticated connection can only produce/consume on channels its own role - created (or any, if superuser). -- **Fail open.** If no token was registered (subsystem off, token hash full) or - authentication fails for any reason, the connection attempt returns NULL and - the segment runs unfiltered — never an error, never wrong results. -- **Without a token** the client omits the marker and password, and the - connection goes through ordinary pg_hba authentication (previous behavior); - this also covers hand-built or test deployments where the admin chose to - provision pg_hba entries instead. - -The `anser.conn` GUC itself is a marker only (`PGC_BACKEND`, not settable in -`postgresql.conf`, not synced to segments); its value is read from the raw -startup options during authentication. +The coordinator services these while it is blocked receiving tuples: the +interconnect adds every dispatch socket to its wait set +(`ic_udpifc.c`, `ic_tcp.c`), and a readable one leads to +`checkForCancelFromQD` → `cdbdisp_checkForCancel` → `processResults`, which is +where the notify handler runs. Under `gp_interconnect_type=proxy` that check is +driven by a 2 s timer instead (`ic_proxy_backend.c`), so filter delivery can lag +by up to that long. ## GUCs | GUC | Default | Context | Meaning | | --- | --- | --- | --- | -| `anser.enable` | `off` | POSTMASTER | Master switch. When on, the channel-map shared memory is sized/created and the gather + send background workers are started at postmaster start. Off = the whole subsystem is absent (zero shmem, no workers). | -| `anser.runtime_filter` | `off` | USERSET | Enables the post-planning pass that injects bloom-filter producer/consumer nodes into a matching plan. Requires `anser.enable`; without it the pass is a no-op even when the subsystem is up. | -| `anser.max_channels` | `0` (auto) | POSTMASTER | Number of channels the map can hold; sizes the channel hash, the producer submission queue, and (× `anser.max_consumers_per_channel`) the consumer wait table. `0` auto-sizes to `max_connections * gp_max_slices` — at most `max_connections` concurrent queries, each opening up to `gp_max_slices` runtime-filter channels — falling back to a fixed per-connection budget (8) when `gp_max_slices` is unbounded (`0`). Captured once at postmaster start so it is stable across all backends. | -| `anser.max_info_size` | `65 MB` | POSTMASTER | Maximum serialized payload (unioned bloom filter + part header) a channel may hold; caps per-channel memory and bounds the effective bloom-filter size. The default is `64 MB + 1 MB` so a full 64 MB power-of-two bitset fits with its header; `bloom_create` also floors every bitset at 1 MB. | -| `anser.max_consumers_per_channel` | `64` | POSTMASTER | Wait-table slots reserved per channel; the consumer wait table is sized `anser.max_channels * this`. Bounds how many consumers can block on one channel at once. | -| `anser.timeout_ms` | `1000` | USERSET | Produce/collect deadline. A channel that is still collecting parts when this elapses is cancelled by the maintenance sweep, so waiting consumers fail open (run unfiltered) rather than hang. | +| `anser.enable` | `off` | SIGHUP | Master switch. With it off the plan pass never injects anything and no filters are exchanged. | +| `anser.runtime_filter` | `off` | USERSET | Enables the post-planning pass that injects bloom-filter producer/consumer nodes into a matching plan. Requires `anser.enable`. | +| `anser.max_info_size` | `65 MB` | POSTMASTER | Maximum serialized payload (merged bloom filter + part header) a channel may hold; caps the effective bloom-filter size. The default is `64 MB + 1 MB` so a full 64 MB power-of-two bitset fits with its header; `bloom_create` also floors every bitset at 1 MB. | +| `anser.timeout_ms` | `1000` | USERSET | How long a consumer waits for its filter before running unfiltered. The deadline matters because a producer that gets squelched never publishes at all: `ExecSquelchNode` only marks a `CustomScanState`, it does not call the node back. | -## Data flow: producer → gather (bitwise union) → consumer +## Data flow: producer → merge (bitwise union) → consumer The parts from all segments are combined into **one** payload by a **bitwise OR on the coordinator**, and that single combined payload is delivered to every @@ -313,7 +130,7 @@ consumer. This is the core of Anser and worth stating precisely, because it is ``` segment 0 producer: bitset 0000 0001 ┐ -segment 1 producer: bitset 0000 0010 ├─ libpq ─► gather service (coordinator) +segment 1 producer: bitset 0000 0010 ├─ NOTIFY ─► QD backend (notify hook) segment N producer: ... ┘ │ │ fold each part into the │ running merged bitset: @@ -322,7 +139,7 @@ segment N producer: ... ┘ │ ▼ = 0000 0011 (one part) channel payload = single merged bitset │ - send service ──────────┼───────────────┐ + sideband push ───────────┼───────────────┐ ▼ ▼ ▼ consumer seg 0 consumer seg 1 ... consumer seg N each receives the SAME combined 0000 0011 @@ -335,33 +152,32 @@ Step by step: `max_payload` / a `condition_key`-derived seed, all carried in the plan node — *not* on the wire) and serializes it as one *part*. Because every producer and the consumer pass the identical parameters, every part has a byte-for-byte - identical size and shape. Segment producers push their parts to the coordinator - concurrently over their own libpq connections — the network transfer is - parallel, and the submission queue is sized `channels * per-channel producers` - so they hand off without serializing. + identical size and shape. Publishing is fire-and-forget: the producer sends + its part and carries on without waiting for an acknowledgement. -2. **Gather (coordinator, once per part).** The coordinator never reconstructs a +2. **Merge (coordinator, once per part).** The coordinator never reconstructs a filter — it works on raw bytes. The **first** part is stored verbatim; every later part is folded into the channel's payload with an in-place **bitwise OR** - of the bitset (`AnserBloomFoldPartInPlace` in `anserfilter.c`, from - `AnserStorePayloadDSM` in `anser.c`). The payload is therefore always a - **single merged bitset**, the size of one filter — it does **not** grow with - the segment count. The OR requires the incoming part to be the same size as the - accumulator (guaranteed by the shared parameters); a mismatch makes the fold - fail and the channel is cancelled (consumers fail open). - -3. **Deliver (coordinator → every consumer).** Once every expected part is folded - (channel `READY`), the send service delivers a copy of that one combined - bitset to each waiting consumer. Delivery is O(segments) bytes, not - O(segments²), and the union work is done once on the master rather than - repeated in every consumer. + of the bitset (`AnserBloomFoldPartInPlace` in `anserfilter.c`, called from + `anserdispatch.c`). The payload is therefore always a **single merged bitset**, + the size of one filter — it does **not** grow with the segment count. Folding + happens as parts arrive, so only the last fold is on the critical path. The OR + requires the incoming part to be the same size as the accumulator (guaranteed + by the shared parameters); a mismatch cancels the channel and consumers fail + open. + +3. **Deliver (coordinator → every consumer).** Once every expected part is + folded, the merged bitset is pushed to each subscriber. Delivery is per + consumer: a failed write costs that one segment its filter and leaves the + others alone. A consumer that subscribes *after* the channel completed — which + happens routinely, since producers on other segments may finish first — is + served immediately. 4. **Consume (per segment).** Each consumer rebuilds an empty filter from its own plan parameters (the same `total_elems` / `max_payload` / seed the producers used) and loads the received bitset into it (`AnserBloomDeserializePart`), requiring the received length to match exactly (else it fails open). It does - **not** re-union anything; the merged header's part count is surfaced as the - `Rows Removed by Bloom Filter` / parts-received EXPLAIN stats. + **not** re-union anything. Correctness note: the combined filter is the OR (super-set) of every segment's build keys, so it can only ever have *false positives*, never false negatives — @@ -369,100 +185,18 @@ a probe row it rejects genuinely cannot join. Anser therefore only changes performance, never results; any failure along this path degrades to "no filter" (fail open). -## Channel - -A **channel** is one rendezvous point between the producers and consumers of a -single piece of runtime information, for a single query. It is a shared-memory -entry (`AnserChannelEntry`) in the coordinator's channel hash, addressed by an -`AnserChannelKey`: - -``` -AnserChannelKey = { gp_session_id, gp_command_count, condition_id, condition_key[64] } -``` - -- `gp_session_id` + `gp_command_count` scope the channel to one query execution, - so keys never collide across sessions or across statements in a session. -- `condition_id` distinguishes multiple filters within the same query. -- `condition_key` is an opaque string describing the filtered condition (today a - synthetic `rf:.=.` string). Both sides derive it - independently and must agree — it is what makes a producer and a consumer meet - on the same channel. - -The entry also tracks bookkeeping used by the state machine: `expected_producers` -/ `done_producers` (one part per segment), `consumers` / `expected_consumers` / -`done_consumers` (delivery accounting), the `creator_role` (only that role or a -superuser may produce/consume on it), the payload (`dsm_handle` + `data_len`), -and `created_at` / `updated_at` timestamps used by the maintenance sweep. - -The channel map is finite and fixed-size. Terminal channels are reclaimed by the -background maintenance sweep (or, under map pressure, by emergency reclamation on -registration) so their slots can be reused; a fresh registration landing on a -terminal entry resets it in place. - -## `AnserChannelState` and the state flow - -A channel moves through five states (`AnserChannelState`): - -| State | Meaning | -| --- | --- | -| `PENDING` | Registered; no producer part received yet. | -| `COLLECTING` | At least one part received; still waiting for the rest. | -| `READY` | All expected parts collected and unioned; payload deliverable. | -| `CANCELLED` | Aborted (timeout / explicit cancel / owner death / query cancel). Terminal. Consumers fail open. | -| `CONSUMED` | Every expected consumer has been delivered the payload. Terminal. | - -``` - register (RegisterCondition / ProducerBegin) - │ - ▼ - ┌─────────────┐ - │ PENDING │ - └─────────────┘ - │ first part published - ▼ - ┌─────────────┐ - ┌──────────────│ COLLECTING │ - │ └─────────────┘ - │ │ done_producers == expected_producers - │ ▼ - │ ┌─────────────┐ - cancel / │ │ READY │ - timeout / │ └─────────────┘ - owner death│ │ done_consumers == expected_consumers - / query │ ▼ - cancel │ ┌─────────────┐ - │ │ CONSUMED │ (terminal) - ▼ └─────────────┘ - ┌─────────────┐ │ - │ CANCELLED │ (terminal) │ - └─────────────┘ │ - │ │ - └──────────┬──────────┘ - ▼ - maintenance sweep reclaims slot (→ NOT_FOUND) - or a fresh register() resets the entry to PENDING -``` - -**Transitions:** - -- **create → `PENDING`** — `AnserRegisterCondition` / `AnserProducerBegin` insert - the entry (or reset a terminal one) with `expected_producers` set. -- **`PENDING` → `COLLECTING`** — the first part is published (`AnserPublish` / - the gather service applying a submitted part). The part is unioned into the - payload and `done_producers` is incremented. -- **`COLLECTING` → `READY`** — the part that makes `done_producers` reach - `expected_producers` completes the union; the global payload is now - deliverable and the send service wakes waiting consumers. -- **`READY` → `CONSUMED`** — the send service delivers the payload to each - waiting consumer; when `done_consumers` reaches `expected_consumers` (one per - segment) the channel is recycled to `CONSUMED` and its payload freed. Abandoned - consumers (cancelled mid-wait) stop counting so this can still be reached. -- **`PENDING`/`COLLECTING` → `CANCELLED`** — via `anser.timeout_ms` expiry - (maintenance sweep on a still-`COLLECTING` channel), a producer publishing a - cancel part, a whole-query `AnserCancelQuery`, or the creator backend dying. - Any consumer blocked on the channel is woken with a cancel and **fails open** - (runs unfiltered) — Anser never changes results, only performance. -- **terminal (`CANCELLED`/`CONSUMED`) → gone** — the background maintenance sweep - (unless paused via the test-only `sweep_enabled` knob) removes terminal and - orphaned channels, freeing the slot; a later registration reusing the same key - starts over at `PENDING`. +## Failure handling + +Every failure mode ends in unfiltered execution, never in a wrong answer and +never in an error raised into the query: + +- a producer that is squelched, errors, or produces an oversized part → the + channel never completes (or is cancelled) → consumers hit `anser.timeout_ms` + and run unfiltered; +- a malformed or undecodable part → the channel is cancelled and every consumer + is told so; +- a broken connection → the query is failing anyway, and the interconnect + reports it far more usefully than the filter path could; +- query cancellation → the consumer's wait is a `CHECK_FOR_INTERRUPTS` loop, and + a delivery that arrives after nobody is waiting is discarded by the QE command + loop (`GP_SIDEBAND_MESSAGE` is accepted and ignored there). diff --git a/gpcontrib/anser/anser--1.0.sql b/gpcontrib/anser/anser--1.0.sql deleted file mode 100644 index 5ae42479a1d..00000000000 --- a/gpcontrib/anser/anser--1.0.sql +++ /dev/null @@ -1,57 +0,0 @@ -/* gpcontrib/anser/anser--1.0.sql */ - --- complain if script is sourced in psql, rather than via CREATE EXTENSION -\echo Use "CREATE EXTENSION anser" to load this file. \quit - -/* - * The coordinator-side edges of the Anser network transport. Segment - * executors call these over libpq while running a plan that carries Anser - * runtime-filter nodes; they are not a user-facing API. - * - * Execution location is left at the default (EXECUTE ON ANY), even though the - * channel map only exists in coordinator shared memory: CREATE FUNCTION - * accepts EXECUTE ON COORDINATOR only for set-returning functions (see - * validate_sql_exec_location() in commands/functioncmds.c). It costs nothing - * here -- the transport calls these as "SELECT anser.publish(...)" with no - * FROM clause, which a coordinator backend evaluates locally -- and a call that - * did somehow reach a segment would find no channel map and return false, i.e. - * fail open. - * - * The default EXECUTE grant to PUBLIC -- and the USAGE grant on the schema - * below -- are intentional and must not be revoked: segments connect back as - * the query's own role, so restricting these functions would silently disable - * runtime filtering for every non-superuser query. Callers are confined to - * the channels their own role created (see anserfuncs.c). - */ - -GRANT USAGE ON SCHEMA anser TO PUBLIC; - -CREATE FUNCTION anser.producer_begin( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - expected_producers int4) -RETURNS bool -AS 'MODULE_PATHNAME', 'anser_producer_begin' -LANGUAGE C STRICT VOLATILE; - -CREATE FUNCTION anser.publish( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - payload bytea, - cancelled bool) -RETURNS bool -AS 'MODULE_PATHNAME', 'anser_publish' -LANGUAGE C STRICT VOLATILE; - -CREATE FUNCTION anser.consume_wait( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text) -RETURNS bytea -AS 'MODULE_PATHNAME', 'anser_consume_wait' -LANGUAGE C STRICT VOLATILE; diff --git a/gpcontrib/anser/anser.control b/gpcontrib/anser/anser.control deleted file mode 100644 index 2546b23edc0..00000000000 --- a/gpcontrib/anser/anser.control +++ /dev/null @@ -1,24 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# anser extension -comment = 'Anser adaptive information sharing (runtime bloom filters)' -default_version = '1.0' -module_pathname = '$libdir/anser' -schema = 'anser' -relocatable = false -superuser = true diff --git a/gpcontrib/anser/anser_test--1.0.sql b/gpcontrib/anser/anser_test--1.0.sql index de8b54457a2..1772e748653 100644 --- a/gpcontrib/anser/anser_test--1.0.sql +++ b/gpcontrib/anser/anser_test--1.0.sql @@ -3,82 +3,6 @@ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION anser_test" to load this file. \quit -CREATE FUNCTION anser_test_register_condition( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - expected_producers int4) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_subscribe( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_publish( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - payload bytea, - cancelled bool) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_publish_value( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - value int4) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_consume( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - timeout_ms int4) -RETURNS bytea -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_consume_has( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text, - value int4) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_state( - gp_session_id int4, - gp_command_count int4, - condition_id int4, - condition_key text) -RETURNS text -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_cancel_query( - gp_session_id int4, - gp_command_count int4) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - CREATE FUNCTION anser_test_bloom_roundtrip( condition_key text, value int4) @@ -89,64 +13,14 @@ LANGUAGE C STRICT; CREATE FUNCTION anser_test_bloom_fold_inplace() RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_bloom_rejects_mismatch() -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_node_roundtrip(value int4) -RETURNS bool -AS 'MODULE_PATHNAME' LANGUAGE C STRICT; -CREATE FUNCTION anser_test_client_roundtrip(value int4) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_token_roundtrip() -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_multi_consumer(value int4) -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_abandoned_consumer_recycles(value int4) +CREATE FUNCTION anser_test_bloom_rejects_mismatch() RETURNS bool AS 'MODULE_PATHNAME' LANGUAGE C STRICT; -CREATE FUNCTION anser_test_dsm_free_on_success() -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_dsm_free_on_timeout() -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_dsm_free_on_cancel() +CREATE FUNCTION anser_test_node_roundtrip(value int4) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_set_sweep(enabled bool) -RETURNS void -AS 'MODULE_PATHNAME' LANGUAGE C STRICT; - -CREATE FUNCTION anser_test_sweep() -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C; - -CREATE FUNCTION anser_test_max_channels_stable_across_slices() -RETURNS bool -AS 'MODULE_PATHNAME' -LANGUAGE C; diff --git a/gpcontrib/anser/anser_test.control b/gpcontrib/anser/anser_test.control index e07633dd15d..8a4f941a49b 100644 --- a/gpcontrib/anser/anser_test.control +++ b/gpcontrib/anser/anser_test.control @@ -20,4 +20,3 @@ comment = 'test helpers for the anser extension' default_version = '1.0' module_pathname = '$libdir/anser' relocatable = true -requires = 'anser' diff --git a/gpcontrib/anser/expected/anser_runtime_filter.out b/gpcontrib/anser/expected/anser_runtime_filter.out index 21e1fc61ce7..eb4765ac25a 100644 --- a/gpcontrib/anser/expected/anser_runtime_filter.out +++ b/gpcontrib/anser/expected/anser_runtime_filter.out @@ -5,9 +5,6 @@ -- exercising the Motion-directly-under-CustomScan case; on a single segment it -- degrades to the leaf case. Either way the feature must (a) inject the -- producer/consumer nodes and (b) never change query results. --- The plan pass only injects when the extension is installed in this --- database, since that is what makes the segments' callbacks resolvable. -CREATE EXTENSION anser; -- Deterministic plan shape: force a hash join. SET enable_nestloop = off; SET enable_mergejoin = off; @@ -224,4 +221,3 @@ RESET anser.runtime_filter; RESET optimizer; RESET enable_nestloop; RESET enable_mergejoin; -DROP EXTENSION anser; diff --git a/gpcontrib/anser/expected/anser_test.out b/gpcontrib/anser/expected/anser_test.out index 784e11df8d4..0a65887d5e1 100644 --- a/gpcontrib/anser/expected/anser_test.out +++ b/gpcontrib/anser/expected/anser_test.out @@ -1,336 +1,35 @@ -CREATE EXTENSION anser_test CASCADE; -NOTICE: installing required extension "anser" --- Pause the background maintenance sweep so terminal (CANCELLED/CONSUMED) --- channels stay observable and the state assertions below are deterministic --- rather than racing the live gather/send services. Re-enabled at the end, --- where we prove the sweep actually reclaims them. -SELECT anser_test_set_sweep(false); - anser_test_set_sweep ----------------------- - -(1 row) - --- Happy path: one condition, two producers, two consumers. Each producer --- publishes a real bloom part (multi-payload combine is bloom-only now); the two --- parts union on the coordinator, so each consumer's received filter contains --- both producers' values. -SELECT anser_test_register_condition(1, 1, 1, 'join_a', 2); - anser_test_register_condition -------------------------------- - t -(1 row) - -SELECT anser_test_subscribe(1, 1, 1, 'join_a'); - anser_test_subscribe ----------------------- - t -(1 row) - -SELECT anser_test_subscribe(1, 1, 1, 'join_a'); - anser_test_subscribe ----------------------- - t -(1 row) - -SELECT anser_test_publish_value(1, 1, 1, 'join_a', 10); - anser_test_publish_value --------------------------- - t -(1 row) - -SELECT anser_test_state(1, 1, 1, 'join_a'); - anser_test_state ------------------- - COLLECTING -(1 row) - -SELECT anser_test_publish_value(1, 1, 1, 'join_a', 20); - anser_test_publish_value --------------------------- - t -(1 row) - -SELECT anser_test_state(1, 1, 1, 'join_a'); - anser_test_state ------------------- - READY -(1 row) - -SELECT anser_test_consume_has(1, 1, 1, 'join_a', 10); - anser_test_consume_has ------------------------- - t -(1 row) - -SELECT anser_test_consume_has(1, 1, 1, 'join_a', 20); - anser_test_consume_has ------------------------- - t -(1 row) - -SELECT anser_test_state(1, 1, 1, 'join_a'); - anser_test_state ------------------- - CONSUMED -(1 row) - --- Timeout/cancel path: no producer publishes. -SELECT anser_test_register_condition(1, 1, 2, 'join_timeout', 1); - anser_test_register_condition -------------------------------- - t -(1 row) - -SELECT anser_test_subscribe(1, 1, 2, 'join_timeout'); - anser_test_subscribe ----------------------- - t -(1 row) - -SELECT anser_test_consume(1, 1, 2, 'join_timeout', 1) IS NULL; - ?column? ----------- - t -(1 row) - -SELECT anser_test_state(1, 1, 2, 'join_timeout'); - anser_test_state ------------------- - CANCELLED -(1 row) - --- Input validation: negative IDs/counts and overlong condition keys fail. -SELECT anser_test_register_condition(1, 1, -1, 'bad_id', 1); - anser_test_register_condition -------------------------------- - f -(1 row) - -SELECT anser_test_register_condition(1, 1, 3, 'bad_count', 0); - anser_test_register_condition -------------------------------- - f -(1 row) - -SELECT anser_test_register_condition(1, 1, 3, repeat('x', 64), 1); - anser_test_register_condition -------------------------------- - f -(1 row) - -SELECT anser_test_subscribe(1, 1, -1, 'bad_id'); - anser_test_subscribe ----------------------- - f -(1 row) - -SELECT anser_test_subscribe(1, 1, 3, repeat('x', 64)); - anser_test_subscribe ----------------------- - f -(1 row) - --- Query-level cancellation touches all channels for the command. -SELECT anser_test_register_condition(1, 2, 1, 'join_b', 1); - anser_test_register_condition -------------------------------- - t -(1 row) - -SELECT anser_test_register_condition(1, 2, 2, 'join_c', 1); - anser_test_register_condition -------------------------------- - t -(1 row) - -SELECT anser_test_cancel_query(1, 2); - anser_test_cancel_query -------------------------- - -(1 row) - -SELECT anser_test_state(1, 2, 1, 'join_b'); - anser_test_state ------------------- - CANCELLED -(1 row) - -SELECT anser_test_state(1, 2, 2, 'join_c'); - anser_test_state ------------------- - CANCELLED -(1 row) - --- GUC sizing guard: AnserMaxChannels() is memoized at postmaster start, so a --- per-session SET gp_max_slices must not change the reported channel-map size --- (otherwise the shared arrays and the Len functions would disagree). -SELECT anser_test_max_channels_stable_across_slices() AS max_channels_stable; - max_channels_stable ---------------------- - t -(1 row) - --- Bloom payload protocol and standalone producer/consumer helpers. +CREATE EXTENSION anser_test; +-- Bloom payload protocol: serialize one part and read it back. SELECT anser_test_bloom_roundtrip('bf_roundtrip', 42); anser_test_bloom_roundtrip ---------------------------- t (1 row) --- In-place fold: same-size union mutates the buffer; a mismatched size is --- rejected. This is the coordinator's only combine path (first part is stored --- verbatim, every later part folds in here). +-- In-place fold: same-size union mutates the accumulator; a mismatched size is +-- rejected. This is the coordinator's only combine path (the first part is +-- kept verbatim, every later part folds into it). SELECT anser_test_bloom_fold_inplace() AS fold_inplace_ok; fold_inplace_ok ----------------- t (1 row) -SELECT anser_test_node_roundtrip(168); - anser_test_node_roundtrip ---------------------------- - t -(1 row) - --- Safety regression: the consumer rebuilds the filter from its own parameters and --- requires the received bitset to be exactly the expected size (and the header --- magic to match); truncated/oversized/corrupt parts are rejected (fail open). +-- Safety regression: the consumer rebuilds the filter from its own parameters +-- and requires the received bitset to be exactly the expected size (and the +-- header magic to match); truncated/oversized/corrupt parts are rejected. SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; reject_mismatch ----------------- t (1 row) --- SQL-function round trip through the live services: producer_begin -> publish --- (gather service appends, channel goes READY) -> consume_wait (send service --- delivers) returns the payload. Proves the full producer -> gather -> send -> --- consumer chain, not just the map. -SELECT anser.producer_begin(10, 1, 1, 'svc_roundtrip', 1) AS begin_ok; - begin_ok ----------- - t -(1 row) - -SELECT anser.publish(10, 1, 1, 'svc_roundtrip', '\x6162'::bytea, false) AS publish_ok; - publish_ok ------------- - t -(1 row) - -SELECT encode(anser.consume_wait(10, 1, 1, 'svc_roundtrip'), 'escape') AS payload; - payload ---------- - ab -(1 row) - --- Producer-begin timeout: begin arms the produce deadline (COLLECTING) but no --- producer publishes; the gather maintenance pass cancels the whole dataset --- after anser.timeout_ms, so the waiting consumer is delivered a cancel and --- consume_wait returns NULL. -SELECT anser.producer_begin(11, 1, 1, 'svc_timeout', 1) AS begin_ok; - begin_ok ----------- - t -(1 row) - -SELECT anser_test_state(11, 1, 1, 'svc_timeout') AS state_after_begin; - state_after_begin -------------------- - COLLECTING -(1 row) - -SELECT anser.consume_wait(11, 1, 1, 'svc_timeout') IS NULL AS consume_cancelled; - consume_cancelled -------------------- - t -(1 row) - --- Client helper loopback: drive AnserClientPublish / AnserClientConsumeWait --- against the local coordinator over libpq, proving the client transport --- end-to-end without a multi-node cluster. -SELECT anser_test_client_roundtrip(4242) AS client_ok; - client_ok ------------ - t -(1 row) - --- Session token: registration, validation, and rejection of bogus token/user --- (the token authenticates the segment -> QD backward connection). -SELECT anser_test_token_roundtrip() AS token_ok; - token_ok ----------- - t -(1 row) - --- Multi-consumer partial delivery: two consumers block concurrently on one --- channel (loopback libpq); one is cancelled mid-wait while the other still --- receives the intact payload. Proves delivery is per-consumer. -SELECT anser_test_multi_consumer(24680) AS partial_delivery_ok; - partial_delivery_ok ---------------------- - t -(1 row) - --- Regression guard (abandoned-consumer recycle): a consumer cancelled mid-wait --- must stop counting toward the channel's expected consumers, so the channel --- still recycles to CONSUMED after the surviving consumer is delivered instead --- of lingering forever in READY with stale data. -SELECT anser_test_abandoned_consumer_recycles(13579) AS recycled_no_stale_data; - recycled_no_stale_data ------------------------- - t -(1 row) - --- Clearing works: with the sweep paused, the cancelled channel above is still --- present as CANCELLED. Re-enable the sweep and run one synchronously; the --- terminal channel is then reclaimed (NOT_FOUND), proving maintenance clears it. -SELECT anser_test_state(1, 1, 2, 'join_timeout') AS before_clear; - before_clear --------------- - CANCELLED -(1 row) - -SELECT anser_test_set_sweep(true); - anser_test_set_sweep ----------------------- - -(1 row) - -SELECT anser_test_sweep(); - anser_test_sweep ------------------- - -(1 row) - -SELECT anser_test_state(1, 1, 2, 'join_timeout') AS after_clear; - after_clear -------------- - NOT_FOUND -(1 row) - --- Payload-DSM lifetime (run last: these toggle the sweep and reclaim terminal --- channels). Each proves the shared channel payload DSM is freed at the right --- moment: (1) success -> freed by the sweep after the last consume; (2) only --- 3/5 producers -> freed when the produce timeout cancels the channel; (3) --- cancelled with consumers attached -> freed by the sweep only after every --- consumer slot has drained (not eagerly at cancel). -SELECT anser_test_dsm_free_on_success() AS dsm_free_success; - dsm_free_success ------------------- - t -(1 row) - -SELECT anser_test_dsm_free_on_timeout() AS dsm_free_timeout; - dsm_free_timeout ------------------- - t -(1 row) - -SELECT anser_test_dsm_free_on_cancel() AS dsm_free_cancel; - dsm_free_cancel ------------------ +-- Producer and consumer driven end to end in one backend: publish a part, let +-- the coordinator side merge it, then receive and query the filter. +SELECT anser_test_node_roundtrip(168); + anser_test_node_roundtrip +--------------------------- t (1 row) DROP EXTENSION anser_test; -DROP EXTENSION anser; diff --git a/gpcontrib/anser/include/anser.h b/gpcontrib/anser/include/anser.h index 9ed57dc307f..63c13356163 100644 --- a/gpcontrib/anser/include/anser.h +++ b/gpcontrib/anser/include/anser.h @@ -18,8 +18,19 @@ * under the License. * * anser.h - * Shared-memory channel map for the Anser adaptive information - * sharing subsystem. + * Shared definitions for the Anser adaptive information sharing + * subsystem. + * + * Anser lets producers on the segments publish a small piece of information + * about a running query -- today a bloom filter over a join-build key -- have + * the coordinator combine the per-segment parts, and hand the result back to + * consumers on the segments, which use it to prune work. + * + * Everything travels over the dispatch connection the coordinator already + * holds open to each segment (see ansersideband.h), so there is no shared + * memory, no background worker and no second connection to authenticate: a + * channel exists only in the coordinator backend running the query, for as + * long as that query runs. * * IDENTIFICATION * gpcontrib/anser/include/anser.h @@ -31,44 +42,14 @@ #include "postgres.h" -#include "datatype/timestamp.h" -#include "storage/dsm.h" -#include "storage/latch.h" -#include "storage/lwlock.h" - #define ANSER_CONDITION_KEY_SIZE 64 /* - * Poll interval (milliseconds) a backend sleeps on its latch between rechecks - * when the awaited change does NOT set its latch, so it must recheck shared - * state itself (waiting for a channel state in AnserWaitForState, or for a free - * submission slot in AnserEnqueueSubmission). Kept small so waits stay - * responsive without busy-looping. - */ -#define ANSER_WAIT_POLL_INTERVAL_MS 10L - -/* - * Safety wakeup (milliseconds) for latch-driven waits where the event always - * sets the waiter's latch (a producer's submission ACK in AnserWaitSubmissionAck, - * a consumer's delivery in AnserWaitSlotResult). The wait normally ends on the - * latch; this timeout only bounds how long a lost wakeup could stall it. - */ -#define ANSER_WAIT_LATCH_TIMEOUT_MS 1000L - -/* - * Wakeup interval (milliseconds) for a background service's main loop. Each - * service runs its data-path pass whenever its latch fires; this timed wakeup - * additionally bounds how long a stale COLLECTING channel or a dead-backend slot - * can linger between latches before periodic maintenance reclaims it. - */ -#define ANSER_SERVICE_WAKEUP_INTERVAL_MS 1000L - -/* - * Registered adaptive-information condition for one running command. + * Identifies one channel: a single condition within one running command. * - * condition_key is an opaque symbol that identifies the condition (the - * optimizer-generated equivalence-class symbols described in the Anser - * paper); the channel map only compares keys for equality. + * condition_key is an opaque symbol naming the condition -- the + * optimizer-generated equivalence-class symbol described in the Anser paper. + * Channels are only ever compared for equality, never ordered. */ typedef struct AnserChannelKey { @@ -78,173 +59,10 @@ typedef struct AnserChannelKey char condition_key[ANSER_CONDITION_KEY_SIZE]; } AnserChannelKey; -/* - * Channel lifecycle: PENDING (created, awaiting producers) -> COLLECTING - * (first part received) -> READY (all expected parts unioned) -> - * CONSUMED (all expected consumers delivered). CANCELLED replaces any - * state on produce timeout, producer cancel, or owning query end. - */ -typedef enum AnserChannelState -{ - ANSER_CHANNEL_PENDING = 0, - ANSER_CHANNEL_COLLECTING, - ANSER_CHANNEL_READY, - ANSER_CHANNEL_CANCELLED, - ANSER_CHANNEL_CONSUMED -} AnserChannelState; - -/* - * One channel in the shared-memory map: the condition key, lifecycle state, - * creator ownership, producer/consumer accounting, and the DSM handle of - * the gathered payload. - */ -typedef struct AnserChannelEntry -{ - AnserChannelKey key; - AnserChannelState state; - Oid creator_role; /* authenticated role that created the channel; - * only this role (or a superuser) may - * produce/consume on it -- see anserfuncs.c */ - int32 expected_producers; - int32 done_producers; - int32 consumers; - int32 expected_consumers; /* consumers to deliver before recycling the - * payload (one per segment); 0 = unknown */ - int32 done_consumers; - Size data_len; - dsm_handle dsm_handle; - TimestampTz updated_at; /* last activity; drives the produce timeout */ -} AnserChannelEntry; - -/* - * Shared control block: effective sizing limits, the background services' - * latches, and the maintenance-sweep switch. - */ -typedef struct AnserControl -{ - uint32 max_channels; - Size max_info_size; - Latch gather_latch; - Latch send_latch; - bool sweep_enabled; /* when false, the periodic maintenance sweep - * leaves terminal channels in place; a test-only - * knob so terminal state can be observed - * deterministically. Emergency (map-full) - * reclamation is unaffected. */ -} AnserControl; - -/* GUCs */ +/* GUCs (defined in anserinit.c). */ extern bool gp_anser_enable; extern bool gp_anser_runtime_filter; -extern bool gp_anser_conn; /* startup-option marker for token-auth conns */ -extern int gp_anser_max_channels; extern int gp_anser_max_info_size; extern int gp_anser_timeout_ms; -extern int gp_anser_max_consumers_per_channel; - -/* - * The two LWLocks of the "anser" named tranche, requested in _PG_init and - * resolved in AnserShmemInit: AnserChannelLock guards the channel map, the - * consumer wait table and the session-token hash; AnserRingLock guards the - * inbound submission queue. - */ -extern LWLock *AnserChannelLock; -extern LWLock *AnserRingLock; - -#define ANSER_LWLOCK_TRANCHE "anser" -#define ANSER_NUM_LWLOCKS 2 - -/* Shared-memory setup. */ -extern Size AnserShmemSize(void); -extern void AnserShmemInit(void); - -/* - * The session-token hash owned by anserauth.c; folded into the sizing and - * setup above so all Anser shared state is requested in one place. - */ -extern Size AnserAuthShmemSize(void); -extern void AnserAuthShmemInit(void); - -/* - * Effective channel-map size (anser.max_channels, or its auto-sizing from - * max_connections * gp_max_slices). Computed once and cached for the life of - * the process; see the definition in anser.c. - */ -extern int AnserMaxChannels(void); - -/* Public channel-manager API. */ -extern bool AnserSubscribe(const AnserChannelKey *channel_key); -extern bool AnserPublish(const AnserChannelKey *channel_key, - const void *payload, Size payload_len, - bool cancelled); -extern bool AnserWaitProducersRegistered(const AnserChannelKey *channel_key, - long timeout_ms); -extern bool AnserWaitReady(const AnserChannelKey *channel_key, - bool *cancelled); -extern bool AnserConsumeReady(const AnserChannelKey *channel_key, - void *buffer, Size buffer_size, Size *payload_len, - bool *cancelled); -extern AnserChannelState AnserChannelGetState(const AnserChannelKey *channel_key, - bool *found); -extern int AnserChannelConsumerCount(const AnserChannelKey *channel_key); -extern int AnserChannelPayloadBytes(const AnserChannelKey *channel_key); -extern void AnserCancelQuery(int gp_session_id, int gp_command_count); -extern void AnserAttachServiceLatch(bool gather_service); -extern void AnserDetachServiceLatch(bool gather_service); -extern void AnserWaitServiceLatch(bool gather_service, long timeout_ms); -extern void AnserWakeServiceLatch(bool gather_service); -extern void AnserServiceMaintenance(void); -extern void AnserSetSweepEnabled(bool enabled); - -/* - * Network-path API. - * - * These entry points back the anser.* SQL functions that remote - * (segment) producers and consumers call over libpq. Unlike the direct - * AnserPublish/AnserConsume* API above, they do not touch the channel payload - * from the calling backend: producers hand their part to the gather service - * through the inbound submission queue and block for an ACK; consumers register - * a wait slot and block until the send service delivers or cancels it. - */ -extern bool AnserProducerBegin(const AnserChannelKey *channel_key, - int expected_producers, - Oid caller_role, bool caller_is_super); -extern bool AnserProducerSubmit(const AnserChannelKey *channel_key, - int expected_producers, - const void *payload, Size payload_len, - bool cancelled, - Oid caller_role, bool caller_is_super); -extern bool AnserConsumerWait(const AnserChannelKey *channel_key, - void **payload, Size *payload_len, - bool *cancelled, - Oid caller_role, bool caller_is_super); - -/* - * Data-path cycles executed by the background services. Each performs one - * non-blocking pass over the shared state; the service loops call them - * whenever their latch fires or the maintenance timer elapses. - */ -extern void AnserGatherServiceCycle(void); -extern void AnserSendServiceCycle(void); - -/* - * Background-service entry points. The two mains are resolved by name from - * this library (bgw_function_name), so they must be exported. - */ -extern PGDLLEXPORT void AnserGatherServiceMain(Datum main_arg); -extern PGDLLEXPORT void AnserSendServiceMain(Datum main_arg); -extern bool AnserStartRule(Datum main_arg); - -/* - * Session-token authentication for the segment -> coordinator libpq - * transport (parallel-retrieve-cursor model; see anser.c). The QD calls - * AnserGetOrCreateSessionToken at plan time; the coordinator backend accepting - * the connection calls AnserConnClaims/AnserConnCheckPassword, which _PG_init - * installs as the core custom-authentication hooks (see libpq/auth.h). - */ -extern char *AnserGetOrCreateSessionToken(Oid user_id); -extern bool AnserSessionTokenIsValid(Oid user_id, const char *token_hex); -extern bool AnserConnClaims(struct Port *port); -extern bool AnserConnCheckPassword(struct Port *port, const char *passwd); #endif /* ANSER_H */ diff --git a/gpcontrib/anser/include/anserbloom.h b/gpcontrib/anser/include/anserbloom.h index f8cc8cefe83..b6b42703906 100644 --- a/gpcontrib/anser/include/anserbloom.h +++ b/gpcontrib/anser/include/anserbloom.h @@ -44,7 +44,6 @@ typedef struct AnserBloomFilterConsumeState AnserBloomFilterConsumeState; /* Producer side: build one Bloom filter part and publish it to the channel. */ /* - * `token` is the QD session token used to authenticate the segment -> QD * libpq connection (parallel-retrieve-cursor model); NULL means connect * without it and rely on pg_hba. Ignored on the coordinator-local path. */ @@ -53,8 +52,7 @@ extern AnserBloomFilterProduceState *ExecInitAnserBloomFilterProduce( int64 total_elems, Size max_payload_bytes, uint32 part_index, - uint32 total_parts, - const char *token); + uint32 total_parts); extern void ExecAnserBloomFilterProduceAddDatum(AnserBloomFilterProduceState *state, Datum value, bool isnull); extern bool ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state); @@ -66,8 +64,7 @@ extern AnserBloomFilterConsumeState *ExecInitAnserBloomFilterConsume( const AnserChannelKey *channel_key, int64 total_elems, Size max_payload_bytes, - uint32 expected_parts, - const char *token); + uint32 expected_parts); extern bool ExecAnserBloomFilterConsume(AnserBloomFilterConsumeState *state, long registration_timeout_ms); extern bloom_filter *ExecAnserBloomFilterConsumerGetFilter( diff --git a/gpcontrib/anser/include/anserclient.h b/gpcontrib/anser/include/anserclient.h deleted file mode 100644 index f3999604255..00000000000 --- a/gpcontrib/anser/include/anserclient.h +++ /dev/null @@ -1,60 +0,0 @@ -/*------------------------------------------------------------------------- - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * anserclient.h - * libpq client helpers that let a remote (segment) backend reach the - * coordinator-resident Anser services over an ordinary connection to the QD. - * - * IDENTIFICATION - * gpcontrib/anser/include/anserclient.h - * - *------------------------------------------------------------------------- - */ -#ifndef ANSERCLIENT_H -#define ANSERCLIENT_H - -#include "anser.h" - -/* - * Publish one producer part to the coordinator. Opens a short-lived libpq - * connection to the QD, runs anser.producer_begin + anser.publish, and - * closes. Fail-open: any connection/protocol error best-effort publishes a - * cancel for the dataset and returns false, never raising. - * - * `token` is the QD session token used to authenticate the connection (the - * parallel-retrieve-cursor model: anser.conn=true + token as password, - * bypassing pg_hba); NULL or "" connects without it and relies on pg_hba. - */ -extern bool AnserClientPublish(const AnserChannelKey *channel_key, - uint32 expected_producers, - const void *payload, Size payload_len, - bool cancelled, const char *token); - -/* - * Wait for delivery of a channel payload from the coordinator. Opens a - * query-lifetime libpq connection to the QD, runs anser.consume_wait, and - * blocks (interruptibly) until the row arrives. On success *payload points at a - * palloc'd copy of the bytes. Connection loss is treated as a cancel for this - * consumer only. `token` is as in AnserClientPublish. - */ -extern bool AnserClientConsumeWait(const AnserChannelKey *channel_key, - void **payload, Size *payload_len, - bool *cancelled, const char *token); - -#endif /* ANSERCLIENT_H */ diff --git a/gpcontrib/anser/include/anserplan.h b/gpcontrib/anser/include/anserplan.h index 704537feb0b..3f4556adb1f 100644 --- a/gpcontrib/anser/include/anserplan.h +++ b/gpcontrib/anser/include/anserplan.h @@ -52,23 +52,19 @@ extern void AnserRegisterRuntimeFilterMethods(void); * tables live). Each wraps `child` in a pass-through CustomScan carrying the * runtime-filter parameters in custom_private; the caller assigns plan_node_id. * `key_attno` is the build (producer) / probe (consumer) join-key attno in the - * child's output tuple. `token` is the QD session token segment executors - * present when connecting back to the QD (NULL or "" means none: fall back to - * pg_hba-driven authentication). + * child's output tuple. */ extern CustomScan *AnserBuildBloomProducerScan(Plan *child, AttrNumber key_attno, uint32 condition_id, const char *condition_key, int64 total_elems, Size max_payload_bytes, - int64 planned_bytes, - const char *token); + int64 planned_bytes); extern CustomScan *AnserBuildBloomConsumerScan(Plan *child, AttrNumber key_attno, uint32 condition_id, const char *condition_key, int64 total_elems, Size max_payload_bytes, - int64 planned_bytes, - const char *token); + int64 planned_bytes); #endif /* ANSERPLAN_H */ diff --git a/gpcontrib/anser/include/ansersideband.h b/gpcontrib/anser/include/ansersideband.h new file mode 100644 index 00000000000..e671b57d3a4 --- /dev/null +++ b/gpcontrib/anser/include/ansersideband.h @@ -0,0 +1,112 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * ansersideband.h + * Anser transport over the existing QD <-> QE dispatch connection. + * + * Instead of opening a second libpq connection back to the coordinator, a + * segment executor reuses the connection the dispatcher already holds open: + * QE -> QD travels as a NOTIFY (the model nextval() uses, see + * cdb_sequence_nextval_qe in commands/sequence.c), and QD -> QE as a + * GP_SIDEBAND_MESSAGE the waiting QE reads off its own socket. + * + * The two directions are not symmetric, and the wire formats differ for a + * reason. A NOTIFY payload is delivered by pq_sendstring(), so it must be a + * NUL-free C string -- hence the text header and base64 body. The QD -> QE + * push is written with pqPutnchar(), which performs no conversion, so the + * merged filter travels as raw binary. That matters: the merged payload is + * sent once per consumer, while each part is sent once. + * + * IDENTIFICATION + * gpcontrib/anser/include/ansersideband.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERSIDEBAND_H +#define ANSERSIDEBAND_H + +#include "anser.h" + +struct CdbDispatchResult; /* #include "cdb/cdbdispatchresult.h" */ +struct pgNotify; /* #include "libpq-fe.h" */ + +/* + * NOTIFY channel for QE -> QD Anser traffic. The dispatcher hands notifies it + * does not recognize to cdbdisp_notify_hook, where we match on this name. + */ +#define ANSER_NOTIFY_CHANNEL "anser_rf" + +/* First token of every QE -> QD payload; bump when the format changes. */ +#define ANSER_WIRE_TAG "anser1" + +/* Message kinds (QE -> QD). */ +#define ANSER_WIRE_KIND_PART 'P' /* a producer's serialized part */ +#define ANSER_WIRE_KIND_SUBSCRIBE 'S' /* a consumer registering interest */ + +/* Flag bits, shared by both directions. */ +#define ANSER_WIRE_F_CANCELLED 0x0001 + +/* + * QE side (ansersideband.c). + * + * AnserSidebandPublish is fire-and-forget: unlike the libpq transport it does + * not wait for the coordinator to acknowledge the part. + * + * AnserSidebandConsumeWait blocks on this backend's own dispatch socket until + * the merged payload arrives, the channel is cancelled, or timeout_ms elapses. + * On success *payload is palloc'd in the caller's context. A false return + * always means "run unfiltered", never an error. + */ +extern bool AnserSidebandPublish(const AnserChannelKey *channel_key, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, + bool cancelled); +extern bool AnserSidebandConsumeWait(const AnserChannelKey *channel_key, + void **payload, Size *payload_len, + bool *cancelled, long timeout_ms); + +/* + * QD side (anserdispatch.c). + * + * AnserDispatchNotifyHandler is installed as cdbdisp_notify_hook; it folds + * arriving parts and pushes the merged payload to subscribers. The Local + * variants serve producers and consumers running on the coordinator itself, + * which have no dispatch connection to themselves and so operate on the same + * per-query channel table directly. + */ +extern bool AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, + struct pgNotify *notify); +extern bool AnserDispatchLocalPublish(const AnserChannelKey *channel_key, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, + bool cancelled); +extern bool AnserDispatchLocalConsume(const AnserChannelKey *channel_key, + void **payload, Size *payload_len, + bool *cancelled); + +/* + * Drop per-query state. AnserSidebandResetAll does both sides and is what + * the executor/transaction-end callbacks call; the halves are exposed because + * each lives with the state it owns. + */ +extern void AnserDispatchReset(void); +extern void AnserSidebandResetInbox(void); +extern void AnserSidebandResetAll(void); + +#endif /* ANSERSIDEBAND_H */ diff --git a/gpcontrib/anser/sql/anser_runtime_filter.sql b/gpcontrib/anser/sql/anser_runtime_filter.sql index 8e82779e2bb..c8a100a2e7c 100644 --- a/gpcontrib/anser/sql/anser_runtime_filter.sql +++ b/gpcontrib/anser/sql/anser_runtime_filter.sql @@ -6,9 +6,6 @@ -- degrades to the leaf case. Either way the feature must (a) inject the -- producer/consumer nodes and (b) never change query results. --- The plan pass only injects when the extension is installed in this --- database, since that is what makes the segments' callbacks resolvable. -CREATE EXTENSION anser; -- Deterministic plan shape: force a hash join. SET enable_nestloop = off; @@ -144,4 +141,3 @@ RESET optimizer; RESET enable_nestloop; RESET enable_mergejoin; -DROP EXTENSION anser; diff --git a/gpcontrib/anser/sql/anser_test.sql b/gpcontrib/anser/sql/anser_test.sql index 47a7f98a9b3..bd2c3417f03 100644 --- a/gpcontrib/anser/sql/anser_test.sql +++ b/gpcontrib/anser/sql/anser_test.sql @@ -1,117 +1,20 @@ -CREATE EXTENSION anser_test CASCADE; +CREATE EXTENSION anser_test; --- Pause the background maintenance sweep so terminal (CANCELLED/CONSUMED) --- channels stay observable and the state assertions below are deterministic --- rather than racing the live gather/send services. Re-enabled at the end, --- where we prove the sweep actually reclaims them. -SELECT anser_test_set_sweep(false); - --- Happy path: one condition, two producers, two consumers. Each producer --- publishes a real bloom part (multi-payload combine is bloom-only now); the two --- parts union on the coordinator, so each consumer's received filter contains --- both producers' values. -SELECT anser_test_register_condition(1, 1, 1, 'join_a', 2); -SELECT anser_test_subscribe(1, 1, 1, 'join_a'); -SELECT anser_test_subscribe(1, 1, 1, 'join_a'); -SELECT anser_test_publish_value(1, 1, 1, 'join_a', 10); -SELECT anser_test_state(1, 1, 1, 'join_a'); -SELECT anser_test_publish_value(1, 1, 1, 'join_a', 20); -SELECT anser_test_state(1, 1, 1, 'join_a'); -SELECT anser_test_consume_has(1, 1, 1, 'join_a', 10); -SELECT anser_test_consume_has(1, 1, 1, 'join_a', 20); -SELECT anser_test_state(1, 1, 1, 'join_a'); - --- Timeout/cancel path: no producer publishes. -SELECT anser_test_register_condition(1, 1, 2, 'join_timeout', 1); -SELECT anser_test_subscribe(1, 1, 2, 'join_timeout'); -SELECT anser_test_consume(1, 1, 2, 'join_timeout', 1) IS NULL; -SELECT anser_test_state(1, 1, 2, 'join_timeout'); - --- Input validation: negative IDs/counts and overlong condition keys fail. -SELECT anser_test_register_condition(1, 1, -1, 'bad_id', 1); -SELECT anser_test_register_condition(1, 1, 3, 'bad_count', 0); -SELECT anser_test_register_condition(1, 1, 3, repeat('x', 64), 1); -SELECT anser_test_subscribe(1, 1, -1, 'bad_id'); -SELECT anser_test_subscribe(1, 1, 3, repeat('x', 64)); - --- Query-level cancellation touches all channels for the command. -SELECT anser_test_register_condition(1, 2, 1, 'join_b', 1); -SELECT anser_test_register_condition(1, 2, 2, 'join_c', 1); -SELECT anser_test_cancel_query(1, 2); -SELECT anser_test_state(1, 2, 1, 'join_b'); -SELECT anser_test_state(1, 2, 2, 'join_c'); - --- GUC sizing guard: AnserMaxChannels() is memoized at postmaster start, so a --- per-session SET gp_max_slices must not change the reported channel-map size --- (otherwise the shared arrays and the Len functions would disagree). -SELECT anser_test_max_channels_stable_across_slices() AS max_channels_stable; - --- Bloom payload protocol and standalone producer/consumer helpers. +-- Bloom payload protocol: serialize one part and read it back. SELECT anser_test_bloom_roundtrip('bf_roundtrip', 42); --- In-place fold: same-size union mutates the buffer; a mismatched size is --- rejected. This is the coordinator's only combine path (first part is stored --- verbatim, every later part folds in here). + +-- In-place fold: same-size union mutates the accumulator; a mismatched size is +-- rejected. This is the coordinator's only combine path (the first part is +-- kept verbatim, every later part folds into it). SELECT anser_test_bloom_fold_inplace() AS fold_inplace_ok; -SELECT anser_test_node_roundtrip(168); --- Safety regression: the consumer rebuilds the filter from its own parameters and --- requires the received bitset to be exactly the expected size (and the header --- magic to match); truncated/oversized/corrupt parts are rejected (fail open). +-- Safety regression: the consumer rebuilds the filter from its own parameters +-- and requires the received bitset to be exactly the expected size (and the +-- header magic to match); truncated/oversized/corrupt parts are rejected. SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; --- SQL-function round trip through the live services: producer_begin -> publish --- (gather service appends, channel goes READY) -> consume_wait (send service --- delivers) returns the payload. Proves the full producer -> gather -> send -> --- consumer chain, not just the map. -SELECT anser.producer_begin(10, 1, 1, 'svc_roundtrip', 1) AS begin_ok; -SELECT anser.publish(10, 1, 1, 'svc_roundtrip', '\x6162'::bytea, false) AS publish_ok; -SELECT encode(anser.consume_wait(10, 1, 1, 'svc_roundtrip'), 'escape') AS payload; - --- Producer-begin timeout: begin arms the produce deadline (COLLECTING) but no --- producer publishes; the gather maintenance pass cancels the whole dataset --- after anser.timeout_ms, so the waiting consumer is delivered a cancel and --- consume_wait returns NULL. -SELECT anser.producer_begin(11, 1, 1, 'svc_timeout', 1) AS begin_ok; -SELECT anser_test_state(11, 1, 1, 'svc_timeout') AS state_after_begin; -SELECT anser.consume_wait(11, 1, 1, 'svc_timeout') IS NULL AS consume_cancelled; - --- Client helper loopback: drive AnserClientPublish / AnserClientConsumeWait --- against the local coordinator over libpq, proving the client transport --- end-to-end without a multi-node cluster. -SELECT anser_test_client_roundtrip(4242) AS client_ok; - --- Session token: registration, validation, and rejection of bogus token/user --- (the token authenticates the segment -> QD backward connection). -SELECT anser_test_token_roundtrip() AS token_ok; - --- Multi-consumer partial delivery: two consumers block concurrently on one --- channel (loopback libpq); one is cancelled mid-wait while the other still --- receives the intact payload. Proves delivery is per-consumer. -SELECT anser_test_multi_consumer(24680) AS partial_delivery_ok; - --- Regression guard (abandoned-consumer recycle): a consumer cancelled mid-wait --- must stop counting toward the channel's expected consumers, so the channel --- still recycles to CONSUMED after the surviving consumer is delivered instead --- of lingering forever in READY with stale data. -SELECT anser_test_abandoned_consumer_recycles(13579) AS recycled_no_stale_data; - --- Clearing works: with the sweep paused, the cancelled channel above is still --- present as CANCELLED. Re-enable the sweep and run one synchronously; the --- terminal channel is then reclaimed (NOT_FOUND), proving maintenance clears it. -SELECT anser_test_state(1, 1, 2, 'join_timeout') AS before_clear; -SELECT anser_test_set_sweep(true); -SELECT anser_test_sweep(); -SELECT anser_test_state(1, 1, 2, 'join_timeout') AS after_clear; - --- Payload-DSM lifetime (run last: these toggle the sweep and reclaim terminal --- channels). Each proves the shared channel payload DSM is freed at the right --- moment: (1) success -> freed by the sweep after the last consume; (2) only --- 3/5 producers -> freed when the produce timeout cancels the channel; (3) --- cancelled with consumers attached -> freed by the sweep only after every --- consumer slot has drained (not eagerly at cancel). -SELECT anser_test_dsm_free_on_success() AS dsm_free_success; -SELECT anser_test_dsm_free_on_timeout() AS dsm_free_timeout; -SELECT anser_test_dsm_free_on_cancel() AS dsm_free_cancel; +-- Producer and consumer driven end to end in one backend: publish a part, let +-- the coordinator side merge it, then receive and query the filter. +SELECT anser_test_node_roundtrip(168); DROP EXTENSION anser_test; -DROP EXTENSION anser; diff --git a/gpcontrib/anser/src/anser.c b/gpcontrib/anser/src/anser.c deleted file mode 100644 index 35d624daa8c..00000000000 --- a/gpcontrib/anser/src/anser.c +++ /dev/null @@ -1,2041 +0,0 @@ -/*------------------------------------------------------------------------- - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * anser.c - * Shared-memory channel map for the Anser adaptive information - * sharing subsystem. - * - * IDENTIFICATION - * gpcontrib/anser/src/anser.c - * - *------------------------------------------------------------------------- - */ -#include "postgres.h" - -#include "anser.h" -#include "anserfilter.h" -#include "cdb/cdbutil.h" -#include "cdb/cdbvars.h" -#include "miscadmin.h" -#include "storage/dsm_impl.h" -#include "storage/ipc.h" -#include "storage/lwlock.h" -#include "storage/proc.h" -#include "storage/procarray.h" -#include "storage/shmem.h" -#include "utils/builtins.h" -#include "utils/guc.h" -#include "utils/hsearch.h" -#include "utils/timestamp.h" -#include "utils/wait_event.h" - -#define ANSER_CONTROL_NAME "Anser Control" -#define ANSER_CHANNEL_HASH_NAME "Anser Channel Hash" -#define ANSER_SUBMISSION_QUEUE_NAME "Anser Submission Queue" -#define ANSER_WAIT_TABLE_NAME "Anser Consumer Wait Table" - -bool gp_anser_enable = false; -bool gp_anser_runtime_filter = false; -bool gp_anser_conn = false; /* marker GUC, set only via startup options */ -int gp_anser_max_channels = 0; /* 0 = auto (see AnserMaxChannels) */ -int gp_anser_max_info_size = 64 * 1024 * 1024 + 1024 * 1024; -int gp_anser_timeout_ms = 1000; -int gp_anser_max_consumers_per_channel = 64; - -/* - * Fallback per-connection channel budget used to auto-size the channel map when - * gp_max_slices is left unbounded (0). Each concurrent query can open at most - * one channel per runtime-filter slice, so the map is sized for - * max_connections * max_slices; when max_slices is unbounded we assume this many - * filter-carrying slices per query. Only used to derive the default; an - * explicit anser.max_channels overrides it entirely. - */ -#define ANSER_AUTO_SLICES_PER_CONN 8 - -/* - * Inbound submission queue. - * - * A remote producer backend (anser.publish) hands one part to the gather - * service through a free slot here, then blocks on its own proc latch until the - * gather service flips the slot to a terminal state and wakes it. The producer - * keeps its payload DSM segment attached for the whole wait, so the gather - * service can attach the same handle without a pin/unpin dance. - */ -typedef enum AnserSubmissionState -{ - ANSER_SUBMIT_FREE = 0, /* slot available */ - ANSER_SUBMIT_PENDING, /* filled by producer, awaiting gather */ - ANSER_SUBMIT_ACCEPTED, /* gather appended the part */ - ANSER_SUBMIT_REJECTED /* gather refused (cancel/overflow/lost DSM) */ -} AnserSubmissionState; - -typedef struct AnserSubmissionEntry -{ - AnserSubmissionState state; - AnserChannelKey key; - int32 expected_producers; - dsm_handle dsm_handle; /* producer's part, DSM_HANDLE_INVALID if none */ - Size len; - bool cancelled; - int producer_pid; - Latch *producer_latch; -} AnserSubmissionEntry; - -/* - * Consumer wait table. - * - * A remote consumer backend (anser.consume_wait) registers a slot and blocks - * on its proc latch. The send service, when a channel becomes READY, copies the - * payload into a fresh pinned DSM segment per waiting consumer, stamps the - * handle here, and wakes the consumer, which attaches, copies the bytes out, and - * frees the segment. On CANCELLED it just flips the slot and wakes. - */ -typedef enum AnserWaitSlotState -{ - ANSER_WAIT_FREE = 0, /* slot available */ - ANSER_WAIT_WAITING, /* consumer registered, blocked */ - ANSER_WAIT_DELIVERED, /* send service stamped a payload segment */ - ANSER_WAIT_CANCELLED /* send service cancelled this consumer */ -} AnserWaitSlotState; - -typedef struct AnserWaitSlot -{ - AnserWaitSlotState state; - AnserChannelKey key; - dsm_handle dsm_handle; /* per-consumer payload copy, pinned by sender */ - Size len; - int consumer_pid; - Latch *consumer_latch; -} AnserWaitSlot; - -/* - * The named tranche requested in _PG_init, resolved in AnserShmemInit(). NULL - * until then, which is one of the things AnserInitialized() checks. - */ -LWLock *AnserChannelLock = NULL; -LWLock *AnserRingLock = NULL; - -static AnserControl *AnserCtl = NULL; -static HTAB *AnserChannelHash = NULL; -static AnserSubmissionEntry *AnserSubmissionQueue = NULL; -static AnserWaitSlot *AnserWaitTable = NULL; -/* - * Data-path operations -- the internal machinery the public API and the gather/ - * send service cycles drive: producer submissions, gather apply, consumer wait - * slots, payload storage/delivery, and the maintenance sweeps. - */ -static int AnserEnqueueSubmission(const AnserChannelKey *channel_key, - int expected_producers, dsm_handle handle, - Size len, bool cancelled); -static bool AnserWaitSubmissionAck(int slot); -static void AnserAbandonSubmission(int slot); -static bool AnserGatherApply(const AnserChannelKey *channel_key, - int expected_producers, dsm_handle handle, - Size len, bool cancelled); -static int AnserRegisterWaitSlot(const AnserChannelKey *channel_key); -static bool AnserWaitSlotResult(int slot, void **payload, Size *payload_len, - bool *cancelled); -static void AnserAbandonWaitSlot(int slot); -static bool AnserWaitForState(const AnserChannelKey *channel_key, - long timeout_ms, bool registration_only, - bool *cancelled); -static bool AnserStorePayloadDSM(AnserChannelEntry *entry, - const void *payload, Size payload_len); -static void AnserReleasePayloadDSM(AnserChannelEntry *entry); -static bool AnserDeliverChannelData(const AnserChannelEntry *entry, - void *buffer, Size buffer_size, - Size *payload_len); -static void AnserCancelStaleChannels(void); -static void AnserSweepOrphanChannels(void); -static void AnserReapSubmissionSlots(void); -static void AnserReapWaitSlots(void); - -/* - * Internal helpers -- shared-memory sizing, small predicates, and key building - * used by the operations above. - */ -static bool AnserInitialized(void); -static Size AnserChannelHashSize(void); -static int AnserSubmissionQueueLen(void); -static int AnserWaitTableLen(void); -static Size AnserSubmissionQueueSize(void); -static Size AnserWaitTableSize(void); -static bool AnserPidIsLive(int pid); -static bool AnserChannelHasWaiters(const AnserChannelKey *channel_key); -static bool AnserChannelOwnerIsAlive(const AnserChannelEntry *entry); -static bool AnserChannelAccessAllowed(const AnserChannelKey *channel_key, - Oid caller_role, bool caller_is_super, - bool *found); - -/* - * Shared-memory setup -- one-time structure initialization at postmaster start. - */ -static void AnserInitializeControl(bool found); -static void AnserInitializeChannelHash(void); -static void AnserInitializeSubmissionQueue(bool found); -static void AnserInitializeWaitTable(bool found); - -Size -AnserShmemSize(void) -{ - Size size = 0; - - if (!gp_anser_enable) - return 0; - - size = add_size(size, MAXALIGN(sizeof(AnserControl))); - size = add_size(size, AnserChannelHashSize()); - size = add_size(size, AnserSubmissionQueueSize()); - size = add_size(size, AnserWaitTableSize()); - size = add_size(size, AnserAuthShmemSize()); - - return size; -} - -/* - * Create (postmaster) or attach to (EXEC_BACKEND child) the Anser shared state. - * Called from the shmem_startup_hook, after _PG_init has requested both the - * space and the LWLock tranche. - */ -void -AnserShmemInit(void) -{ - LWLockPadded *locks; - bool found; - - if (!gp_anser_enable) - return; - - locks = GetNamedLWLockTranche(ANSER_LWLOCK_TRANCHE); - AnserChannelLock = &locks[0].lock; - AnserRingLock = &locks[1].lock; - - AnserCtl = (AnserControl *) ShmemInitStruct(ANSER_CONTROL_NAME, - sizeof(AnserControl), - &found); - AnserInitializeControl(found); - AnserInitializeChannelHash(); - - AnserSubmissionQueue = (AnserSubmissionEntry *) - ShmemInitStruct(ANSER_SUBMISSION_QUEUE_NAME, - AnserSubmissionQueueSize(), &found); - AnserInitializeSubmissionQueue(found); - - AnserWaitTable = (AnserWaitSlot *) - ShmemInitStruct(ANSER_WAIT_TABLE_NAME, - AnserWaitTableSize(), &found); - AnserInitializeWaitTable(found); - - AnserAuthShmemInit(); -} - -bool -AnserSubscribe(const AnserChannelKey *channel_key) -{ - AnserChannelEntry *entry; - bool found; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &found); - if (!found) - { - LWLockRelease(AnserChannelLock); - return false; - } - - entry->consumers++; - entry->updated_at = GetCurrentTimestamp(); - LWLockRelease(AnserChannelLock); - - return true; -} - -bool -AnserPublish(const AnserChannelKey *channel_key, const void *payload, - Size payload_len, bool cancelled) -{ - AnserChannelEntry *entry; - bool found; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &found); - if (!found) - { - LWLockRelease(AnserChannelLock); - return false; - } - - if (cancelled) - { - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = GetCurrentTimestamp(); - SetLatch(&AnserCtl->send_latch); - LWLockRelease(AnserChannelLock); - return true; - } - - if (payload_len > (Size) gp_anser_max_info_size) - { - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = GetCurrentTimestamp(); - SetLatch(&AnserCtl->send_latch); - LWLockRelease(AnserChannelLock); - return false; - } - - if (entry->state == ANSER_CHANNEL_PENDING) - entry->state = ANSER_CHANNEL_COLLECTING; - - if (payload != NULL && payload_len > 0) - { - if (!AnserStorePayloadDSM(entry, payload, payload_len)) - { - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = GetCurrentTimestamp(); - SetLatch(&AnserCtl->send_latch); - LWLockRelease(AnserChannelLock); - return false; - } - } - - entry->done_producers++; - if (entry->done_producers >= entry->expected_producers) - entry->state = ANSER_CHANNEL_READY; - entry->updated_at = GetCurrentTimestamp(); - - SetLatch(&AnserCtl->gather_latch); - SetLatch(&AnserCtl->send_latch); - LWLockRelease(AnserChannelLock); - - return true; -} - -bool -AnserWaitProducersRegistered(const AnserChannelKey *channel_key, long timeout_ms) -{ - bool cancelled = false; - - return AnserWaitForState(channel_key, timeout_ms, true, &cancelled) && - !cancelled; -} - -bool -AnserWaitReady(const AnserChannelKey *channel_key, bool *cancelled) -{ - return AnserWaitForState(channel_key, -1, false, cancelled); -} - -bool -AnserConsumeReady(const AnserChannelKey *channel_key, void *buffer, - Size buffer_size, Size *payload_len, bool *cancelled) -{ - AnserChannelEntry *entry; - bool found; - bool ready; - bool is_cancelled; - bool delivered = false; - - Assert(payload_len != NULL); - Assert(cancelled != NULL); - - *payload_len = 0; - *cancelled = false; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - LWLockAcquire(AnserChannelLock, LW_SHARED); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &found); - if (!found) - { - LWLockRelease(AnserChannelLock); - return false; - } - - ready = (entry->state == ANSER_CHANNEL_READY); - is_cancelled = (entry->state == ANSER_CHANNEL_CANCELLED); - if (ready && !is_cancelled) - delivered = AnserDeliverChannelData(entry, buffer, buffer_size, - payload_len); - LWLockRelease(AnserChannelLock); - - if (!ready || is_cancelled || !delivered) - { - if (is_cancelled) - *cancelled = true; - return false; - } - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &found); - if (!found) - { - LWLockRelease(AnserChannelLock); - return false; - } - - is_cancelled = (entry->state == ANSER_CHANNEL_CANCELLED); - if (is_cancelled) - { - *cancelled = true; - LWLockRelease(AnserChannelLock); - return false; - } - - if (entry->state != ANSER_CHANNEL_READY) - { - LWLockRelease(AnserChannelLock); - return false; - } - - entry->done_consumers++; - if (entry->consumers == 0 || entry->done_consumers >= entry->consumers) - { - entry->state = ANSER_CHANNEL_CONSUMED; - AnserReleasePayloadDSM(entry); - } - entry->updated_at = GetCurrentTimestamp(); - LWLockRelease(AnserChannelLock); - - return true; -} - -AnserChannelState -AnserChannelGetState(const AnserChannelKey *channel_key, bool *found) -{ - AnserChannelEntry *entry; - bool local_found; - AnserChannelState state = ANSER_CHANNEL_CANCELLED; - - if (found != NULL) - *found = false; - - if (!AnserInitialized() || channel_key == NULL) - return state; - - LWLockAcquire(AnserChannelLock, LW_SHARED); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &local_found); - if (local_found) - state = entry->state; - LWLockRelease(AnserChannelLock); - - if (found != NULL) - *found = local_found; - return state; -} - -/* - * Bytes of payload the channel currently holds, or -1 if it is not in the map. - * Introspection for tests observing the payload-DSM lifetime: > 0 while a - * payload is pinned, 0 once it has been freed but the entry still lingers, and - * -1 once the entry has been reclaimed (payload freed and removed). - */ -int -AnserChannelPayloadBytes(const AnserChannelKey *channel_key) -{ - AnserChannelEntry *entry; - bool found; - int bytes = -1; - - if (!AnserInitialized() || channel_key == NULL) - return -1; - - LWLockAcquire(AnserChannelLock, LW_SHARED); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &found); - if (found) - bytes = (int) entry->data_len; - LWLockRelease(AnserChannelLock); - - return bytes; -} - -/* - * Number of consumers currently subscribed to a channel, or -1 if the channel - * is unknown. Read-only introspection used by tests to sequence a publish only - * after all expected consumers have registered. - */ -int -AnserChannelConsumerCount(const AnserChannelKey *channel_key) -{ - AnserChannelEntry *entry; - bool found; - int count = -1; - - if (!AnserInitialized() || channel_key == NULL) - return -1; - - LWLockAcquire(AnserChannelLock, LW_SHARED); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, - HASH_FIND, &found); - if (found) - count = entry->consumers; - LWLockRelease(AnserChannelLock); - - return count; -} - -void -AnserAttachServiceLatch(bool gather_service) -{ - if (!AnserInitialized()) - return; - - OwnLatch(gather_service ? &AnserCtl->gather_latch : &AnserCtl->send_latch); -} - -void -AnserDetachServiceLatch(bool gather_service) -{ - if (!AnserInitialized()) - return; - - DisownLatch(gather_service ? &AnserCtl->gather_latch : &AnserCtl->send_latch); -} - -void -AnserWaitServiceLatch(bool gather_service, long timeout_ms) -{ - if (!AnserInitialized()) - { - (void) WaitLatch(MyLatch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - timeout_ms, - PG_WAIT_EXTENSION); - ResetLatch(MyLatch); - return; - } - - if (gather_service) - { - (void) WaitLatch(&AnserCtl->gather_latch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - timeout_ms, - PG_WAIT_EXTENSION); - ResetLatch(&AnserCtl->gather_latch); - } - else - { - (void) WaitLatch(&AnserCtl->send_latch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - timeout_ms, - PG_WAIT_EXTENSION); - ResetLatch(&AnserCtl->send_latch); - } -} - -void -AnserWakeServiceLatch(bool gather_service) -{ - if (!AnserInitialized()) - return; - - if (gather_service) - SetLatch(&AnserCtl->gather_latch); - else - SetLatch(&AnserCtl->send_latch); -} - -void -AnserServiceMaintenance(void) -{ - if (!AnserInitialized()) - return; - - /* - * The periodic sweep is gated so tests can pause reclamation and observe - * terminal (CANCELLED/CONSUMED) channels deterministically. Emergency - * reclamation on a full map is not gated -- it calls the sweep directly. - */ - if (!AnserCtl->sweep_enabled) - return; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - AnserSweepOrphanChannels(); - LWLockRelease(AnserChannelLock); -} - -/* - * Enable or disable the periodic maintenance sweep. Test-only: production - * always leaves it enabled. Toggling it lets a test freeze terminal channels - * in place (to assert their state) and then re-enable + force a sweep to prove - * reclamation works. - */ -void -AnserSetSweepEnabled(bool enabled) -{ - if (!AnserInitialized()) - return; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - AnserCtl->sweep_enabled = enabled; - LWLockRelease(AnserChannelLock); -} - -void -AnserCancelQuery(int gp_session_id, int gp_command_count) -{ - HASH_SEQ_STATUS status; - AnserChannelEntry *entry; - - if (!AnserInitialized()) - return; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - hash_seq_init(&status, AnserChannelHash); - while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) - { - if (entry->key.gp_session_id == gp_session_id && - entry->key.gp_command_count == gp_command_count) - { - /* - * Do not free the payload DSM here: a READY channel may already have - * DELIVERED wait slots borrowing it (consumers mid-read). Just mark - * it cancelled; the sweep releases the DSM once no slot still - * references it (unlike the gather/timeout cancels, this one can hit - * a channel past READY). - */ - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = GetCurrentTimestamp(); - } - } - SetLatch(&AnserCtl->send_latch); - LWLockRelease(AnserChannelLock); -} - -/* - * Register/refresh a channel on behalf of a remote producer and arm the produce - * deadline by moving it to COLLECTING. Idempotent: repeated begins from the - * several producers of one channel just refresh expected_producers and the - * deadline. This is the "a producer opened a connection" signal. - */ -bool -AnserProducerBegin(const AnserChannelKey *channel_key, int expected_producers, - Oid caller_role, bool caller_is_super) -{ - AnserChannelEntry *entry; - bool found; - TimestampTz now; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - if (expected_producers <= 0) - { - ereport(WARNING, - (errmsg("could not begin Anser channel: expected producers must be greater than zero"))); - return false; - } - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, - HASH_ENTER_NULL, &found); - if (entry == NULL) - { - AnserSweepOrphanChannels(); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, - HASH_ENTER_NULL, &found); - } - - if (entry == NULL) - { - LWLockRelease(AnserChannelLock); - ereport(WARNING, - (errmsg("could not begin Anser channel: channel map is full"))); - return false; - } - - /* - * A live channel belongs to the role that created it: another role may not - * hijack it by guessing its (session, command, condition) key. - */ - if (found && - entry->state != ANSER_CHANNEL_CANCELLED && - entry->state != ANSER_CHANNEL_CONSUMED && - !caller_is_super && - OidIsValid(entry->creator_role) && - entry->creator_role != caller_role) - { - LWLockRelease(AnserChannelLock); - return false; - } - - now = GetCurrentTimestamp(); - if (!found) - { - MemSet(entry, 0, sizeof(AnserChannelEntry)); - entry->key = *channel_key; - entry->state = ANSER_CHANNEL_PENDING; - entry->creator_role = caller_role; - entry->dsm_handle = DSM_HANDLE_INVALID; - } - else if (entry->state == ANSER_CHANNEL_CANCELLED || - entry->state == ANSER_CHANNEL_CONSUMED) - { - /* - * Terminal channel already on this key -- an anomaly, since keys are - * unique per (session, command, condition). Do not resurrect it: - * reviving a completed/aborted channel could strand a straggler wait slot - * or hand one query's data to another. Fail so the caller falls open and - * the sweep reclaims the leftover. - */ - LWLockRelease(AnserChannelLock); - return false; - } - - entry->expected_producers = expected_producers; - - /* - * Fix the consumer count when the channel is created, rather than having - * every consumer re-assert it: it is a property of the query topology (one - * consumer per segment executing the consumer slice) known here. The send - * service must deliver to all of them before recycling the payload. - * - * getgpsegmentCount() is the per-segment count, which matches the - * segment-executed filters Anser targets; a coordinator-only consumer - * slice would want 1, which this proxy does not represent. - */ - entry->expected_consumers = getgpsegmentCount(); - - if (entry->state == ANSER_CHANNEL_PENDING) - entry->state = ANSER_CHANNEL_COLLECTING; - entry->updated_at = now; - - SetLatch(&AnserCtl->gather_latch); - LWLockRelease(AnserChannelLock); - - return true; -} - -/* - * Remote-producer publish: hand one part to the gather service and block for - * its ACK. The payload is copied into a DSM segment kept attached for the whole - * wait, so the gather service can read it by handle. Fail-open: any local - * failure downgrades the submission to a cancel so the dataset dies cleanly - * rather than hanging consumers. - */ -bool -AnserProducerSubmit(const AnserChannelKey *channel_key, - int expected_producers, const void *payload, - Size payload_len, bool cancelled, - Oid caller_role, bool caller_is_super) -{ - dsm_segment *seg = NULL; - dsm_handle handle = DSM_HANDLE_INVALID; - int slot; - bool accepted; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - /* Refuse to feed a channel owned by a different role. */ - if (!AnserChannelAccessAllowed(channel_key, caller_role, caller_is_super, - NULL)) - return false; - - if (!cancelled && payload_len > (Size) gp_anser_max_info_size) - { - cancelled = true; - payload = NULL; - payload_len = 0; - } - - if (!cancelled && payload != NULL && payload_len > 0) - { - seg = dsm_create(payload_len, DSM_CREATE_NULL_IF_MAXSEGMENTS); - if (seg == NULL) - { - cancelled = true; - payload_len = 0; - } - else - { - memcpy(dsm_segment_address(seg), payload, payload_len); - handle = dsm_segment_handle(seg); - } - } - - slot = AnserEnqueueSubmission(channel_key, expected_producers, handle, - cancelled ? 0 : payload_len, cancelled); - if (slot < 0) - { - if (seg != NULL) - dsm_detach(seg); - return false; - } - - /* - * If we are interrupted while waiting for the ACK, reclaim our submission - * slot so it does not linger until this backend exits. (Our payload DSM is - * released by the aborting transaction's resource owner.) - */ - PG_TRY(); - { - accepted = AnserWaitSubmissionAck(slot); - } - PG_CATCH(); - { - AnserAbandonSubmission(slot); - PG_RE_THROW(); - } - PG_END_TRY(); - - if (seg != NULL) - dsm_detach(seg); - - return accepted; -} - -/* - * Give up a submission slot after the producer is interrupted mid-wait. If the - * gather service already finished with it, reclaim it now; if it is still - * pending, detach ourselves (clear pid/latch) so the gather neither wakes a gone - * backend nor leaves the slot for us to reclaim -- the reaper frees it once the - * gather marks it terminal. - */ -static void -AnserAbandonSubmission(int slot) -{ - AnserSubmissionEntry *e = &AnserSubmissionQueue[slot]; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - if (e->producer_pid == MyProcPid) - { - if (e->state == ANSER_SUBMIT_ACCEPTED || - e->state == ANSER_SUBMIT_REJECTED) - e->state = ANSER_SUBMIT_FREE; - else if (e->state == ANSER_SUBMIT_PENDING) - { - e->producer_pid = 0; - e->producer_latch = NULL; - } - } - LWLockRelease(AnserRingLock); -} - -/* - * Claim a free submission slot (waiting for one if the queue is momentarily - * full) and mark it PENDING for the gather service. Returns the slot index. - */ -static int -AnserEnqueueSubmission(const AnserChannelKey *channel_key, - int expected_producers, dsm_handle handle, - Size len, bool cancelled) -{ - int len_slots = AnserSubmissionQueueLen(); - - for (;;) - { - int i; - - CHECK_FOR_INTERRUPTS(); - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - for (i = 0; i < len_slots; i++) - { - AnserSubmissionEntry *e = &AnserSubmissionQueue[i]; - - if (e->state == ANSER_SUBMIT_FREE) - { - e->key = *channel_key; - e->expected_producers = expected_producers; - e->dsm_handle = handle; - e->len = len; - e->cancelled = cancelled; - e->producer_pid = MyProcPid; - e->producer_latch = &MyProc->procLatch; - e->state = ANSER_SUBMIT_PENDING; - LWLockRelease(AnserRingLock); - SetLatch(&AnserCtl->gather_latch); - return i; - } - } - LWLockRelease(AnserRingLock); - - ResetLatch(MyLatch); - (void) WaitLatch(MyLatch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - ANSER_WAIT_POLL_INTERVAL_MS, PG_WAIT_EXTENSION); - } -} - -/* - * Block on the proc latch until the gather service reaches a terminal state for - * our slot, then release the slot and report whether the part was accepted. - */ -static bool -AnserWaitSubmissionAck(int slot) -{ - AnserSubmissionEntry *e = &AnserSubmissionQueue[slot]; - - for (;;) - { - AnserSubmissionState st; - - CHECK_FOR_INTERRUPTS(); - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - st = e->state; - if (st == ANSER_SUBMIT_ACCEPTED || st == ANSER_SUBMIT_REJECTED) - { - e->state = ANSER_SUBMIT_FREE; - LWLockRelease(AnserRingLock); - return st == ANSER_SUBMIT_ACCEPTED; - } - LWLockRelease(AnserRingLock); - - ResetLatch(MyLatch); - (void) WaitLatch(MyLatch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - ANSER_WAIT_LATCH_TIMEOUT_MS, PG_WAIT_EXTENSION); - } -} - -/* - * Remote-consumer wait: subscribe, register a wait slot, and block on the proc - * latch until the send service delivers a payload or cancels this consumer. On - * success *payload points at a freshly palloc'd copy of the bytes. The calling - * backend does no channel-map polling; the wait happens entirely here. - */ -bool -AnserConsumerWait(const AnserChannelKey *channel_key, void **payload, - Size *payload_len, bool *cancelled, - Oid caller_role, bool caller_is_super) -{ - int slot; - bool result; - - if (payload != NULL) - *payload = NULL; - if (payload_len != NULL) - *payload_len = 0; - if (cancelled != NULL) - *cancelled = false; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - /* - * Wait for a producer to announce the channel before subscribing. In the - * plan tree producers sit below their consumers, so the channel is usually - * registered first; but execution order across the cluster is not - * guaranteed, so a consumer that arrives early waits (up to - * anser.timeout_ms) for registration instead of failing open at once. - * A false return means the producer never registered in time, or the - * dataset was already cancelled -- either way this consumer fails open. - */ - if (!AnserWaitProducersRegistered(channel_key, (long) gp_anser_timeout_ms)) - { - if (cancelled != NULL) - *cancelled = true; - return false; - } - - /* - * Only the owning role (or a superuser) may read a channel. Checked after - * registration so there is a recorded creator_role to compare against. - */ - if (!AnserChannelAccessAllowed(channel_key, caller_role, caller_is_super, - NULL)) - return false; - - /* - * Subscribe before registering the wait slot so the channel's consumer - * count is never lower than the number of live wait slots; the send service - * relies on that ordering for its recycle accounting. - */ - if (!AnserSubscribe(channel_key)) - return false; - - slot = AnserRegisterWaitSlot(channel_key); - if (slot < 0) - { - if (cancelled != NULL) - *cancelled = true; - return false; - } - - SetLatch(&AnserCtl->send_latch); - - /* - * Reclaim our wait slot (and unpin any payload the send service already - * stamped) if we are interrupted before collecting the result, so it does - * not linger until this backend exits. - */ - PG_TRY(); - { - result = AnserWaitSlotResult(slot, payload, payload_len, cancelled); - } - PG_CATCH(); - { - AnserAbandonWaitSlot(slot); - PG_RE_THROW(); - } - PG_END_TRY(); - - return result; -} - -/* - * Give up a wait slot after the consumer is interrupted mid-wait, unpinning any - * per-consumer payload copy the send service stamped but we never collected. - * Guarded by pid so a slot already reclaimed and reused is left untouched. - */ -static void -AnserAbandonWaitSlot(int slot) -{ - AnserWaitSlot *s = &AnserWaitTable[slot]; - AnserChannelKey key; - bool was_waiting = false; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - if (s->consumer_pid == MyProcPid && s->state != ANSER_WAIT_FREE) - { - /* - * The slot's dsm_handle is borrowed from the channel (which owns and - * frees the payload DSM), so abandoning just stops this slot from - * borrowing -- do not unpin it here. - */ - was_waiting = (s->state == ANSER_WAIT_WAITING); - key = s->key; - s->dsm_handle = DSM_HANDLE_INVALID; - s->len = 0; - s->state = ANSER_WAIT_FREE; - } - LWLockRelease(AnserRingLock); - - /* - * A consumer that abandons before any data was delivered to it must no - * longer count toward the channel's expected consumer total; otherwise the - * send service's "delivered to every consumer" recycle test can never be - * satisfied and the channel lingers in READY forever. (A slot that was - * already DELIVERED is left counted: the send service incremented - * done_consumers for it, so the accounting still balances.) - */ - if (was_waiting) - { - AnserChannelEntry *entry; - bool found; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, &key, - HASH_FIND, &found); - if (found && entry->consumers > 0) - { - entry->consumers--; - /* Let the send service re-evaluate recycling. */ - SetLatch(&AnserCtl->send_latch); - } - LWLockRelease(AnserChannelLock); - } -} - -/* - * Claim a free wait-table slot for this consumer. Returns the slot index, or - * -1 if the table is full (the consumer then fails open). - */ -static int -AnserRegisterWaitSlot(const AnserChannelKey *channel_key) -{ - int len_slots = AnserWaitTableLen(); - int i; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - for (i = 0; i < len_slots; i++) - { - AnserWaitSlot *s = &AnserWaitTable[i]; - - if (s->state == ANSER_WAIT_FREE) - { - s->key = *channel_key; - s->dsm_handle = DSM_HANDLE_INVALID; - s->len = 0; - s->consumer_pid = MyProcPid; - s->consumer_latch = &MyProc->procLatch; - s->state = ANSER_WAIT_WAITING; - LWLockRelease(AnserRingLock); - return i; - } - } - LWLockRelease(AnserRingLock); - - return -1; -} - -/* - * Block until the send service resolves our wait slot. On DELIVERED, attach the - * per-consumer payload segment, copy it into palloc'd memory, and free the - * segment (the send service pinned it and handed us ownership). - */ -static bool -AnserWaitSlotResult(int slot, void **payload, Size *payload_len, - bool *cancelled) -{ - AnserWaitSlot *s = &AnserWaitTable[slot]; - AnserChannelKey slot_key = s->key; - - for (;;) - { - AnserWaitSlotState st; - dsm_handle handle = DSM_HANDLE_INVALID; - Size len = 0; - - CHECK_FOR_INTERRUPTS(); - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - st = s->state; - if (st == ANSER_WAIT_DELIVERED) - { - /* - * Read the borrowed channel payload handle but leave the slot - * DELIVERED: that keeps the channel's payload DSM alive (the sweep - * will not reclaim a channel with a DELIVERED slot) until we have - * copied it out below. We flip the slot to FREE only afterward. - */ - handle = s->dsm_handle; - len = s->len; - } - else if (st == ANSER_WAIT_CANCELLED) - { - s->state = ANSER_WAIT_FREE; - } - LWLockRelease(AnserRingLock); - - if (st == ANSER_WAIT_CANCELLED) - { - if (cancelled != NULL) - *cancelled = true; - return false; - } - - if (st == ANSER_WAIT_DELIVERED) - { - void *buf = NULL; - bool vanished = false; - - if (handle != DSM_HANDLE_INVALID) - { - dsm_segment *seg = dsm_attach(handle); - - if (seg == NULL) - vanished = true; /* should not happen: we hold DELIVERED */ - else - { - if (len > 0) - { - buf = palloc(len); - memcpy(buf, dsm_segment_address(seg), len); - } - /* Borrowed handle -- detach, but the channel owns/frees it. */ - dsm_detach(seg); - } - } - - /* Done reading: release the slot so the channel can be reclaimed. */ - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - if (s->state == ANSER_WAIT_DELIVERED) - s->state = ANSER_WAIT_FREE; - LWLockRelease(AnserRingLock); - - if (vanished) - { - if (cancelled != NULL) - *cancelled = true; - return false; - } - - if (payload != NULL) - *payload = buf; - if (payload_len != NULL) - *payload_len = len; - return true; - } - - /* - * Still WAITING. Guard against the registration/recycle race: if our - * channel has already been recycled (CONSUMED/CANCELLED) or swept out of - * the map between AnserWaitProducersRegistered and our slot - * registration, the send service will never resolve this slot -- there - * is no live payload to deliver. Reclaim the slot ourselves and fail - * open rather than block forever. - */ - if (st == ANSER_WAIT_WAITING) - { - bool found = false; - AnserChannelState cstate = AnserChannelGetState(&slot_key, &found); - - if (!found || - cstate == ANSER_CHANNEL_CANCELLED || - cstate == ANSER_CHANNEL_CONSUMED) - { - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - if (s->consumer_pid == MyProcPid && - s->state == ANSER_WAIT_WAITING) - s->state = ANSER_WAIT_FREE; - LWLockRelease(AnserRingLock); - - if (cancelled != NULL) - *cancelled = true; - return false; - } - } - - ResetLatch(MyLatch); - (void) WaitLatch(MyLatch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - ANSER_WAIT_LATCH_TIMEOUT_MS, PG_WAIT_EXTENSION); - } -} - -/* - * One gather-service pass: drain the submission queue, cancel channels that have - * sat in COLLECTING past the produce deadline, and reclaim slots left behind by - * producers that died mid-wait. - */ -void -AnserGatherServiceCycle(void) -{ - int len_slots; - int i; - - if (!AnserInitialized() || AnserSubmissionQueue == NULL) - return; - - len_slots = AnserSubmissionQueueLen(); - for (i = 0; i < len_slots; i++) - { - AnserSubmissionEntry *e = &AnserSubmissionQueue[i]; - AnserChannelKey key; - int32 expected_producers; - dsm_handle handle; - Size len; - bool cancelled; - bool accepted; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - if (e->state != ANSER_SUBMIT_PENDING) - { - LWLockRelease(AnserRingLock); - continue; - } - key = e->key; - expected_producers = e->expected_producers; - handle = e->dsm_handle; - len = e->len; - cancelled = e->cancelled; - LWLockRelease(AnserRingLock); - - accepted = AnserGatherApply(&key, expected_producers, handle, len, - cancelled); - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - /* The slot is still ours: only the gather service leaves PENDING. */ - e->state = accepted ? ANSER_SUBMIT_ACCEPTED : ANSER_SUBMIT_REJECTED; - if (e->producer_latch != NULL) - SetLatch(e->producer_latch); - LWLockRelease(AnserRingLock); - } - - AnserCancelStaleChannels(); - AnserReapSubmissionSlots(); -} - -/* - * Apply one submitted part to its channel: attach the producer's payload, append - * it (or cancel the dataset), and advance the channel toward READY. Mirrors the - * direct AnserPublish path but sourced from a DSM handle. - */ -static bool -AnserGatherApply(const AnserChannelKey *channel_key, int expected_producers, - dsm_handle handle, Size len, bool cancelled) -{ - AnserChannelEntry *entry; - bool found; - dsm_segment *seg = NULL; - void *addr = NULL; - - if (!cancelled && handle != DSM_HANDLE_INVALID && len > 0) - { - seg = dsm_attach(handle); - if (seg == NULL) - cancelled = true; /* producer gone / segment lost */ - else - addr = dsm_segment_address(seg); - } - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, - HASH_FIND, &found); - - /* - * No producer_begin registered this channel (or it was already recycled): - * refuse the part rather than creating an unowned channel, which would - * bypass the creator_role access check. The client always begins before - * publishing, so a legitimate part always finds its channel here. A - * dataset already in a terminal state likewise refuses late parts. - */ - if (!found || - entry->state == ANSER_CHANNEL_CANCELLED || - entry->state == ANSER_CHANNEL_CONSUMED) - { - LWLockRelease(AnserChannelLock); - if (seg != NULL) - dsm_detach(seg); - return false; - } - - if (expected_producers > 0) - entry->expected_producers = expected_producers; - - if (cancelled) - { - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = GetCurrentTimestamp(); - AnserReleasePayloadDSM(entry); - LWLockRelease(AnserChannelLock); - if (seg != NULL) - dsm_detach(seg); - SetLatch(&AnserCtl->send_latch); - return true; - } - - if (entry->state == ANSER_CHANNEL_PENDING) - entry->state = ANSER_CHANNEL_COLLECTING; - - if (addr != NULL && len > 0) - { - if (!AnserStorePayloadDSM(entry, addr, len)) - { - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = GetCurrentTimestamp(); - AnserReleasePayloadDSM(entry); - LWLockRelease(AnserChannelLock); - if (seg != NULL) - dsm_detach(seg); - SetLatch(&AnserCtl->send_latch); - return false; - } - } - - entry->done_producers++; - if (entry->done_producers >= entry->expected_producers) - entry->state = ANSER_CHANNEL_READY; - entry->updated_at = GetCurrentTimestamp(); - LWLockRelease(AnserChannelLock); - - if (seg != NULL) - dsm_detach(seg); - SetLatch(&AnserCtl->send_latch); - - return true; -} - -/* - * Cancel any channel that announced producers (COLLECTING) but did not reach - * READY within anser.timeout_ms. Cancellation is whole-dataset: - * all-parts-or-nothing. - */ -static void -AnserCancelStaleChannels(void) -{ - HASH_SEQ_STATUS status; - AnserChannelEntry *entry; - TimestampTz now = GetCurrentTimestamp(); - bool any = false; - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - hash_seq_init(&status, AnserChannelHash); - while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) - { - if (entry->state == ANSER_CHANNEL_COLLECTING && - TimestampDifferenceExceeds(entry->updated_at, now, - gp_anser_timeout_ms)) - { - entry->state = ANSER_CHANNEL_CANCELLED; - entry->updated_at = now; - AnserReleasePayloadDSM(entry); - any = true; - } - } - LWLockRelease(AnserChannelLock); - - if (any) - SetLatch(&AnserCtl->send_latch); -} - -/* - * Reclaim terminal submission slots whose producer backend has exited without - * consuming the ACK (e.g. cancelled mid-wait). - */ -static void -AnserReapSubmissionSlots(void) -{ - int len_slots = AnserSubmissionQueueLen(); - int i; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - for (i = 0; i < len_slots; i++) - { - AnserSubmissionEntry *e = &AnserSubmissionQueue[i]; - - if ((e->state == ANSER_SUBMIT_ACCEPTED || - e->state == ANSER_SUBMIT_REJECTED) && - !AnserPidIsLive(e->producer_pid)) - e->state = ANSER_SUBMIT_FREE; - } - LWLockRelease(AnserRingLock); -} - -/* - * One send-service pass: deliver every READY/CANCELLED channel to its waiting - * consumers and reclaim slots left behind by consumers that have exited. - */ -void -AnserSendServiceCycle(void) -{ - HASH_SEQ_STATUS status; - AnserChannelEntry *entry; - int len_slots; - - if (!AnserInitialized() || AnserWaitTable == NULL) - return; - - len_slots = AnserWaitTableLen(); - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - hash_seq_init(&status, AnserChannelHash); - while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) - { - bool ready = (entry->state == ANSER_CHANNEL_READY); - - /* - * Stragglers that registered after a channel finished (cancelled, or - * already consumed) can no longer be handed data; they are delivered a - * cancel so they fail open instead of blocking forever on a channel the - * sweep would otherwise never reclaim. - */ - bool cancel_waiters = (entry->state == ANSER_CHANNEL_CANCELLED || - entry->state == ANSER_CHANNEL_CONSUMED); - int i; - - if (!ready && !cancel_waiters) - continue; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - for (i = 0; i < len_slots; i++) - { - AnserWaitSlot *s = &AnserWaitTable[i]; - - if (s->state != ANSER_WAIT_WAITING) - continue; - if (memcmp(&s->key, &entry->key, sizeof(AnserChannelKey)) != 0) - continue; - - if (cancel_waiters) - { - s->dsm_handle = DSM_HANDLE_INVALID; - s->len = 0; - s->state = ANSER_WAIT_CANCELLED; - if (s->consumer_latch != NULL) - SetLatch(s->consumer_latch); - continue; - } - - /* - * READY: lend this consumer the channel's single payload segment -- - * the slot borrows entry->dsm_handle rather than getting its own - * copy. The consumer copies it out and only then frees the slot; the - * payload DSM is released once the channel is reclaimed with no slot - * still borrowing it (AnserChannelHasWaiters / the sweep). Holding - * the slot DELIVERED keeps that handle alive across the read. - */ - s->dsm_handle = entry->dsm_handle; - s->len = entry->data_len; - s->state = ANSER_WAIT_DELIVERED; - if (s->consumer_latch != NULL) - SetLatch(s->consumer_latch); - - entry->done_consumers++; - } - LWLockRelease(AnserRingLock); - - /* - * Recycle once every expected consumer has been handed the payload. Do - * NOT free the payload DSM here: consumers still hold DELIVERED slots - * that borrow it. It is released when the sweep reclaims this now - * terminal channel, after every borrowing slot has drained. - */ - if (ready && entry->expected_consumers > 0 && - entry->done_consumers >= entry->expected_consumers) - { - entry->state = ANSER_CHANNEL_CONSUMED; - entry->updated_at = GetCurrentTimestamp(); - } - } - LWLockRelease(AnserChannelLock); - - AnserReapWaitSlots(); -} - -/* - * Reclaim wait slots whose consumer backend has exited, unpinning any payload - * copy the send service already stamped but the consumer never collected. - */ -static void -AnserReapWaitSlots(void) -{ - int len_slots = AnserWaitTableLen(); - int i; - - LWLockAcquire(AnserRingLock, LW_EXCLUSIVE); - for (i = 0; i < len_slots; i++) - { - AnserWaitSlot *s = &AnserWaitTable[i]; - - if (s->state == ANSER_WAIT_FREE) - continue; - if (AnserPidIsLive(s->consumer_pid)) - continue; - - /* - * A dead consumer's slot is freed without touching its dsm_handle: that - * handle is borrowed from the channel (the channel owns and frees the - * payload DSM), so freeing the slot just stops it from borrowing. - */ - s->dsm_handle = DSM_HANDLE_INVALID; - s->len = 0; - s->state = ANSER_WAIT_FREE; - } - LWLockRelease(AnserRingLock); -} - -/* - * Effective size of the channel map. - * - * When anser.max_channels is set explicitly (> 0) it wins. Otherwise the map - * is auto-sized to max_connections * max_slices: at most MaxConnections - * concurrent queries, each opening up to gp_max_slices runtime-filter channels. - * gp_max_slices == 0 means "unbounded", for which we substitute a fixed - * per-connection budget (ANSER_AUTO_SLICES_PER_CONN) so the map stays finite. - * - * This value sizes fixed shared memory at postmaster start, so it must be stable - * for the life of the postmaster and identical in every backend. MaxConnections - * is PGC_POSTMASTER (stable), but gp_max_slices is PGC_USERSET, so we cache the - * computed value on first use. That first use is the postmaster's shmem-sizing - * pass (before any backend forks or any session runs SET), so the cache captures - * the postmaster-level gp_max_slices and is inherited unchanged by every - * backend -- a later per-session SET gp_max_slices cannot resize the map. - * - * Exposed (non-static) so the regression suite can prove the cache holds: see - * anser_test_max_channels_stable_across_slices(). - */ -int -AnserMaxChannels(void) -{ - static int cached = 0; - int slices; - int64 v; - - if (gp_anser_max_channels > 0) - return gp_anser_max_channels; - - if (cached > 0) - return cached; - - slices = (gp_max_slices > 0) ? gp_max_slices : ANSER_AUTO_SLICES_PER_CONN; - v = (int64) MaxConnections * (int64) slices; - - if (v < 1) - v = 1; - if (v > INT_MAX) - v = INT_MAX; - - cached = (int) v; - return cached; -} - -static Size -AnserChannelHashSize(void) -{ - return hash_estimate_size(AnserMaxChannels(), - sizeof(AnserChannelEntry)); -} - -/* - * The submission queue holds parts in flight between blocked producers and the - * gather service. Every segment producing for a channel submits its own part, - * and they hand off concurrently, so -- like the consumer wait table -- we size - * for one in-flight slot per producer per channel (channels * per-channel - * producers, which mirrors the per-channel consumer count = segment count). - * Producers that still find it full wait for a free slot rather than failing. - */ -static int -AnserSubmissionQueueLen(void) -{ - int64 len = (int64) AnserMaxChannels() * - (int64) gp_anser_max_consumers_per_channel; - - /* Guard against int overflow from extreme GUC settings. */ - if (len > INT_MAX) - len = INT_MAX; - - return (int) len; -} - -static int -AnserWaitTableLen(void) -{ - int64 len = (int64) AnserMaxChannels() * - (int64) gp_anser_max_consumers_per_channel; - - /* Guard against int overflow from extreme GUC settings. */ - if (len > INT_MAX) - len = INT_MAX; - - return (int) len; -} - -static Size -AnserSubmissionQueueSize(void) -{ - return mul_size(sizeof(AnserSubmissionEntry), - (Size) AnserSubmissionQueueLen()); -} - -static Size -AnserWaitTableSize(void) -{ - return mul_size(sizeof(AnserWaitSlot), (Size) AnserWaitTableLen()); -} - -static void -AnserInitializeSubmissionQueue(bool found) -{ - if (!found) - MemSet(AnserSubmissionQueue, 0, AnserSubmissionQueueSize()); -} - -static void -AnserInitializeWaitTable(bool found) -{ - if (!found) - MemSet(AnserWaitTable, 0, AnserWaitTableSize()); -} - -static bool -AnserPidIsLive(int pid) -{ - if (pid == 0) - return false; - - return BackendPidGetProc(pid) != NULL; -} - -/* - * Does any consumer still have a WAITING wait slot for this channel? Callers - * hold AnserChannelLock; we take AnserRingLock (channel-lock-then-ring-lock - * order, matching the send cycle) to read the wait table. - */ -static bool -AnserChannelHasWaiters(const AnserChannelKey *channel_key) -{ - int len_slots; - int i; - bool found = false; - - if (AnserWaitTable == NULL) - return false; - - len_slots = AnserWaitTableLen(); - LWLockAcquire(AnserRingLock, LW_SHARED); - for (i = 0; i < len_slots; i++) - { - /* - * A slot still references the channel while it is WAITING (not yet - * delivered) or DELIVERED (delivered but the consumer has not finished - * copying the borrowed payload out). Either blocks reclaim: the sweep - * must not free the payload DSM while a DELIVERED slot could still attach - * it. - */ - if ((AnserWaitTable[i].state == ANSER_WAIT_WAITING || - AnserWaitTable[i].state == ANSER_WAIT_DELIVERED) && - memcmp(&AnserWaitTable[i].key, channel_key, - sizeof(AnserChannelKey)) == 0) - { - found = true; - break; - } - } - LWLockRelease(AnserRingLock); - - return found; -} - -/* - * May this caller produce/consume on the channel? A superuser always may; any - * other role may only touch a channel it created. An unknown channel, or one - * with no recorded creator, is permitted here -- callers handle "not found" - * through their normal paths. If found is non-NULL it receives whether the - * channel currently exists. - */ -static bool -AnserChannelAccessAllowed(const AnserChannelKey *channel_key, Oid caller_role, - bool caller_is_super, bool *found) -{ - AnserChannelEntry *entry; - bool local_found; - bool allowed = true; - - LWLockAcquire(AnserChannelLock, LW_SHARED); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, channel_key, - HASH_FIND, &local_found); - if (local_found && !caller_is_super && - OidIsValid(entry->creator_role) && - entry->creator_role != caller_role) - allowed = false; - LWLockRelease(AnserChannelLock); - - if (found != NULL) - *found = local_found; - - return allowed; -} - -static void -AnserInitializeControl(bool found) -{ - if (!found) - { - MemSet(AnserCtl, 0, sizeof(AnserControl)); - AnserCtl->max_channels = AnserMaxChannels(); - AnserCtl->max_info_size = gp_anser_max_info_size; - InitSharedLatch(&AnserCtl->gather_latch); - InitSharedLatch(&AnserCtl->send_latch); - AnserCtl->sweep_enabled = true; - } -} - -static void -AnserInitializeChannelHash(void) -{ - HASHCTL hctl; - - MemSet(&hctl, 0, sizeof(hctl)); - hctl.keysize = sizeof(AnserChannelKey); - hctl.entrysize = sizeof(AnserChannelEntry); - - AnserChannelHash = ShmemInitHash(ANSER_CHANNEL_HASH_NAME, - AnserMaxChannels(), - AnserMaxChannels(), - &hctl, - HASH_ELEM | HASH_BLOBS); -} - -/* - * Recycle terminal or orphaned channels before declaring registration failure. - */ -static void -AnserSweepOrphanChannels(void) -{ - HASH_SEQ_STATUS status; - AnserChannelEntry *entry; - AnserChannelKey *remove_keys; - int remove_count = 0; - int i; - - Assert(LWLockHeldByMeInMode(AnserChannelLock, LW_EXCLUSIVE)); - - if (AnserChannelHash == NULL) - return; - - remove_keys = (AnserChannelKey *) palloc(sizeof(AnserChannelKey) * - (Size) AnserMaxChannels()); - - hash_seq_init(&status, AnserChannelHash); - while ((entry = (AnserChannelEntry *) hash_seq_search(&status)) != NULL) - { - bool recycle = false; - - if (entry->state == ANSER_CHANNEL_CONSUMED || - entry->state == ANSER_CHANNEL_CANCELLED) - recycle = true; - else if (!AnserChannelOwnerIsAlive(entry)) - recycle = true; - - /* - * Never recycle a channel that still has consumers blocked on it: the - * send service must first deliver the payload or a cancel to those wait - * slots. Removing the channel out from under them would strand the - * consumers, which only wake on their slot. - */ - if (recycle && AnserChannelHasWaiters(&entry->key)) - recycle = false; - - if (recycle) - { - AnserReleasePayloadDSM(entry); - remove_keys[remove_count++] = entry->key; - } - } - - for (i = 0; i < remove_count; i++) - (void) hash_search(AnserChannelHash, - &remove_keys[i], - HASH_REMOVE, - NULL); - - pfree(remove_keys); -} - -/* - * Is the query that owns this channel still alive? - * - * Validation is deliberately conservative, at session granularity rather than - * per query/command: a channel lives as long as its owning coordinator session - * does, and AnserCancelQuery() provides explicit cleanup at query end/failure. - */ -static bool -AnserChannelOwnerIsAlive(const AnserChannelEntry *entry) -{ - Assert(entry != NULL); - - /* - * A channel belongs to one query, identified by gp_session_id. It is alive - * as long as that coordinator (QD) session still has a backend in the proc - * array; once the session is gone -- query finished/aborted, or a fixed test - * session id that never maps to a live backend -- the channel is orphaned and - * may be reclaimed. - * - * Liveness is deliberately tied to the session, NOT to the backend that - * created the channel: network-path producers create it from a short-lived - * libpq request backend (AnserClientPublish PQfinish's the connection right - * after publishing), so that backend is normally already gone while the - * channel is still needed by consumers. - */ - if (entry->key.gp_session_id <= 0) - return true; /* no session to check against; keep it */ - - return FindProcByGpSessionId((long) entry->key.gp_session_id) != NULL; -} - -static bool -AnserStorePayloadDSM(AnserChannelEntry *entry, const void *payload, - Size payload_len) -{ - dsm_segment *acc_seg = NULL; - dsm_segment *new_seg; - void *acc_addr = NULL; - - Assert(LWLockHeldByMeInMode(AnserChannelLock, LW_EXCLUSIVE)); - Assert(entry != NULL); - - if (payload == NULL || payload_len == 0) - return true; - - if (payload_len > (Size) gp_anser_max_info_size) - return false; - - /* Attach the channel's running merged payload, if it already has one. */ - if (entry->dsm_handle != DSM_HANDLE_INVALID && entry->data_len > 0) - { - acc_seg = dsm_attach(entry->dsm_handle); - if (acc_seg == NULL) - return false; - acc_addr = dsm_segment_address(acc_seg); - } - - /* - * Subsequent part: fold it into the existing payload in place. Every part on - * a channel shares the same (condition-key-derived) bloom parameters, so it is - * the same serialized size and the union is a bitwise OR of the bitsets -- no - * fresh segment, no full-payload copy. Safe because we hold AnserChannelLock - * and consumers only ever read their own copies. A part that cannot fold in - * place (wrong size, malformed) is rejected: the caller then cancels the - * channel and its consumers fail open. - */ - if (acc_addr != NULL) - { - bool folded = AnserBloomFoldPartInPlace(acc_addr, entry->data_len, - payload, payload_len); - - dsm_detach(acc_seg); - return folded; - } - - /* - * First part: store it verbatim in a fresh, pinned segment. The coordinator - * never reconstructs a filter from the payload -- it only copies the first - * part and OR-folds the rest -- so no bitset parameters are needed here. - */ - new_seg = dsm_create(payload_len, DSM_CREATE_NULL_IF_MAXSEGMENTS); - if (new_seg == NULL) - return false; - memcpy(dsm_segment_address(new_seg), payload, payload_len); - - AnserReleasePayloadDSM(entry); - dsm_pin_segment(new_seg); - entry->dsm_handle = dsm_segment_handle(new_seg); - entry->data_len = payload_len; - dsm_detach(new_seg); - - return true; -} - -static void -AnserReleasePayloadDSM(AnserChannelEntry *entry) -{ - Assert(LWLockHeldByMeInMode(AnserChannelLock, LW_EXCLUSIVE)); - - if (entry == NULL || entry->dsm_handle == DSM_HANDLE_INVALID) - return; - - dsm_unpin_segment(entry->dsm_handle); - entry->dsm_handle = DSM_HANDLE_INVALID; - entry->data_len = 0; -} - -/* - * Consume a ready channel payload. - */ -static bool -AnserDeliverChannelData(const AnserChannelEntry *entry, void *buffer, - Size buffer_size, Size *payload_len) -{ - dsm_segment *seg; - void *addr; - - Assert(LWLockHeldByMe(AnserChannelLock)); - Assert(entry != NULL); - Assert(payload_len != NULL); - - if (entry->dsm_handle == DSM_HANDLE_INVALID) - { - *payload_len = 0; - return (entry->data_len == 0); - } - - if (buffer == NULL && entry->data_len > 0) - return false; - - if (buffer_size < entry->data_len) - return false; - - seg = dsm_attach(entry->dsm_handle); - if (seg == NULL) - return false; - - addr = dsm_segment_address(seg); - if (entry->data_len > 0) - memcpy(buffer, addr, entry->data_len); - dsm_detach(seg); - - *payload_len = entry->data_len; - - return true; -} - -static bool -AnserWaitForState(const AnserChannelKey *channel_key, long timeout_ms, - bool registration_only, bool *cancelled) -{ - TimestampTz start_time = GetCurrentTimestamp(); - - if (cancelled != NULL) - *cancelled = false; - - if (!AnserInitialized() || channel_key == NULL) - return false; - - for (;;) - { - AnserChannelEntry *entry; - bool found; - bool registered; - bool ready; - bool is_cancelled; - - CHECK_FOR_INTERRUPTS(); - - LWLockAcquire(AnserChannelLock, LW_SHARED); - entry = (AnserChannelEntry *) hash_search(AnserChannelHash, - channel_key, - HASH_FIND, - &found); - registered = found && entry->expected_producers > 0; - ready = found && entry->state == ANSER_CHANNEL_READY; - is_cancelled = found && entry->state == ANSER_CHANNEL_CANCELLED; - LWLockRelease(AnserChannelLock); - - if (is_cancelled) - { - if (cancelled != NULL) - *cancelled = true; - return false; - } - - if (registration_only) - { - if (registered) - return true; - } - else if (ready) - return true; - - if (timeout_ms >= 0 && - TimestampDifferenceExceeds(start_time, GetCurrentTimestamp(), - timeout_ms)) - return false; - - ResetLatch(MyLatch); - (void) WaitLatch(MyLatch, - WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - ANSER_WAIT_POLL_INTERVAL_MS, - PG_WAIT_EXTENSION); - } -} - -static bool -AnserInitialized(void) -{ - return gp_anser_enable && AnserChannelLock != NULL && AnserCtl != NULL && - AnserChannelHash != NULL && AnserWaitTable != NULL; -} diff --git a/gpcontrib/anser/src/anser_test.c b/gpcontrib/anser/src/anser_test.c index bb33ef454d8..5fa36eeb75a 100644 --- a/gpcontrib/anser/src/anser_test.c +++ b/gpcontrib/anser/src/anser_test.c @@ -27,24 +27,15 @@ */ #include "postgres.h" -#include "libpq-fe.h" - #include "anser.h" #include "anserbloom.h" -#include "anserclient.h" #include "anserfilter.h" -#include "cdb/cdbutil.h" +#include "ansersideband.h" #include "cdb/cdbvars.h" -#include "commands/dbcommands.h" #include "fmgr.h" #include "lib/bloomfilter.h" #include "miscadmin.h" -#include "postmaster/postmaster.h" -#include "storage/latch.h" -#include "utils/acl.h" #include "utils/builtins.h" -#include "utils/guc.h" -#include "utils/wait_event.h" #include "varatt.h" /* @@ -56,241 +47,11 @@ #define ANSER_TEST_ELEMS 32 #define ANSER_TEST_MAX_PAYLOAD (1024 * 1024) -PG_FUNCTION_INFO_V1(anser_test_register_condition); -PG_FUNCTION_INFO_V1(anser_test_subscribe); -PG_FUNCTION_INFO_V1(anser_test_publish); -PG_FUNCTION_INFO_V1(anser_test_publish_value); -PG_FUNCTION_INFO_V1(anser_test_consume); -PG_FUNCTION_INFO_V1(anser_test_consume_has); -PG_FUNCTION_INFO_V1(anser_test_state); -PG_FUNCTION_INFO_V1(anser_test_cancel_query); PG_FUNCTION_INFO_V1(anser_test_bloom_roundtrip); PG_FUNCTION_INFO_V1(anser_test_bloom_fold_inplace); PG_FUNCTION_INFO_V1(anser_test_bloom_rejects_mismatch); PG_FUNCTION_INFO_V1(anser_test_node_roundtrip); -PG_FUNCTION_INFO_V1(anser_test_client_roundtrip); -PG_FUNCTION_INFO_V1(anser_test_token_roundtrip); -PG_FUNCTION_INFO_V1(anser_test_multi_consumer); -PG_FUNCTION_INFO_V1(anser_test_abandoned_consumer_recycles); -PG_FUNCTION_INFO_V1(anser_test_dsm_free_on_success); -PG_FUNCTION_INFO_V1(anser_test_dsm_free_on_timeout); -PG_FUNCTION_INFO_V1(anser_test_dsm_free_on_cancel); -PG_FUNCTION_INFO_V1(anser_test_set_sweep); -PG_FUNCTION_INFO_V1(anser_test_sweep); -PG_FUNCTION_INFO_V1(anser_test_max_channels_stable_across_slices); - -static bool build_test_key(FunctionCallInfo fcinfo, AnserChannelKey *key); -static char *anser_make_test_part(const char *condition_key, int32 value, - Size *len_out); -static const char *state_to_string(AnserChannelState state); -static char *anser_loopback_host(void); -static PGconn *anser_open_consumer(const AnserChannelKey *key); -static bool anser_wait_consumer_count(const AnserChannelKey *key, int target); -static void anser_cancel_conn(PGconn *conn); -static bool anser_drain_until_idle(PGconn *conn); -static bool anser_consumer_got_payload(PGconn *conn, const unsigned char *expected, - Size expected_len); -static bool anser_consumer_returned_row(PGconn *conn); -static bool anser_wait_channel_consumed(const AnserChannelKey *key); - -Datum -anser_test_register_condition(PG_FUNCTION_ARGS) -{ - int32 gp_session_id = PG_GETARG_INT32(0); - int32 gp_command_count = PG_GETARG_INT32(1); - int32 condition_id_arg = PG_GETARG_INT32(2); - char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); - int32 expected_producers_arg = PG_GETARG_INT32(4); - AnserChannelKey key; - - if (condition_id_arg < 0 || expected_producers_arg <= 0) - PG_RETURN_BOOL(false); - - if (strlen(condition_key) >= ANSER_CONDITION_KEY_SIZE) - PG_RETURN_BOOL(false); - - /* - * Drive the production registration entry point (AnserProducerBegin) rather - * than a test-only variant, so the state machine we exercise below is the - * one real producers use. - */ - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = gp_session_id; - key.gp_command_count = gp_command_count; - key.condition_id = (uint32) condition_id_arg; - strlcpy(key.condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); - - PG_RETURN_BOOL(AnserProducerBegin(&key, expected_producers_arg, - GetUserId(), superuser())); -} - -Datum -anser_test_subscribe(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - - if (!build_test_key(fcinfo, &key)) - PG_RETURN_BOOL(false); - PG_RETURN_BOOL(AnserSubscribe(&key)); -} - -Datum -anser_test_publish(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - bytea *payload = PG_GETARG_BYTEA_PP(4); - bool cancelled = PG_GETARG_BOOL(5); - - if (!build_test_key(fcinfo, &key)) - PG_RETURN_BOOL(false); - - PG_RETURN_BOOL(AnserPublish(&key, - VARDATA_ANY(payload), - VARSIZE_ANY_EXHDR(payload), - cancelled)); -} - -/* - * Publish a real serialized bloom part carrying a single int value. Multiple - * producers on one channel each call this; the coordinator stores the first part - * and OR-folds the rest (all same size), so the merged filter contains every - * published value. Needed by the state-machine test that drives >1 producer: - * the coordinator only combines serialized bloom parts, which a raw SQL bytea - * literal cannot express. - */ -Datum -anser_test_publish_value(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - int32 value = PG_GETARG_INT32(4); - char *part; - Size len = 0; - bool ok; - - if (!build_test_key(fcinfo, &key)) - PG_RETURN_BOOL(false); - - part = anser_make_test_part(key.condition_key, value, &len); - if (part == NULL) - PG_RETURN_BOOL(false); - - ok = AnserPublish(&key, part, len, false); - pfree(part); - PG_RETURN_BOOL(ok); -} - -Datum -anser_test_consume(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - int32 timeout_arg = PG_GETARG_INT32(4); - char *buffer; - Size payload_len = 0; - bool cancelled = false; - bytea *result; - - if (timeout_arg < 0) - PG_RETURN_NULL(); - - if (!build_test_key(fcinfo, &key)) - PG_RETURN_NULL(); - - /* - * Consume through the production path -- AnserWaitReady + AnserConsumeReady, - * the same pair the executor's bloom consumer uses. The timeout argument is - * advisory here: a channel that never becomes READY is cancelled by the - * gather service's stale-channel sweep after anser.timeout_ms, which wakes - * this wait with cancelled = true (so a "timeout" returns NULL). - */ - if (!AnserWaitReady(&key, &cancelled) || cancelled) - PG_RETURN_NULL(); - - buffer = (char *) palloc((Size) gp_anser_max_info_size); - if (!AnserConsumeReady(&key, buffer, (Size) gp_anser_max_info_size, - &payload_len, &cancelled) || cancelled) - PG_RETURN_NULL(); - - result = (bytea *) palloc(VARHDRSZ + payload_len); - SET_VARSIZE(result, VARHDRSZ + payload_len); - if (payload_len > 0) - memcpy(VARDATA(result), buffer, payload_len); - - PG_RETURN_BYTEA_P(result); -} - -/* - * Consume the merged bloom payload and test membership of a single value. Like - * anser_test_consume, but rebuilds the filter from the shared (ANSER_TEST_ELEMS, - * ANSER_TEST_MAX_PAYLOAD, key-derived seed) parameters -- exactly how the real - * consumer node reconstructs it, with the parameters carried by the node rather - * than the wire. Returns true iff the value is present in the received filter. - */ -Datum -anser_test_consume_has(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - int32 value = PG_GETARG_INT32(4); - Datum d = Int32GetDatum(value); - char *buffer; - Size payload_len = 0; - bool cancelled = false; - bloom_filter *filter; - bool has; - - if (!build_test_key(fcinfo, &key)) - PG_RETURN_BOOL(false); - - if (!AnserWaitReady(&key, &cancelled) || cancelled) - PG_RETURN_BOOL(false); - - buffer = (char *) palloc((Size) gp_anser_max_info_size); - if (!AnserConsumeReady(&key, buffer, (Size) gp_anser_max_info_size, - &payload_len, &cancelled) || cancelled) - { - pfree(buffer); - PG_RETURN_BOOL(false); - } - - filter = AnserBloomDeserializePart(buffer, payload_len, - ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, - AnserBloomSeed(key.condition_key), - NULL, NULL); - pfree(buffer); - if (filter == NULL) - PG_RETURN_BOOL(false); - - has = !bloom_lacks_element(filter, (unsigned char *) &d, sizeof(Datum)); - bloom_free(filter); - PG_RETURN_BOOL(has); -} - -Datum -anser_test_state(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - bool found = false; - AnserChannelState state; - - if (!build_test_key(fcinfo, &key)) - PG_RETURN_TEXT_P(cstring_to_text("NOT_FOUND")); - - state = AnserChannelGetState(&key, &found); - if (!found) - PG_RETURN_TEXT_P(cstring_to_text("NOT_FOUND")); - - PG_RETURN_TEXT_P(cstring_to_text(state_to_string(state))); -} - -Datum -anser_test_cancel_query(PG_FUNCTION_ARGS) -{ - int32 gp_session_id = PG_GETARG_INT32(0); - int32 gp_command_count = PG_GETARG_INT32(1); - - AnserCancelQuery(gp_session_id, gp_command_count); - PG_RETURN_VOID(); -} Datum anser_test_bloom_roundtrip(PG_FUNCTION_ARGS) @@ -504,6 +265,15 @@ anser_test_bloom_rejects_mismatch(PG_FUNCTION_ARGS) PG_RETURN_BOOL(ok); } +/* Send a cancel request for conn's in-flight query (best effort). */ +/* Printable name for a channel state ("UNKNOWN" when out of range). */ +/* + * Drive a producer and a consumer through the whole path in this one backend. + * + * Coordinator-local, so it exercises the merge, the channel table and the + * lifetime rules without needing segments; the segment half (NOTIFY out, + * sideband message in) is covered by the runtime-filter test on a cluster. + */ Datum anser_test_node_roundtrip(PG_FUNCTION_ARGS) { @@ -512,904 +282,51 @@ anser_test_node_roundtrip(PG_FUNCTION_ARGS) AnserBloomFilterConsumeState *consumer; int32 value_arg = PG_GETARG_INT32(0); Datum value = Int32GetDatum(value_arg); - bool ok; - - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 99; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "node_roundtrip", ANSER_CONDITION_KEY_SIZE); - if (!AnserProducerBegin(&key, 1, GetUserId(), superuser())) - PG_RETURN_BOOL(false); - if (!AnserSubscribe(&key)) - PG_RETURN_BOOL(false); - - producer = ExecInitAnserBloomFilterProduce(&key, 32, 1024 * 1024, 0, 1, - NULL); - if (producer == NULL) - PG_RETURN_BOOL(false); - ExecAnserBloomFilterProduceAddDatum(producer, value, false); - ok = ExecAnserBloomFilterProducePublish(producer); - ExecEndAnserBloomFilterProduce(producer); - if (!ok) - PG_RETURN_BOOL(false); - - consumer = ExecInitAnserBloomFilterConsume(&key, 32, 1024 * 1024, 1, - NULL); - if (consumer == NULL) - PG_RETURN_BOOL(false); - ok = ExecAnserBloomFilterConsume(consumer, 1000) && - ExecAnserBloomFilterConsumerGetFilter(consumer) != NULL && - ExecAnserBloomFilterConsumerReceivedParts(consumer) == 1 && - !ExecAnserBloomFilterConsumerWasCancelled(consumer) && - !bloom_lacks_element(ExecAnserBloomFilterConsumerGetFilter(consumer), - (unsigned char *) &value, sizeof(Datum)); - ExecEndAnserBloomFilterConsume(consumer); - PG_RETURN_BOOL(ok); -} - -/* - * Drive the libpq client helpers against our own coordinator (loopback), proving - * the AnserClient* path end-to-end without a multi-node cluster. The helpers - * read the QD address from qdHostname/qdPostmasterPort, which are blank on the - * coordinator itself, so we point them at the local postmaster for the duration - * of the call and restore them afterward. - */ -Datum -anser_test_client_roundtrip(PG_FUNCTION_ARGS) -{ - int32 value_arg = PG_GETARG_INT32(0); - char *saved_host = qdHostname; - int saved_port = qdPostmasterPort; - AnserChannelKey key; - unsigned char payload[sizeof(int32)]; - void *out = NULL; - Size out_len = 0; - bool cancelled = false; bool ok = false; - qdHostname = anser_loopback_host(); - qdPostmasterPort = PostPortNumber; - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 20; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "client_loopback", ANSER_CONDITION_KEY_SIZE); - - memcpy(payload, &value_arg, sizeof(payload)); - - PG_TRY(); - { - if (AnserClientPublish(&key, 1, payload, sizeof(payload), false, NULL) && - AnserClientConsumeWait(&key, &out, &out_len, &cancelled, NULL) && - !cancelled && - out_len == sizeof(payload) && - memcmp(out, payload, out_len) == 0) - ok = true; - } - PG_FINALLY(); - { - qdHostname = saved_host; - qdPostmasterPort = saved_port; - } - PG_END_TRY(); - - if (out != NULL) - pfree(out); - - PG_RETURN_BOOL(ok); -} - -/* - * Session-token round trip: register this session's token, prove it validates - * for this session user, that a bogus token and a bogus user are rejected, and - * that a second call returns the same token (one token per session). - */ -Datum -anser_test_token_roundtrip(PG_FUNCTION_ARGS) -{ - Oid user = GetSessionUserId(); - char *token = AnserGetOrCreateSessionToken(user); - char *again; - bool ok; - - if (token == NULL) - PG_RETURN_BOOL(false); - - ok = AnserSessionTokenIsValid(user, token) && - !AnserSessionTokenIsValid(user, "00000000000000000000000000000000") && - !AnserSessionTokenIsValid(InvalidOid, token); - - again = AnserGetOrCreateSessionToken(user); - ok = ok && again != NULL && strcmp(again, token) == 0; - - PG_RETURN_BOOL(ok); -} - -/* - * Best loopback target for a libpq connection to our own postmaster: the first - * configured Unix-socket directory when available (avoids TCP/hba surprises), - * otherwise "localhost". - */ -static char * -anser_loopback_host(void) -{ - const char *sockdirs = GetConfigOption("unix_socket_directories", true, false); - - if (sockdirs != NULL && sockdirs[0] == '/') - { - const char *comma = strchr(sockdirs, ','); - Size len = comma != NULL ? (Size) (comma - sockdirs) : strlen(sockdirs); - - return pnstrdup(sockdirs, len); - } - - return pstrdup("localhost"); -} - -/* - * Multi-consumer partial delivery. - * - * Two consumers block concurrently on the same channel (real libpq loopback - * connections to our own coordinator). One is cancelled mid-wait, standing in - * for a broken consumer connection; the other keeps waiting. We then publish - * the payload and assert that the survivor receives it intact while the - * cancelled consumer got no data -- proving delivery is per-consumer, not - * all-or-nothing across consumers. - */ -Datum -anser_test_multi_consumer(PG_FUNCTION_ARGS) -{ - int32 value_arg = PG_GETARG_INT32(0); - char *saved_host = qdHostname; - int saved_port = qdPostmasterPort; - AnserChannelKey key; - unsigned char payload[sizeof(int32)]; - PGconn *keep = NULL; - PGconn *lost = NULL; - bool ok = false; - - qdHostname = anser_loopback_host(); - qdPostmasterPort = PostPortNumber; - - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 31; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "multi_consumer", ANSER_CONDITION_KEY_SIZE); - - memcpy(payload, &value_arg, sizeof(payload)); + key.gp_session_id = gp_session_id; + key.gp_command_count = gp_command_count; + key.condition_id = 77; + strlcpy(key.condition_key, "sideband_roundtrip", ANSER_CONDITION_KEY_SIZE); PG_TRY(); { - /* Producer announces the channel (one producer expected). */ - if (AnserProducerBegin(&key, 1, GetUserId(), superuser())) + producer = ExecInitAnserBloomFilterProduce(&key, 32, 1024 * 1024, 0, 1, + NULL); + if (producer == NULL) + ok = false; + else { - keep = anser_open_consumer(&key); - lost = anser_open_consumer(&key); - - /* Publish only once both consumers have registered wait slots. */ - if (keep != NULL && lost != NULL && - anser_wait_consumer_count(&key, 2)) - { - bool lost_failed; - - /* - * Break the "lost" consumer mid-wait and let its cancel fully - * resolve before publishing, so the send service can never race - * a delivery into it. - */ - anser_cancel_conn(lost); - lost_failed = !anser_consumer_returned_row(lost); - - if (lost_failed && - AnserPublish(&key, payload, sizeof(payload), false)) - ok = anser_consumer_got_payload(keep, payload, - sizeof(payload)); - } + ExecAnserBloomFilterProduceAddDatum(producer, value, false); + ok = ExecAnserBloomFilterProducePublish(producer); + ExecEndAnserBloomFilterProduce(producer); } - } - PG_FINALLY(); - { - if (keep != NULL) - PQfinish(keep); - if (lost != NULL) - PQfinish(lost); - qdHostname = saved_host; - qdPostmasterPort = saved_port; - } - PG_END_TRY(); - - PG_RETURN_BOOL(ok); -} - -/* - * Regression guard: an abandoned consumer must not block channel recycling. - * - * Same shape as anser_test_multi_consumer, but the assertion is specifically - * that the channel does NOT leave stale data behind: after one consumer is - * cancelled mid-wait and the surviving consumers are delivered, the channel - * must recycle to CONSUMED. The cancelled consumer must not count toward the - * expected consumer total, or done_consumers would never catch up and the - * channel would wedge in READY forever (never reclaimable); this helper would - * then time out waiting for CONSUMED and return false. - */ -Datum -anser_test_abandoned_consumer_recycles(PG_FUNCTION_ARGS) -{ - int32 value_arg = PG_GETARG_INT32(0); - char *saved_host = qdHostname; - int saved_port = qdPostmasterPort; - AnserChannelKey key; - unsigned char payload[sizeof(int32)]; - int nseg = getgpsegmentCount(); - PGconn **keep; - PGconn *lost = NULL; - int i; - bool ok = false; - - if (nseg < 1) - nseg = 1; - - qdHostname = anser_loopback_host(); - qdPostmasterPort = PostPortNumber; - - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 32; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "abandon_recycle", ANSER_CONDITION_KEY_SIZE); - - memcpy(payload, &value_arg, sizeof(payload)); - /* - * A channel recycles to CONSUMED once expected_consumers (== segment count, - * one consumer per segment) have been delivered. Open exactly that many - * surviving consumers plus one that abandons mid-wait: the abandoned one must - * neither receive data nor block the recycle once the survivors are served. - */ - keep = (PGconn **) palloc0(sizeof(PGconn *) * nseg); - - PG_TRY(); - { - bool all_open = true; - - if (AnserProducerBegin(&key, 1, GetUserId(), superuser())) + if (ok) { - for (i = 0; i < nseg; i++) + consumer = ExecInitAnserBloomFilterConsume(&key, 32, 1024 * 1024, 1, + NULL); + if (consumer == NULL) + ok = false; + else { - keep[i] = anser_open_consumer(&key); - if (keep[i] == NULL) - all_open = false; - } - lost = anser_open_consumer(&key); - - if (all_open && lost != NULL && - anser_wait_consumer_count(&key, nseg + 1)) - { - anser_cancel_conn(lost); - (void) anser_consumer_returned_row(lost); - - if (AnserPublish(&key, payload, sizeof(payload), false)) - { - bool all_got = true; - - for (i = 0; i < nseg; i++) - { - if (!anser_consumer_got_payload(keep[i], payload, - sizeof(payload))) - all_got = false; - } - if (all_got) - ok = anser_wait_channel_consumed(&key); - } + ok = ExecAnserBloomFilterConsume(consumer, 1000) && + ExecAnserBloomFilterConsumerGetFilter(consumer) != NULL && + ExecAnserBloomFilterConsumerReceivedParts(consumer) == 1 && + !ExecAnserBloomFilterConsumerWasCancelled(consumer) && + !bloom_lacks_element(ExecAnserBloomFilterConsumerGetFilter(consumer), + (unsigned char *) &value, + sizeof(Datum)); + ExecEndAnserBloomFilterConsume(consumer); } } } PG_FINALLY(); { - for (i = 0; i < nseg; i++) - if (keep[i] != NULL) - PQfinish(keep[i]); - if (lost != NULL) - PQfinish(lost); - qdHostname = saved_host; - qdPostmasterPort = saved_port; + AnserSidebandResetAll(); } PG_END_TRY(); PG_RETURN_BOOL(ok); } - -/* - * Payload-DSM lifetime, scenario (1): 5 producers, N (= segment count) consumers, - * successful delivery. The shared payload DSM must survive past the last consume - * (the recycle to CONSUMED does not free it) and be released only when the sweep - * reclaims the drained channel. We keep the sweep paused to observe the deferred - * state, then sweep explicitly. - */ -Datum -anser_test_dsm_free_on_success(PG_FUNCTION_ARGS) -{ - char *saved_host = qdHostname; - int saved_port = qdPostmasterPort; - AnserChannelKey key; - char *part; - Size part_len = 0; - int nseg = getgpsegmentCount(); - PGconn **cons; - int i; - bool all_read = true; - bool present_after_consume = false; - bool gone_after_sweep = false; - - if (nseg < 1) - nseg = 1; - - AnserSetSweepEnabled(false); /* observe the deferred free ourselves */ - qdHostname = anser_loopback_host(); - qdPostmasterPort = PostPortNumber; - - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 40; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "dsm_success", ANSER_CONDITION_KEY_SIZE); - part = anser_make_test_part(key.condition_key, 7, &part_len); - - cons = (PGconn **) palloc0(sizeof(PGconn *) * nseg); - - PG_TRY(); - { - bool ready = false; - - /* 5 producers publish until the channel is READY (payload allocated). */ - if (part != NULL && AnserProducerBegin(&key, 5, GetUserId(), superuser())) - { - int p; - - ready = true; - for (p = 0; p < 5; p++) - if (!AnserPublish(&key, part, part_len, false)) - ready = false; - } - - if (ready) - { - bool all_open = true; - - for (i = 0; i < nseg; i++) - { - cons[i] = anser_open_consumer(&key); - if (cons[i] == NULL) - all_open = false; - } - - if (all_open && anser_wait_consumer_count(&key, nseg)) - { - /* Every consumer receives and copies out the shared payload. */ - for (i = 0; i < nseg; i++) - if (!anser_consumer_returned_row(cons[i])) - all_read = false; - - /* Consumed, but the payload DSM is still pinned (freed by sweep). */ - present_after_consume = AnserChannelPayloadBytes(&key) > 0; - - AnserSetSweepEnabled(true); - AnserServiceMaintenance(); - AnserSetSweepEnabled(false); - - gone_after_sweep = AnserChannelPayloadBytes(&key) < 0; - } - } - } - PG_FINALLY(); - { - /* sweep_enabled is shared postmaster-wide state: always restore it. */ - AnserSetSweepEnabled(true); - for (i = 0; i < nseg; i++) - if (cons[i] != NULL) - PQfinish(cons[i]); - qdHostname = saved_host; - qdPostmasterPort = saved_port; - } - PG_END_TRY(); - - PG_RETURN_BOOL(all_read && present_after_consume && gone_after_sweep); -} - -/* - * Payload-DSM lifetime, scenario (2): only 3 of 5 producers publish, so the - * channel never reaches READY. It stays COLLECTING with a partial payload until - * the produce deadline elapses, at which point the gather maintenance cancels it - * and frees the payload. (Consumers are omitted: a COLLECTING channel is never - * delivered, so nothing borrows the payload -- the free needs no consumers.) - */ -Datum -anser_test_dsm_free_on_timeout(PG_FUNCTION_ARGS) -{ - AnserChannelKey key; - char *part; - Size part_len = 0; - char saved_timeout[32]; - bool present_collecting = false; - bool freed_after_timeout = false; - - AnserSetSweepEnabled(false); - snprintf(saved_timeout, sizeof(saved_timeout), "%d", gp_anser_timeout_ms); - SetConfigOption("anser.timeout_ms", "100", PGC_USERSET, PGC_S_SESSION); - - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 41; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "dsm_timeout", ANSER_CONDITION_KEY_SIZE); - part = anser_make_test_part(key.condition_key, 5, &part_len); - - PG_TRY(); - { - int p; - - if (part != NULL && AnserProducerBegin(&key, 5, GetUserId(), superuser())) - { - for (p = 0; p < 3; p++) - (void) AnserPublish(&key, part, part_len, false); - - present_collecting = AnserChannelPayloadBytes(&key) > 0; - - /* - * Past the produce deadline the gather maintenance cancels the - * still-COLLECTING channel and frees its partial payload. Drive one - * gather cycle after the timeout so this is deterministic. - */ - pg_usleep(200000L); /* 200 ms > anser.timeout_ms (100 ms) */ - AnserGatherServiceCycle(); - - freed_after_timeout = AnserChannelPayloadBytes(&key) <= 0; - } - } - PG_FINALLY(); - { - SetConfigOption("anser.timeout_ms", saved_timeout, - PGC_USERSET, PGC_S_SESSION); - /* sweep_enabled is shared postmaster-wide state: always restore it. */ - AnserSetSweepEnabled(true); - AnserServiceMaintenance(); /* reclaim the cancelled entry */ - } - PG_END_TRY(); - - PG_RETURN_BOOL(present_collecting && freed_after_timeout); -} - -/* - * Payload-DSM lifetime, scenario (3): 5 producers, N consumers, then the query is - * cancelled while consumers are attached. The cancel must NOT free the payload - * DSM (a consumer may still be borrowing it); it is released only after every - * consumer slot has drained and the sweep reclaims the channel. - */ -Datum -anser_test_dsm_free_on_cancel(PG_FUNCTION_ARGS) -{ - char *saved_host = qdHostname; - int saved_port = qdPostmasterPort; - AnserChannelKey key; - char *part; - Size part_len = 0; - int nseg = getgpsegmentCount(); - PGconn **cons; - int i; - bool present_after_cancel = false; - bool gone_after_sweep = false; - - if (nseg < 1) - nseg = 1; - - AnserSetSweepEnabled(false); - qdHostname = anser_loopback_host(); - qdPostmasterPort = PostPortNumber; - - MemSet(&key, 0, sizeof(key)); - key.gp_session_id = 42; - key.gp_command_count = 1; - key.condition_id = 1; - strlcpy(key.condition_key, "dsm_cancel", ANSER_CONDITION_KEY_SIZE); - part = anser_make_test_part(key.condition_key, 9, &part_len); - - cons = (PGconn **) palloc0(sizeof(PGconn *) * nseg); - - PG_TRY(); - { - bool ready = false; - - if (part != NULL && AnserProducerBegin(&key, 5, GetUserId(), superuser())) - { - int p; - - ready = true; - for (p = 0; p < 5; p++) - if (!AnserPublish(&key, part, part_len, false)) - ready = false; - } - - if (ready) - { - bool all_open = true; - - for (i = 0; i < nseg; i++) - { - cons[i] = anser_open_consumer(&key); - if (cons[i] == NULL) - all_open = false; - } - - if (all_open && anser_wait_consumer_count(&key, nseg)) - { - /* - * Cancel with consumers attached. This marks the channel - * cancelled but must leave the payload DSM pinned -- a consumer - * may still be borrowing it -- so it is still present right after. - */ - AnserCancelQuery(key.gp_session_id, key.gp_command_count); - present_after_cancel = AnserChannelPayloadBytes(&key) > 0; - - /* Drain every consumer (each reads its copy or gets cancelled). */ - for (i = 0; i < nseg; i++) - (void) anser_consumer_returned_row(cons[i]); - - /* Slots drained: the sweep may now reclaim and free the payload. */ - AnserSetSweepEnabled(true); - AnserServiceMaintenance(); - AnserSetSweepEnabled(false); - - gone_after_sweep = AnserChannelPayloadBytes(&key) < 0; - } - } - } - PG_FINALLY(); - { - /* sweep_enabled is shared postmaster-wide state: always restore it. */ - AnserSetSweepEnabled(true); - for (i = 0; i < nseg; i++) - if (cons[i] != NULL) - PQfinish(cons[i]); - qdHostname = saved_host; - qdPostmasterPort = saved_port; - } - PG_END_TRY(); - - PG_RETURN_BOOL(present_after_cancel && gone_after_sweep); -} - -/* - * Open a loopback connection to our coordinator and fire anser.consume_wait - * asynchronously (binary result), leaving the connection blocked server-side. - */ -static PGconn * -anser_open_consumer(const AnserChannelKey *key) -{ - const char *keywords[5]; - const char *values[5]; - const char *params[4]; - char portstr[12]; - char ssid[12]; - char ccnt[12]; - char condid[12]; - PGconn *conn; - int n = 0; - - snprintf(portstr, sizeof(portstr), "%d", qdPostmasterPort); - - keywords[n] = "host"; - values[n] = qdHostname; - n++; - keywords[n] = "port"; - values[n] = portstr; - n++; - keywords[n] = "dbname"; - values[n] = get_database_name(MyDatabaseId); - n++; - keywords[n] = "user"; - values[n] = GetUserNameFromId(GetUserId(), false); - n++; - keywords[n] = NULL; - values[n] = NULL; - - conn = PQconnectdbParams(keywords, values, false); - if (conn == NULL) - return NULL; - if (PQstatus(conn) != CONNECTION_OK) - { - PQfinish(conn); - return NULL; - } - - snprintf(ssid, sizeof(ssid), "%d", key->gp_session_id); - snprintf(ccnt, sizeof(ccnt), "%d", key->gp_command_count); - snprintf(condid, sizeof(condid), "%d", (int) key->condition_id); - params[0] = ssid; - params[1] = ccnt; - params[2] = condid; - params[3] = key->condition_key; - - if (!PQsendQueryParams(conn, - "SELECT anser.consume_wait($1::int4, $2::int4, $3::int4, $4::text)", - 4, NULL, params, NULL, NULL, 1)) - { - PQfinish(conn); - return NULL; - } - - return conn; -} - -/* - * Poll the shared channel map until at least `target` consumers have subscribed - * (or a bounded timeout elapses). We share the coordinator's shmem, so we read - * the count directly rather than through the connections. - */ -static bool -anser_wait_consumer_count(const AnserChannelKey *key, int target) -{ - int i; - - for (i = 0; i < 1000; i++) /* up to ~10s */ - { - CHECK_FOR_INTERRUPTS(); - if (AnserChannelConsumerCount(key) >= target) - return true; - pg_usleep(10000); /* 10ms */ - } - - return false; -} - -/* Send a cancel request for conn's in-flight query (best effort). */ -static void -anser_cancel_conn(PGconn *conn) -{ - PGcancel *cancel = PQgetCancel(conn); - - if (cancel != NULL) - { - char errbuf[256]; - - (void) PQcancel(cancel, errbuf, sizeof(errbuf)); - PQfreeCancel(cancel); - } -} - -/* - * Pump a connection until its outstanding query stops being busy (result ready) - * or a bounded timeout elapses. Returns false on connection loss/timeout. - */ -static bool -anser_drain_until_idle(PGconn *conn) -{ - int i; - - for (i = 0; i < 1000; i++) /* up to ~100s worst case; resolves in ms */ - { - CHECK_FOR_INTERRUPTS(); - if (!PQconsumeInput(conn)) - return false; - if (!PQisBusy(conn)) - return true; - - (void) WaitLatchOrSocket(MyLatch, - WL_LATCH_SET | WL_SOCKET_READABLE | - WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - PQsocket(conn), 100L, PG_WAIT_EXTENSION); - ResetLatch(MyLatch); - } - - return false; -} - -/* True iff the consumer returned exactly the expected payload bytes. */ -static bool -anser_consumer_got_payload(PGconn *conn, const unsigned char *expected, - Size expected_len) -{ - PGresult *res; - PGresult *tmp; - bool ok = false; - - if (!anser_drain_until_idle(conn)) - return false; - - res = PQgetResult(conn); - if (res != NULL && PQresultStatus(res) == PGRES_TUPLES_OK && - PQntuples(res) == 1 && !PQgetisnull(res, 0, 0) && - (Size) PQgetlength(res, 0, 0) == expected_len && - memcmp(PQgetvalue(res, 0, 0), expected, expected_len) == 0) - ok = true; - - if (res != NULL) - PQclear(res); - while ((tmp = PQgetResult(conn)) != NULL) - PQclear(tmp); - - return ok; -} - -/* True iff the consumer returned a non-null data row (it should not have). */ -static bool -anser_consumer_returned_row(PGconn *conn) -{ - PGresult *res; - bool got_row = false; - - if (!anser_drain_until_idle(conn)) - return false; - - while ((res = PQgetResult(conn)) != NULL) - { - if (PQresultStatus(res) == PGRES_TUPLES_OK && - PQntuples(res) >= 1 && !PQgetisnull(res, 0, 0)) - got_row = true; - PQclear(res); - } - - return got_row; -} - -/* - * Poll (bounded) until the channel recycles to CONSUMED, or has already been - * reclaimed entirely. Either outcome means it did not leave stale data behind; - * a channel wedged in READY never reaches this and the poll times out. - */ -static bool -anser_wait_channel_consumed(const AnserChannelKey *key) -{ - int i; - - for (i = 0; i < 1000; i++) /* up to ~10s */ - { - bool found = false; - AnserChannelState state = AnserChannelGetState(key, &found); - - if (!found || state == ANSER_CHANNEL_CONSUMED) - return true; - - CHECK_FOR_INTERRUPTS(); - pg_usleep(10000); /* 10ms */ - } - - return false; -} - -/* - * Pause or resume the background maintenance sweep. With it paused, terminal - * (CANCELLED/CONSUMED) channels stay in the map so tests can assert their state - * without racing the gather/send services. - */ -Datum -anser_test_set_sweep(PG_FUNCTION_ARGS) -{ - bool enabled = PG_GETARG_BOOL(0); - - AnserSetSweepEnabled(enabled); - PG_RETURN_VOID(); -} - -/* - * Guard regression for AnserMaxChannels()'s memoization. - * - * The channel map is sized once at postmaster start; AnserMaxChannels() caches - * that result so a later per-session SET gp_max_slices cannot report a size that - * disagrees with the shared memory actually allocated (which would let the Len - * functions index past the arrays). Read the effective size, change - * gp_max_slices to a value that -- absent the cache -- would grow the auto-sized - * map by orders of magnitude, read again, and assert it did not budge. Runs - * entirely inside this one backend so the two reads bracket the SET. - */ -Datum -anser_test_max_channels_stable_across_slices(PG_FUNCTION_ARGS) -{ - int before = AnserMaxChannels(); - char saved[32]; - bool stable; - - /* Preserve the session value so the test leaves no residue behind. */ - snprintf(saved, sizeof(saved), "%d", gp_max_slices); - - /* MaxConnections * 1000000 would dwarf any real map if recomputed live. */ - SetConfigOption("gp_max_slices", "1000000", PGC_USERSET, PGC_S_SESSION); - - stable = (AnserMaxChannels() == before && before > 0); - - SetConfigOption("gp_max_slices", saved, PGC_USERSET, PGC_S_SESSION); - - PG_RETURN_BOOL(stable); -} - -/* - * Run one maintenance sweep synchronously in this backend (respects the enable - * flag), so a test can prove that reclamation clears terminal channels. - */ -Datum -anser_test_sweep(PG_FUNCTION_ARGS) -{ - AnserServiceMaintenance(); - PG_RETURN_VOID(); -} - -/* - * Build a serialized single bloom part (index 0 of 1) carrying one int value, - * seeded from condition_key so every part on the same channel is byte-identical - * in size and parameters (letting the coordinator OR-fold them in place). The - * caller frees the returned buffer; *len_out gets the serialized length. - */ -static char * -anser_make_test_part(const char *condition_key, int32 value, Size *len_out) -{ - uint64 seed = AnserBloomSeed(condition_key); - bloom_filter *filter = AnserBloomCreate(ANSER_TEST_ELEMS, - ANSER_TEST_MAX_PAYLOAD, seed); - Datum d = Int32GetDatum(value); - Size sz; - Size len = 0; - char *buf; - - bloom_add_element(filter, (unsigned char *) &d, sizeof(Datum)); - sz = AnserBloomSerializedSize(filter); - buf = palloc(sz); - if (!AnserBloomSerializePart(filter, 0, 1, buf, sz, &len)) - { - bloom_free(filter); - pfree(buf); - return NULL; - } - bloom_free(filter); - *len_out = len; - return buf; -} - -/* - * Fill key from the common leading args (session id, command count, condition - * id, condition key) shared by most test functions. Returns false on invalid - * input. - */ -static bool -build_test_key(FunctionCallInfo fcinfo, AnserChannelKey *key) -{ - int32 gp_session_id = PG_GETARG_INT32(0); - int32 gp_command_count = PG_GETARG_INT32(1); - int32 condition_id_arg = PG_GETARG_INT32(2); - char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); - - if (key == NULL || condition_id_arg < 0) - return false; - - if (strlen(condition_key) >= ANSER_CONDITION_KEY_SIZE) - return false; - - MemSet(key, 0, sizeof(AnserChannelKey)); - key->gp_session_id = gp_session_id; - key->gp_command_count = gp_command_count; - key->condition_id = (uint32) condition_id_arg; - strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); - return true; -} - -/* Printable name for a channel state ("UNKNOWN" when out of range). */ -static const char * -state_to_string(AnserChannelState state) -{ - switch (state) - { - case ANSER_CHANNEL_PENDING: - return "PENDING"; - case ANSER_CHANNEL_COLLECTING: - return "COLLECTING"; - case ANSER_CHANNEL_READY: - return "READY"; - case ANSER_CHANNEL_CANCELLED: - return "CANCELLED"; - case ANSER_CHANNEL_CONSUMED: - return "CONSUMED"; - } - - return "UNKNOWN"; -} diff --git a/gpcontrib/anser/src/anserauth.c b/gpcontrib/anser/src/anserauth.c deleted file mode 100644 index a213eb0a8f6..00000000000 --- a/gpcontrib/anser/src/anserauth.c +++ /dev/null @@ -1,430 +0,0 @@ -/*------------------------------------------------------------------------- - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * anserauth.c - * Authentication of the segment -> coordinator Anser connections. - * - * Two halves of one mechanism. The QD side owns a shared-memory hash of - * per-session tokens, handed out at plan time (AnserGetOrCreateSessionToken) - * and verified when a segment presents one (AnserSessionTokenIsValid). The - * backend side supplies the two functions _PG_init installs as the server's - * custom-authentication hooks: AnserConnClaims recognizes an Anser connection - * from its startup marker, and AnserConnCheckPassword accepts or rejects the - * token it sends as password. The wire exchange itself stays in - * libpq/auth.c -- see the CustomAuth*_hook comments there. - * - * IDENTIFICATION - * gpcontrib/anser/src/anserauth.c - * - *------------------------------------------------------------------------- - */ -#include "postgres.h" - -#include - -#include "anser.h" -#include "cdb/cdbvars.h" -#include "common/hashfn.h" -#include "libpq/libpq-be.h" -#include "miscadmin.h" -#include "storage/ipc.h" -#include "storage/lwlock.h" -#include "storage/shmem.h" -#include "utils/acl.h" -#include "utils/builtins.h" -#include "utils/guc.h" -#include "utils/hsearch.h" - -#define ANSER_TOKEN_HASH_NAME "Anser Session Token Hash" - -/* - * Session token hash. - * - * Remote (segment) producers/consumers authenticate their libpq connection to - * the QD with a per-session random token instead of relying on pg_hba entries - * covering the segment hosts -- the parallel-retrieve-cursor model (see - * retrieve_conn_authentication in libpq/auth.c). The QD registers one token - * per (gp_session_id, session user) when a plan gets its first injected - * runtime filter and embeds the token in the dispatched plan; the segment - * connects with anser.conn=true and presents the token as the password, - * and AnserSessionTokenIsValid() verifies it here. Entries are removed when - * the owning QD session exits. - */ -#define ANSER_TOKEN_BYTES 16 /* 128 bits, as ENDPOINT_TOKEN_ARR_LEN */ -#define ANSER_TOKEN_HEX_LEN (ANSER_TOKEN_BYTES * 2) - -/* Token hash key: one token per (gp_session_id, session user). */ -typedef struct AnserTokenTag -{ - int session_id; - Oid user_id; -} AnserTokenTag; - -/* Token hash entry: the hex-encoded random token registered by a session. */ -typedef struct AnserTokenEntry -{ - AnserTokenTag tag; - char token_hex[ANSER_TOKEN_HEX_LEN + 1]; -} AnserTokenEntry; - -static HTAB *AnserTokenHash = NULL; - -/* Set once this backend has registered its session-token cleanup hook. */ -static bool anser_token_exit_registered = false; - -static bool AnserAuthInitialized(void); -static void AnserInitializeTokenHash(void); -static void AnserTokenSessionCleanup(int code, Datum arg); - -/* - * Client authentication for incoming segment -> QD connections. - */ -static bool AnserConnMarkedInCmdOptions(char *cmd_options); -static bool AnserConnMarkedInGucOptions(List *guc_options); - -/* - * Shared-memory sizing and setup for the session-token hash, called from - * AnserShmemSize() / AnserShmemInit() so all Anser shared state is requested - * and created in one place. - */ -Size -AnserAuthShmemSize(void) -{ - return hash_estimate_size(MaxConnections, sizeof(AnserTokenEntry)); -} - -void -AnserAuthShmemInit(void) -{ - AnserInitializeTokenHash(); -} - -/* - * Is the token hash usable? Mirrors AnserInitialized() in anser.c for the - * state this file owns; AnserChannelLock guards the hash and is resolved in - * AnserShmemInit(). - */ -static bool -AnserAuthInitialized(void) -{ - return gp_anser_enable && AnserChannelLock != NULL && - AnserTokenHash != NULL; -} - -static void -AnserInitializeTokenHash(void) -{ - HASHCTL hctl; - - MemSet(&hctl, 0, sizeof(hctl)); - hctl.keysize = sizeof(AnserTokenTag); - hctl.entrysize = sizeof(AnserTokenEntry); - hctl.hash = tag_hash; - - /* One entry per concurrent session; removed when the session exits. */ - AnserTokenHash = ShmemInitHash(ANSER_TOKEN_HASH_NAME, - MaxConnections, - MaxConnections, - &hctl, - HASH_ELEM | HASH_FUNCTION); -} - -/* - * Drop this session's token entry at backend exit. Registered once by the - * first AnserGetOrCreateSessionToken() call in the backend. - */ -static void -AnserTokenSessionCleanup(int code, Datum arg) -{ - AnserTokenTag tag; - - if (AnserTokenHash == NULL) - return; - - tag.session_id = gp_session_id; - tag.user_id = DatumGetObjectId(arg); - - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - (void) hash_search(AnserTokenHash, &tag, HASH_REMOVE, NULL); - LWLockRelease(AnserChannelLock); -} - -/* - * AnserGetOrCreateSessionToken - * - * Return this session's token (palloc'd hex string), generating and - * registering it on first use. NULL when the subsystem is off, the user id - * is invalid, or the token hash is full -- callers fail open (connect without - * the token, i.e. fall back to pg_hba-driven authentication). - * - * user_id must be the *session* user: segment executors connect back to the - * QD as the session user (cdbconn passes MyProcPort->user_name), regardless - * of any SET ROLE in effect on the QD. - */ -char * -AnserGetOrCreateSessionToken(Oid user_id) -{ - AnserTokenTag tag; - AnserTokenEntry *entry; - bool found; - char token_hex[ANSER_TOKEN_HEX_LEN + 1]; - bool have_token = false; - - if (!AnserAuthInitialized() || !OidIsValid(user_id)) - return NULL; - - tag.session_id = gp_session_id; - tag.user_id = user_id; - - /* Copy the token into a stack buffer: no palloc while holding the lock. */ - LWLockAcquire(AnserChannelLock, LW_EXCLUSIVE); - entry = (AnserTokenEntry *) hash_search(AnserTokenHash, &tag, - HASH_ENTER, &found); - if (entry != NULL) - { - if (!found) - { - uint8 token[ANSER_TOKEN_BYTES]; - - if (!pg_strong_random(token, ANSER_TOKEN_BYTES)) - { - (void) hash_search(AnserTokenHash, &tag, HASH_REMOVE, NULL); - entry = NULL; - } - else - { - hex_encode((const char *) token, ANSER_TOKEN_BYTES, - entry->token_hex); - entry->token_hex[ANSER_TOKEN_HEX_LEN] = '\0'; - } - } - if (entry != NULL) - { - strlcpy(token_hex, entry->token_hex, sizeof(token_hex)); - have_token = true; - } - } - LWLockRelease(AnserChannelLock); - - if (!have_token) - return NULL; - - if (!anser_token_exit_registered) - { - anser_token_exit_registered = true; - before_shmem_exit(AnserTokenSessionCleanup, ObjectIdGetDatum(user_id)); - } - - return pstrdup(token_hex); -} - -/* - * AnserSessionTokenIsValid - * - * Token check behind AnserConnCheckPassword(): true iff some live session of - * this exact user registered this token. Runs before InitPostgres in the - * accepting backend; shared-memory pointers are inherited from the postmaster, - * so no attach is needed. - */ -bool -AnserSessionTokenIsValid(Oid user_id, const char *token_hex) -{ - HASH_SEQ_STATUS status; - AnserTokenEntry *entry; - bool valid = false; - - if (!AnserAuthInitialized() || !OidIsValid(user_id) || token_hex == NULL || - strlen(token_hex) != ANSER_TOKEN_HEX_LEN) - return false; - - LWLockAcquire(AnserChannelLock, LW_SHARED); - hash_seq_init(&status, AnserTokenHash); - while ((entry = (AnserTokenEntry *) hash_seq_search(&status)) != NULL) - { - if (entry->tag.user_id == user_id && - strcmp(entry->token_hex, token_hex) == 0) - { - valid = true; - hash_seq_term(&status); - break; - } - } - LWLockRelease(AnserChannelLock); - - return valid; -} - -/* - * CustomAuthClaims_hook: is this an Anser backward (segment -> QD) connection? - * - * The client marks the connection with anser.conn=true in its startup - * packet, either as a command-line option or as a GUC option, so both sources - * are checked -- the same pair of tests the parallel-retrieve-cursor path in - * libpq/auth.c makes for gp_retrieve_conn. - */ -bool -AnserConnClaims(Port *port) -{ - if (port == NULL) - return false; - - return AnserConnMarkedInCmdOptions(port->cmdline_options) || - AnserConnMarkedInGucOptions(port->guc_options); -} - -/* - * CustomAuthCheckPassword_hook: the password of an Anser connection is the - * per-session token the QD handed to the segment in the plan. The connecting - * user must be the session user that registered it. - */ -bool -AnserConnCheckPassword(Port *port, const char *passwd) -{ - Oid owner_uid; - - if (port == NULL || passwd == NULL) - return false; - - owner_uid = get_role_oid(port->user_name, false); - - return AnserSessionTokenIsValid(owner_uid, passwd); -} - -/* - * Return true if the command line contains anser.conn=true. Mirrors - * cmd_options_include_retrieve_conn() in libpq/auth.c. - */ -static bool -AnserConnMarkedInCmdOptions(char *cmd_options) -{ - char **av; - int maxac; - int ac; - int flag; - bool ret = false; - - if (!cmd_options) - return false; - - maxac = 2 + (strlen(cmd_options) + 1) / 2; - - av = (char **) palloc(maxac * sizeof(char *)); - ac = 0; - - av[ac++] = "dummy"; - - pg_split_opts(av, &ac, cmd_options); - - av[ac] = NULL; - -#ifdef HAVE_INT_OPTERR - opterr = 0; -#endif - - while ((flag = getopt(ac, av, "c:-:")) != -1) - { - switch (flag) - { - case 'c': - case '-': - { - char *name, - *value; - - ParseLongOption(optarg, &name, &value); - if (!value) - { - if (flag == '-') - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("--%s requires a value", - optarg))); - else - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("-c %s requires a value", - optarg))); - } - - if ((guc_name_compare(name, "anser.conn") == 0) && - !parse_bool(value, &ret)) - { - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("invalid value for guc anser.conn: \"%s\"", - value))); - } - - pfree(name); - pfree(value); - break; - } - - default: - break; - } - } - - /* - * Reset getopt(3) library so that it will work correctly in subprocesses - * or when this function is called a second time with another array. - */ - optind = 1; -#ifdef HAVE_INT_OPTRESET - optreset = 1; /* some systems need this too */ -#endif - - return ret; -} - -/* - * Return true if startup GUC options contain anser.conn=true. Mirrors - * guc_options_include_retrieve_conn() in libpq/auth.c. - */ -static bool -AnserConnMarkedInGucOptions(List *guc_options) -{ - ListCell *gucopts; - bool ret = false; - - gucopts = list_head(guc_options); - while (gucopts) - { - char *name; - char *value; - - name = lfirst(gucopts); - gucopts = lnext(guc_options, gucopts); - - value = lfirst(gucopts); - gucopts = lnext(guc_options, gucopts); - - if (guc_name_compare(name, "anser.conn") == 0) - { - /* Do not break in case there are more than one such option. */ - if (!parse_bool(value, &ret)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("invalid value for guc anser.conn: \"%s\"", - value))); - } - } - - return ret; -} diff --git a/gpcontrib/anser/src/anserbloomconsume.c b/gpcontrib/anser/src/anserbloomconsume.c index 06173f8250e..64271097c82 100644 --- a/gpcontrib/anser/src/anserbloomconsume.c +++ b/gpcontrib/anser/src/anserbloomconsume.c @@ -29,22 +29,20 @@ #include "anser.h" #include "anserbloom.h" -#include "anserclient.h" #include "anserfilter.h" +#include "ansersideband.h" #include "cdb/cdbvars.h" /* * State for one Bloom filter consumer. Consumes the merged payload for a * channel exactly once (consumed), either from the coordinator's shared - * memory channel map or over libpq. token authenticates the libpq - * transport on segments and is NULL on the coordinator. cancelled records - * that the producer side aborted instead of delivering the payload. + * coordinator. cancelled records that the producer side aborted instead of + * delivering the payload. */ struct AnserBloomFilterConsumeState { AnserChannelKey channel_key; bloom_filter *filter; - char *token; /* QD session token for the libpq transport, or NULL */ int64 total_elems; /* filter sizing, shared with the producer */ Size max_payload_bytes; uint64 seed; @@ -54,15 +52,15 @@ struct AnserBloomFilterConsumeState bool cancelled; }; -static bool ExecAnserBloomFilterConsumeDirect(AnserBloomFilterConsumeState *state, - long registration_timeout_ms); -static bool ExecAnserBloomFilterConsumeClient(AnserBloomFilterConsumeState *state); +static bool ExecAnserBloomFilterConsumeSideband(AnserBloomFilterConsumeState *state, + long timeout_ms); +static bool ExecAnserBloomFilterConsumeFinish(AnserBloomFilterConsumeState *state, + void *payload, Size payload_len); AnserBloomFilterConsumeState * ExecInitAnserBloomFilterConsume(const AnserChannelKey *channel_key, int64 total_elems, Size max_payload_bytes, - uint32 expected_parts, - const char *token) + uint32 expected_parts) { AnserBloomFilterConsumeState *state; @@ -75,7 +73,6 @@ ExecInitAnserBloomFilterConsume(const AnserChannelKey *channel_key, state->max_payload_bytes = max_payload_bytes; state->seed = AnserBloomSeed(channel_key->condition_key); state->expected_parts = expected_parts; - state->token = (token != NULL && token[0] != '\0') ? pstrdup(token) : NULL; return state; } @@ -89,121 +86,65 @@ ExecAnserBloomFilterConsume(AnserBloomFilterConsumeState *state, if (state->consumed) return state->filter != NULL; - /* - * Coordinator-local consumers read the channel map directly; segment - * executors block on the send service over libpq to the QD. The signatures - * are identical -- only the transport differs. - */ - if (Gp_role == GP_ROLE_EXECUTE) - return ExecAnserBloomFilterConsumeClient(state); - - return ExecAnserBloomFilterConsumeDirect(state, registration_timeout_ms); + return ExecAnserBloomFilterConsumeSideband(state, registration_timeout_ms); } /* - * Direct shared-memory consume path (coordinator): wait for producer - * registration, wait for READY, then copy the merged payload out of the - * channel map. + * Dispatch-connection consume path. + * + * On a segment this blocks on our own dispatch socket until the coordinator + * pushes the merged filter; on the coordinator the merge happened in this very + * process, so there is nothing to wait for and we just read it. */ static bool -ExecAnserBloomFilterConsumeDirect(AnserBloomFilterConsumeState *state, - long registration_timeout_ms) +ExecAnserBloomFilterConsumeSideband(AnserBloomFilterConsumeState *state, + long timeout_ms) { - void *payload; + void *payload = NULL; Size payload_len = 0; bool cancelled = false; - bool ready; - - if (!AnserWaitProducersRegistered(&state->channel_key, - registration_timeout_ms)) - { - state->consumed = true; - return false; - } + bool got; - if (!AnserWaitReady(&state->channel_key, &cancelled)) - { - state->cancelled = cancelled; - state->consumed = true; - return false; - } + if (Gp_role == GP_ROLE_EXECUTE) + got = AnserSidebandConsumeWait(&state->channel_key, &payload, + &payload_len, &cancelled, timeout_ms); + else + got = AnserDispatchLocalConsume(&state->channel_key, &payload, + &payload_len, &cancelled); - payload = palloc((Size) gp_anser_max_info_size); - ready = AnserConsumeReady(&state->channel_key, - payload, - (Size) gp_anser_max_info_size, - &payload_len, - &cancelled); - if (!ready || cancelled) + if (!got || cancelled) { - pfree(payload); + if (payload != NULL) + pfree(payload); state->cancelled = cancelled; state->consumed = true; return false; } - /* - * The coordinator has already unioned every segment's part into one merged - * part (see AnserStorePayloadDSM), so we deserialize a single chunk rather - * than unioning N. The merged header's total_parts records how many parts - * were folded, which we surface as the received count. - */ - { - uint32 part_index = 0; - uint32 folded = 0; - - state->filter = AnserBloomDeserializePart(payload, payload_len, - state->total_elems, - state->max_payload_bytes, - state->seed, - &part_index, &folded); - state->received_parts = (state->filter != NULL) ? folded : 0; - } - pfree(payload); - state->consumed = true; - return state->filter != NULL; + return ExecAnserBloomFilterConsumeFinish(state, payload, payload_len); } /* - * Network consume path (segment). Blocks in the coordinator backend via libpq - * until the send service delivers the whole payload (or cancels this consumer); - * there is no registration/ready polling here -- the wait is unbounded and - * cancellation is the only backstop. + * Turn a merged payload into this consumer's filter. + * + * The coordinator unions every segment's part into one before delivery, so a + * single chunk is deserialized rather than N; the merged header's total_parts + * records how many were folded, which we surface as the received count. */ static bool -ExecAnserBloomFilterConsumeClient(AnserBloomFilterConsumeState *state) +ExecAnserBloomFilterConsumeFinish(AnserBloomFilterConsumeState *state, + void *payload, Size payload_len) { - void *payload = NULL; - Size payload_len = 0; - bool cancelled = false; + uint32 part_index = 0; + uint32 folded = 0; - if (!AnserClientConsumeWait(&state->channel_key, &payload, &payload_len, - &cancelled, state->token) || cancelled) - { - if (payload != NULL) - pfree(payload); - state->cancelled = cancelled; - state->consumed = true; - return false; - } + state->filter = AnserBloomDeserializePart(payload, payload_len, + state->total_elems, + state->max_payload_bytes, + state->seed, + &part_index, &folded); + state->received_parts = (state->filter != NULL) ? folded : 0; - /* - * The coordinator has already unioned every segment's part into one merged - * part (see AnserStorePayloadDSM), so we deserialize a single chunk rather - * than unioning N. The merged header's total_parts records how many parts - * were folded, which we surface as the received count. - */ - { - uint32 part_index = 0; - uint32 folded = 0; - - state->filter = AnserBloomDeserializePart(payload, payload_len, - state->total_elems, - state->max_payload_bytes, - state->seed, - &part_index, &folded); - state->received_parts = (state->filter != NULL) ? folded : 0; - } if (payload != NULL) pfree(payload); state->consumed = true; diff --git a/gpcontrib/anser/src/anserbloomproduce.c b/gpcontrib/anser/src/anserbloomproduce.c index 04bc7b44e0c..0ee3b3856f9 100644 --- a/gpcontrib/anser/src/anserbloomproduce.c +++ b/gpcontrib/anser/src/anserbloomproduce.c @@ -29,21 +29,19 @@ #include "anser.h" #include "anserbloom.h" -#include "anserclient.h" #include "anserfilter.h" +#include "ansersideband.h" #include "cdb/cdbvars.h" /* * State for a single bloom filter producer: the target channel, the filter - * being built, this producer's identity within total_parts, and the QD - * session token used by segments to publish over libpq. published and - * cancelled guard against double publication and drive teardown. + * being built, and this producer's identity within total_parts. published + * and cancelled guard against double publication and drive teardown. */ struct AnserBloomFilterProduceState { AnserChannelKey channel_key; bloom_filter *filter; - char *token; /* QD session token for the libpq transport, or NULL */ uint32 part_index; uint32 total_parts; bool published; @@ -51,20 +49,25 @@ struct AnserBloomFilterProduceState }; /* - * Publish one part, choosing the transport by role: coordinator-local callers - * touch the channel map directly (no self-connection), while segment executors - * go over libpq to the QD. total_parts doubles as expected_producers: each - * producer contributes exactly one part. + * Publish one part. + * + * A segment sends it to the coordinator over the dispatch connection; a + * coordinator-local producer hands it to the same per-query channel table the + * coordinator merges into, since it has no connection to itself. total_parts + * doubles as expected_producers: each producer contributes exactly one part. */ static bool AnserProducePublishPart(AnserBloomFilterProduceState *state, const void *payload, Size payload_len, bool cancelled) { if (Gp_role == GP_ROLE_EXECUTE) - return AnserClientPublish(&state->channel_key, state->total_parts, - payload, payload_len, cancelled, state->token); + return AnserSidebandPublish(&state->channel_key, state->part_index, + state->total_parts, payload, payload_len, + cancelled); - return AnserPublish(&state->channel_key, payload, payload_len, cancelled); + return AnserDispatchLocalPublish(&state->channel_key, state->part_index, + state->total_parts, payload, payload_len, + cancelled); } AnserBloomFilterProduceState * @@ -72,8 +75,7 @@ ExecInitAnserBloomFilterProduce(const AnserChannelKey *channel_key, int64 total_elems, Size max_payload_bytes, uint32 part_index, - uint32 total_parts, - const char *token) + uint32 total_parts) { AnserBloomFilterProduceState *state; uint64 seed; @@ -85,7 +87,6 @@ ExecInitAnserBloomFilterProduce(const AnserChannelKey *channel_key, state->channel_key = *channel_key; state->part_index = part_index; state->total_parts = total_parts; - state->token = (token != NULL && token[0] != '\0') ? pstrdup(token) : NULL; seed = AnserBloomSeed(channel_key->condition_key); state->filter = AnserBloomCreate(total_elems, max_payload_bytes, seed); diff --git a/gpcontrib/anser/src/anserclient.c b/gpcontrib/anser/src/anserclient.c deleted file mode 100644 index 0b33cd4ec03..00000000000 --- a/gpcontrib/anser/src/anserclient.c +++ /dev/null @@ -1,453 +0,0 @@ -/*------------------------------------------------------------------------- - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * anserclient.c - * libpq client helpers for the Anser network transport. - * - * A remote producer/consumer (running on a segment, Gp_role == GP_ROLE_EXECUTE) - * cannot touch the coordinator-resident channel map directly. Instead it opens - * an ordinary libpq connection to the QD -- discovered from gp_qd_hostname / - * gp_qd_port, which the dispatcher injects into every QE -- and calls the - * anser.* SQL functions. When the QD supplied a session token (carried - * in the plan), the connection authenticates with it via the anser.conn - * startup marker, bypassing pg_hba (the parallel-retrieve-cursor model); - * otherwise authentication falls back to pg_hba. Encryption and connection - * lifecycle are inherited from libpq; these helpers are the client edges only. - * - * Everything here is fail-open: a broken connection degrades to unfiltered - * execution, never to a wrong result or an error propagated into the query. - * - * IDENTIFICATION - * gpcontrib/anser/src/anserclient.c - * - *------------------------------------------------------------------------- - */ -#include "postgres.h" - -#include "libpq-fe.h" - -#include "anser.h" -#include "anserclient.h" -#include "cdb/cdbvars.h" -#include "commands/dbcommands.h" -#include "libpq/libpq-be.h" -#include "mb/pg_wchar.h" -#include "miscadmin.h" -#include "storage/latch.h" -#include "utils/wait_event.h" - -/* Enough for the decimal form of any int32 argument. */ -#define ANSER_INT_STRLEN 12 - -static PGconn *anser_client_connect(const char *token); -static int anser_client_exec_bool(PGconn *conn, const char *sql, int nparams, - const char *const *values, const int *lengths, - const int *formats); -static int anser_client_producer_begin(PGconn *conn, - const AnserChannelKey *key, - uint32 expected_producers); -static int anser_client_publish_part(PGconn *conn, - const AnserChannelKey *key, - const void *payload, Size payload_len, - bool cancelled); -static PGresult *anser_client_wait_result(PGconn *conn, const char *sql, - int nparams, - const char *const *values, - const int *lengths, - const int *formats, - int result_format); - -/* - * Open a libpq connection to the QD postmaster, reusing the query's database and - * user. Returns NULL (never raises) on any failure so callers can fail open. - * - * When a session token is given, the connection carries the anser.conn=true - * startup marker and the token as password, which the QD authenticates against - * its session-token hash before pg_hba is consulted (see - * AnserConnCheckPassword, installed as the core custom-auth hook) -- so no - * pg_hba entry for the - * segment hosts is needed. Without a token the connection goes through - * ordinary pg_hba-driven authentication. - */ -static PGconn * -anser_client_connect(const char *token) -{ - const char *keywords[10]; - const char *values[10]; - int n = 0; - char portstr[ANSER_INT_STRLEN]; - const char *dbname; - const char *user; - PGconn *conn; - - if (qdHostname == NULL || qdHostname[0] == '\0' || qdPostmasterPort <= 0) - return NULL; - - snprintf(portstr, sizeof(portstr), "%d", qdPostmasterPort); - - if (MyProcPort != NULL && MyProcPort->database_name != NULL) - dbname = MyProcPort->database_name; - else if (OidIsValid(MyDatabaseId)) - dbname = get_database_name(MyDatabaseId); - else - dbname = NULL; - - if (MyProcPort != NULL && MyProcPort->user_name != NULL) - user = MyProcPort->user_name; - else - user = GetUserNameFromId(GetUserId(), true); - - if (dbname == NULL || user == NULL) - return NULL; - - keywords[n] = "host"; - values[n] = qdHostname; - n++; - keywords[n] = "port"; - values[n] = portstr; - n++; - keywords[n] = "dbname"; - values[n] = dbname; - n++; - keywords[n] = "user"; - values[n] = user; - n++; - keywords[n] = "client_encoding"; - values[n] = GetDatabaseEncodingName(); - n++; - keywords[n] = "connect_timeout"; - values[n] = "10"; - n++; - if (token != NULL && token[0] != '\0') - { - keywords[n] = "password"; - values[n] = token; - n++; - keywords[n] = "options"; - values[n] = "-c anser.conn=true"; - n++; - } - keywords[n] = "application_name"; - values[n] = "anser_rf"; - n++; - keywords[n] = NULL; - values[n] = NULL; - - conn = PQconnectdbParams(keywords, values, false); - if (conn == NULL) - return NULL; - if (PQstatus(conn) != CONNECTION_OK) - { - PQfinish(conn); - return NULL; - } - - return conn; -} - -/* - * Run a bool-returning anser.* function. Returns 1 (true), 0 (false), or -1 - * on any protocol error. - */ -static int -anser_client_exec_bool(PGconn *conn, const char *sql, int nparams, - const char *const *values, const int *lengths, - const int *formats) -{ - PGresult *res; - int ret; - - res = PQexecParams(conn, sql, nparams, NULL, values, lengths, formats, 0); - if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK || - PQntuples(res) != 1 || PQnfields(res) != 1) - { - if (res != NULL) - PQclear(res); - return -1; - } - - if (PQgetisnull(res, 0, 0)) - ret = 0; - else - ret = (strcmp(PQgetvalue(res, 0, 0), "t") == 0) ? 1 : 0; - - PQclear(res); - return ret; -} - -static int -anser_client_producer_begin(PGconn *conn, const AnserChannelKey *key, - uint32 expected_producers) -{ - const char *values[5]; - char ssid[ANSER_INT_STRLEN]; - char ccnt[ANSER_INT_STRLEN]; - char condid[ANSER_INT_STRLEN]; - char expected[ANSER_INT_STRLEN]; - - snprintf(ssid, sizeof(ssid), "%d", key->gp_session_id); - snprintf(ccnt, sizeof(ccnt), "%d", key->gp_command_count); - snprintf(condid, sizeof(condid), "%d", (int) key->condition_id); - snprintf(expected, sizeof(expected), "%u", expected_producers); - - values[0] = ssid; - values[1] = ccnt; - values[2] = condid; - values[3] = key->condition_key; - values[4] = expected; - - return anser_client_exec_bool(conn, - "SELECT anser.producer_begin($1::int4, $2::int4, $3::int4, $4::text, $5::int4)", - 5, values, NULL, NULL); -} - -static int -anser_client_publish_part(PGconn *conn, const AnserChannelKey *key, - const void *payload, Size payload_len, bool cancelled) -{ - const char *values[6]; - int lengths[6]; - int formats[6]; - char ssid[ANSER_INT_STRLEN]; - char ccnt[ANSER_INT_STRLEN]; - char condid[ANSER_INT_STRLEN]; - - snprintf(ssid, sizeof(ssid), "%d", key->gp_session_id); - snprintf(ccnt, sizeof(ccnt), "%d", key->gp_command_count); - snprintf(condid, sizeof(condid), "%d", (int) key->condition_id); - - memset(lengths, 0, sizeof(lengths)); - memset(formats, 0, sizeof(formats)); - - values[0] = ssid; - values[1] = ccnt; - values[2] = condid; - values[3] = key->condition_key; - - /* $5 payload: raw bytea in binary format (empty when cancelling). */ - if (!cancelled && payload != NULL && payload_len > 0) - { - values[4] = (const char *) payload; - lengths[4] = (int) payload_len; - } - else - { - values[4] = ""; - lengths[4] = 0; - } - formats[4] = 1; - - values[5] = cancelled ? "t" : "f"; - - return anser_client_exec_bool(conn, - "SELECT anser.publish($1::int4, $2::int4, $3::int4, $4::text, $5::bytea, $6::bool)", - 6, values, lengths, formats); -} - -bool -AnserClientPublish(const AnserChannelKey *channel_key, - uint32 expected_producers, const void *payload, - Size payload_len, bool cancelled, const char *token) -{ - PGconn *conn; - bool ok = false; - - if (channel_key == NULL) - return false; - - conn = anser_client_connect(token); - if (conn == NULL) - return false; /* fail open */ - - if (anser_client_producer_begin(conn, channel_key, expected_producers) == 1 && - anser_client_publish_part(conn, channel_key, payload, payload_len, - cancelled) == 1) - ok = true; - else if (!cancelled) - { - /* - * Something went wrong mid-publish. Best-effort cancel so the dataset - * dies cleanly rather than leaving consumers to time out. - */ - (void) anser_client_publish_part(conn, channel_key, NULL, 0, true); - } - - PQfinish(conn); - return ok; -} - -/* - * Issue a query and block interruptibly for its result. Unlike PQexecParams, - * this pumps the connection through WaitLatchOrSocket so the calling backend - * still honors query cancellation while the coordinator holds the consumer. On - * interrupt we forward a cancel to the QD backend and re-raise, so the blocked - * anser.consume_wait there unwinds and its wait slot is reaped. - */ -static PGresult * -anser_client_wait_result(PGconn *conn, const char *sql, int nparams, - const char *const *values, const int *lengths, - const int *formats, int result_format) -{ - PGresult *res = NULL; - PGresult *tmp; - bool failed = false; - - if (!PQsendQueryParams(conn, sql, nparams, NULL, values, lengths, formats, - result_format)) - return NULL; - - /* Never "return" from inside PG_TRY: flag failures and handle them after. */ - PG_TRY(); - { - for (;;) - { - CHECK_FOR_INTERRUPTS(); - - if (!PQconsumeInput(conn)) - { - failed = true; - break; - } - - if (!PQisBusy(conn)) - break; - - (void) WaitLatchOrSocket(MyLatch, - WL_LATCH_SET | WL_SOCKET_READABLE | - WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - PQsocket(conn), 1000L, - PG_WAIT_EXTENSION); - ResetLatch(MyLatch); - } - } - PG_CATCH(); - { - PGcancel *cancel = PQgetCancel(conn); - - if (cancel != NULL) - { - char errbuf[256]; - - (void) PQcancel(cancel, errbuf, sizeof(errbuf)); - PQfreeCancel(cancel); - } - PG_RE_THROW(); - } - PG_END_TRY(); - - if (failed) - return NULL; - - res = PQgetResult(conn); - /* Drain any trailing results so the connection is reusable/closable. */ - while ((tmp = PQgetResult(conn)) != NULL) - PQclear(tmp); - - return res; -} - -bool -AnserClientConsumeWait(const AnserChannelKey *channel_key, void **payload, - Size *payload_len, bool *cancelled, const char *token) -{ - PGconn *conn; - PGresult *res; - const char *values[4]; - char ssid[ANSER_INT_STRLEN]; - char ccnt[ANSER_INT_STRLEN]; - char condid[ANSER_INT_STRLEN]; - bool ok = false; - - if (payload != NULL) - *payload = NULL; - if (payload_len != NULL) - *payload_len = 0; - if (cancelled != NULL) - *cancelled = false; - - if (channel_key == NULL) - return false; - - conn = anser_client_connect(token); - if (conn == NULL) - { - if (cancelled != NULL) - *cancelled = true; - return false; - } - - snprintf(ssid, sizeof(ssid), "%d", channel_key->gp_session_id); - snprintf(ccnt, sizeof(ccnt), "%d", channel_key->gp_command_count); - snprintf(condid, sizeof(condid), "%d", (int) channel_key->condition_id); - values[0] = ssid; - values[1] = ccnt; - values[2] = condid; - values[3] = channel_key->condition_key; - - PG_TRY(); - { - res = anser_client_wait_result(conn, - "SELECT anser.consume_wait($1::int4, $2::int4, $3::int4, $4::text)", - 4, values, NULL, NULL, 1); - } - PG_CATCH(); - { - PQfinish(conn); - PG_RE_THROW(); - } - PG_END_TRY(); - - if (res == NULL || PQresultStatus(res) != PGRES_TUPLES_OK || - PQntuples(res) != 1 || PQnfields(res) != 1) - { - if (res != NULL) - PQclear(res); - PQfinish(conn); - if (cancelled != NULL) - *cancelled = true; - return false; - } - - if (PQgetisnull(res, 0, 0)) - { - if (cancelled != NULL) - *cancelled = true; - } - else - { - int len = PQgetlength(res, 0, 0); - char *val = PQgetvalue(res, 0, 0); - void *buf = NULL; - - if (len > 0) - { - buf = palloc(len); - memcpy(buf, val, len); - } - if (payload != NULL) - *payload = buf; - if (payload_len != NULL) - *payload_len = (Size) len; - ok = true; - } - - PQclear(res); - PQfinish(conn); - return ok; -} diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c new file mode 100644 index 00000000000..90f9e7cbe3f --- /dev/null +++ b/gpcontrib/anser/src/anserdispatch.c @@ -0,0 +1,507 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserdispatch.c + * Coordinator side of the dispatch-connection transport. + * + * Everything here runs in the QD backend, reached from cdbdisp_notify_hook + * while the dispatcher drains QE messages -- which happens from inside the + * interconnect wait loop, so this code is on the query's critical path. It + * must stay cheap and must not throw for anything recoverable: an error here + * lands in a running query, whereas losing a filter only costs us an + * unfiltered scan. + * + * Because producer merge and consumer delivery both happen in this one + * process, the accumulator is an ordinary palloc'd buffer. There is no shared + * memory, no DSM segment to attach, and no separate worker to hand data to; + * parts are folded in place as they arrive, so only the final fold is on the + * critical path. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserdispatch.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq-fe.h" +#include "libpq-int.h" + +#include "anser.h" +#include "anserfilter.h" +#include "ansersideband.h" +#include "cdb/cdbdisp.h" +#include "cdb/cdbdispatchresult.h" +#include "cdb/cdbvars.h" +#include "common/base64.h" +#include "lib/stringinfo.h" +#include "nodes/pg_list.h" +#include "utils/hsearch.h" +#include "utils/memutils.h" + +/* + * One channel's merge state, living for the duration of the query that created + * it. Keyed by AnserChannelKey, which must be the first field. + */ +typedef struct AnserDispChannel +{ + AnserChannelKey key; + char *payload; /* merged part, or NULL before the first one */ + Size payload_len; + int parts_received; + int expected_parts; /* 0 until a part tells us; from total_parts */ + bool cancelled; + bool complete; /* every expected part folded, or cancelled */ + List *subscribers; /* PGconn * of QEs awaiting delivery */ +} AnserDispChannel; + +/* Parsed QE -> QD message. */ +typedef struct AnserWireMsg +{ + char kind; + AnserChannelKey key; + int part_index; + int total_parts; + int flags; + const char *body; /* base64, not NUL-terminated */ + int body_len; +} AnserWireMsg; + +static HTAB *AnserDispChannels = NULL; +static MemoryContext AnserDispContext = NULL; + +static AnserDispChannel *anser_disp_lookup(const AnserChannelKey *key, bool create); +static bool anser_disp_parse(const char *msg, AnserWireMsg *out); +static void anser_disp_apply_part(AnserDispChannel *chan, const void *payload, + Size payload_len, int total_parts, bool cancelled); +static void anser_disp_deliver(AnserDispChannel *chan); +static bool anser_disp_push(PGconn *conn, AnserDispChannel *chan); + +/* + * Per-query state lives in its own context so it can be dropped wholesale. + * Channels are keyed by (session, command, condition), so entries from an + * earlier command in the same transaction are distinct and simply unused + * until the reset. + */ +static void +anser_disp_init(void) +{ + HASHCTL hctl; + + if (AnserDispChannels != NULL) + return; + + AnserDispContext = AllocSetContextCreate(TopMemoryContext, + "Anser dispatch transport", + ALLOCSET_DEFAULT_SIZES); + + MemSet(&hctl, 0, sizeof(hctl)); + hctl.keysize = sizeof(AnserChannelKey); + hctl.entrysize = sizeof(AnserDispChannel); + hctl.hcxt = AnserDispContext; + + AnserDispChannels = hash_create("Anser dispatch channels", 32, &hctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +void +AnserDispatchReset(void) +{ + if (AnserDispChannels == NULL) + return; + + hash_destroy(AnserDispChannels); + AnserDispChannels = NULL; + MemoryContextDelete(AnserDispContext); + AnserDispContext = NULL; +} + +static AnserDispChannel * +anser_disp_lookup(const AnserChannelKey *key, bool create) +{ + AnserDispChannel *chan; + bool found; + + anser_disp_init(); + + chan = (AnserDispChannel *) hash_search(AnserDispChannels, key, + create ? HASH_ENTER : HASH_FIND, + &found); + if (chan == NULL) + return NULL; + + if (create && !found) + { + /* hash_search only fills the key; initialize the rest. */ + chan->payload = NULL; + chan->payload_len = 0; + chan->parts_received = 0; + chan->expected_parts = 0; + chan->cancelled = false; + chan->complete = false; + chan->subscribers = NIL; + } + + return chan; +} + +/* + * cdbdisp_notify_hook: is this one of ours, and if so, handle it. + * + * Returns false for any notify on another channel so the dispatcher can carry + * on with its own handling. A malformed payload is consumed (it is addressed + * to us) but otherwise ignored: the affected consumers will time out and run + * unfiltered. + */ +bool +AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, + struct pgNotify *notify) +{ + PGnotify *n = (PGnotify *) notify; + AnserWireMsg msg; + AnserDispChannel *chan; + MemoryContext oldcxt; + + if (n == NULL || n->relname == NULL || + strcmp(n->relname, ANSER_NOTIFY_CHANNEL) != 0) + return false; + + if (n->extra == NULL || !anser_disp_parse(n->extra, &msg)) + { + elog(LOG, "anser: ignoring malformed message from a segment"); + return true; + } + + anser_disp_init(); + oldcxt = MemoryContextSwitchTo(AnserDispContext); + + chan = anser_disp_lookup(&msg.key, true); + if (chan == NULL) + { + MemoryContextSwitchTo(oldcxt); + return true; + } + + if (msg.kind == ANSER_WIRE_KIND_SUBSCRIBE) + { + PGconn *conn = ((CdbDispatchResult *) dispatchResult)->segdbDesc->conn; + + /* + * A consumer can subscribe after the channel is already complete -- + * producers on other segments may well have finished first -- so + * deliver immediately in that case rather than recording interest. + */ + if (chan->complete) + (void) anser_disp_push(conn, chan); + else + chan->subscribers = lappend(chan->subscribers, conn); + } + else if (msg.kind == ANSER_WIRE_KIND_PART) + { + char *raw = NULL; + int raw_len = 0; + + if (!(msg.flags & ANSER_WIRE_F_CANCELLED) && msg.body_len > 0) + { + int maxlen = pg_b64_dec_len(msg.body_len); + + raw = palloc(maxlen); + raw_len = pg_b64_decode(msg.body, msg.body_len, raw, maxlen); + if (raw_len < 0 || raw_len > gp_anser_max_info_size) + { + /* Undecodable or oversized: cancel rather than guess. */ + pfree(raw); + raw = NULL; + raw_len = 0; + msg.flags |= ANSER_WIRE_F_CANCELLED; + } + } + + anser_disp_apply_part(chan, raw, (Size) raw_len, msg.total_parts, + (msg.flags & ANSER_WIRE_F_CANCELLED) != 0); + if (raw != NULL) + pfree(raw); + } + + if (chan->complete) + anser_disp_deliver(chan); + + MemoryContextSwitchTo(oldcxt); + return true; +} + +/* + * Fold one part into the channel's accumulator. + * + * The first part is kept verbatim and becomes the accumulator; later parts are + * OR'd into it in place (AnserBloomFoldPartInPlace), so no part is ever copied + * twice and the accumulator is never reallocated. + */ +static void +anser_disp_apply_part(AnserDispChannel *chan, const void *payload, + Size payload_len, int total_parts, bool cancelled) +{ + if (chan->cancelled) + return; /* already dead; nothing to do */ + + if (total_parts > 0 && chan->expected_parts == 0) + chan->expected_parts = total_parts; + + if (cancelled) + { + chan->cancelled = true; + chan->complete = true; + chan->payload = NULL; + chan->payload_len = 0; + return; + } + + if (payload == NULL || payload_len == 0) + { + /* An empty part still counts toward completion. */ + chan->parts_received++; + } + else if (chan->payload == NULL) + { + chan->payload = palloc(payload_len); + memcpy(chan->payload, payload, payload_len); + chan->payload_len = payload_len; + chan->parts_received++; + } + else if (AnserBloomFoldPartInPlace(chan->payload, chan->payload_len, + payload, payload_len)) + { + chan->parts_received++; + } + else + { + /* + * Sizes or parameters disagree, so the parts cannot be unioned. That + * should not happen (every part on a channel is built from the same + * plan parameters), but if it does the only safe answer is to give up + * on the channel. + */ + elog(LOG, "anser: incompatible part for condition %u; cancelling channel", + chan->key.condition_id); + chan->cancelled = true; + chan->complete = true; + chan->payload = NULL; + chan->payload_len = 0; + return; + } + + if (chan->expected_parts > 0 && chan->parts_received >= chan->expected_parts) + chan->complete = true; +} + +/* Push the finished channel to everyone waiting, then forget them. */ +static void +anser_disp_deliver(AnserDispChannel *chan) +{ + ListCell *lc; + + foreach(lc, chan->subscribers) + (void) anser_disp_push((PGconn *) lfirst(lc), chan); + + list_free(chan->subscribers); + chan->subscribers = NIL; +} + +/* + * Write one merged payload to a QE as a GP_SIDEBAND_MESSAGE. + * + * Delivery is per consumer: a write that fails costs that one segment its + * filter (it will time out and run unfiltered) and leaves the others alone. + * This is the same "try to reach every consumer" rule the shared-memory send + * service followed. + */ +static bool +anser_disp_push(PGconn *conn, AnserDispChannel *chan) +{ + int flags = chan->cancelled ? ANSER_WIRE_F_CANCELLED : 0; + int keylen = (int) strlen(chan->key.condition_key); + int paylen = chan->cancelled ? 0 : (int) chan->payload_len; + + if (conn == NULL || PQstatus(conn) != CONNECTION_OK) + return false; + + /* + * Raw binary: pqPutnchar performs no encoding conversion, so unlike the + * QE -> QD direction this needs no base64. + */ + if (pqPutMsgStart(GP_SIDEBAND_MESSAGE, conn) < 0 || + pqPutInt((int) chan->key.condition_id, 4, conn) < 0 || + pqPutInt(flags, 4, conn) < 0 || + pqPutInt(keylen, 4, conn) < 0 || + pqPutnchar(chan->key.condition_key, keylen, conn) < 0 || + pqPutInt(paylen, 4, conn) < 0 || + (paylen > 0 && pqPutnchar(chan->payload, paylen, conn) < 0) || + pqPutMsgEnd(conn) < 0 || + pqFlush(conn) < 0) + { + elog(LOG, "anser: could not deliver filter for condition %u: %s", + chan->key.condition_id, PQerrorMessage(conn)); + return false; + } + + return true; +} + +/* + * Parse a QE -> QD payload. + * + * Layout: a single-line text header, then the condition key, then the body. + * + * anser1 \n + * + * + * The header holds only numbers and one character, so it cannot contain the + * newline that terminates it; key and body are taken by length, so neither + * needs escaping or a delimiter of its own. + */ +static bool +anser_disp_parse(const char *msg, AnserWireMsg *out) +{ + const char *nl; + const char *rest; + char kind; + int ssid, + ccnt, + condid, + part, + total, + flags, + keylen, + bodylen; + + nl = strchr(msg, '\n'); + if (nl == NULL) + return false; + + if (sscanf(msg, ANSER_WIRE_TAG " %c %d %d %d %d %d %d %d %d", + &kind, &ssid, &ccnt, &condid, &part, &total, &flags, + &keylen, &bodylen) != 9) + return false; + + if (kind != ANSER_WIRE_KIND_PART && kind != ANSER_WIRE_KIND_SUBSCRIBE) + return false; + if (condid < 0 || keylen < 0 || bodylen < 0 || + keylen >= ANSER_CONDITION_KEY_SIZE) + return false; + + rest = nl + 1; + if ((int) strlen(rest) != keylen + bodylen) + return false; + + MemSet(out, 0, sizeof(*out)); + out->kind = kind; + out->key.gp_session_id = ssid; + out->key.gp_command_count = ccnt; + out->key.condition_id = (uint32) condid; + memcpy(out->key.condition_key, rest, keylen); + out->key.condition_key[keylen] = '\0'; + out->part_index = part; + out->total_parts = total; + out->flags = flags; + out->body = rest + keylen; + out->body_len = bodylen; + + return true; +} + +/* + * Coordinator-local producer. + * + * A producer running on the QD has no dispatch connection to itself, so it + * folds straight into the same channel table the hook uses. + */ +bool +AnserDispatchLocalPublish(const AnserChannelKey *channel_key, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, + bool cancelled) +{ + AnserDispChannel *chan; + MemoryContext oldcxt; + + if (channel_key == NULL) + return false; + if (!cancelled && payload_len > (Size) gp_anser_max_info_size) + cancelled = true; + + anser_disp_init(); + oldcxt = MemoryContextSwitchTo(AnserDispContext); + + chan = anser_disp_lookup(channel_key, true); + if (chan != NULL) + { + anser_disp_apply_part(chan, payload, payload_len, (int) total_parts, + cancelled); + if (chan->complete) + anser_disp_deliver(chan); + } + + MemoryContextSwitchTo(oldcxt); + return chan != NULL; +} + +/* + * Coordinator-local consumer. + * + * Does not wait: on the coordinator the producer side of the join has already + * run by the time the probe side asks for the filter, so either the channel is + * complete or it never will be (a squelched producer, say) and we fail open. + */ +bool +AnserDispatchLocalConsume(const AnserChannelKey *channel_key, + void **payload, Size *payload_len, bool *cancelled) +{ + AnserDispChannel *chan; + + if (payload != NULL) + *payload = NULL; + if (payload_len != NULL) + *payload_len = 0; + if (cancelled != NULL) + *cancelled = false; + + if (channel_key == NULL || AnserDispChannels == NULL) + return false; + + chan = anser_disp_lookup(channel_key, false); + if (chan == NULL || !chan->complete) + return false; + + if (chan->cancelled || chan->payload == NULL) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + if (payload != NULL) + { + *payload = palloc(chan->payload_len); + memcpy(*payload, chan->payload, chan->payload_len); + } + if (payload_len != NULL) + *payload_len = chan->payload_len; + + return true; +} diff --git a/gpcontrib/anser/src/anserfuncs.c b/gpcontrib/anser/src/anserfuncs.c deleted file mode 100644 index 2f9b3a4f2f2..00000000000 --- a/gpcontrib/anser/src/anserfuncs.c +++ /dev/null @@ -1,200 +0,0 @@ -/*------------------------------------------------------------------------- - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * anserfuncs.c - * SQL functions that expose the Anser network transport. - * - * These are the thin coordinator-side edges of the Anser data path. Remote - * (segment) producers and consumers reach the coordinator-resident channel map - * by opening an ordinary libpq connection to the QD and calling these functions - * (created by anser--1.0.sql in schema "anser"); all real work happens in the - * gather and send background services. A producer announces itself with - * anser.producer_begin(), streams parts with anser.publish(), and a consumer - * blocks in anser.consume_wait() until the send service delivers its payload - * (or cancels it). - * - * Access control: these functions are intentionally left with the default - * EXECUTE grant to PUBLIC. The network transport connects to the QD as the - * *query's own* role, so restricting them to superusers -- or revoking them - * from PUBLIC -- would silently disable runtime filtering for every - * non-superuser query (it would fail open to unfiltered execution). - * - * The (session id, command count, condition) key cannot be derived server-side: - * each call runs in a fresh coordinator backend the segment opened over - * libpq, with its own session -- not the originating query's -- so the key must - * travel in the call. Because it is caller-supplied, we bind every channel to - * the authenticated role that created it (see AnserChannelEntry.creator_role): - * a caller may only produce/consume on a channel its own role created, unless it - * is a superuser. That blocks the dangerous vector -- one role poisoning - * another role's bloom filter, which could drop matching rows -- and leaves only - * same-role/cross-command self-interference, which fails open, never to wrong - * results. - * - * IDENTIFICATION - * gpcontrib/anser/src/anserfuncs.c - * - *------------------------------------------------------------------------- - */ -#include "postgres.h" - -#include "anser.h" -#include "fmgr.h" -#include "miscadmin.h" -#include "utils/acl.h" -#include "utils/builtins.h" -#include "varatt.h" - -PG_FUNCTION_INFO_V1(anser_producer_begin); -PG_FUNCTION_INFO_V1(anser_publish); -PG_FUNCTION_INFO_V1(anser_consume_wait); - -static bool anser_build_key(int32 gp_session_id, int32 gp_command_count, - int32 condition_id, text *condition_key_text, - AnserChannelKey *key); - -/* - * anser_producer_begin(ssid, ccnt, cond_id, cond_key, expected_producers) - * - * Register (idempotently) the channel and arm its produce deadline. This is the - * "a producer opened a connection" signal; if the channel does not become READY - * within anser.timeout_ms the gather service cancels the whole dataset. - */ -Datum -anser_producer_begin(PG_FUNCTION_ARGS) -{ - int32 gp_session_id = PG_GETARG_INT32(0); - int32 gp_command_count = PG_GETARG_INT32(1); - int32 condition_id = PG_GETARG_INT32(2); - text *condition_key = PG_GETARG_TEXT_PP(3); - int32 expected_producers = PG_GETARG_INT32(4); - AnserChannelKey key; - - if (expected_producers <= 0) - PG_RETURN_BOOL(false); - - if (!anser_build_key(gp_session_id, gp_command_count, condition_id, - condition_key, &key)) - PG_RETURN_BOOL(false); - - PG_RETURN_BOOL(AnserProducerBegin(&key, expected_producers, - GetUserId(), superuser())); -} - -/* - * anser_publish(ssid, ccnt, cond_id, cond_key, payload, cancelled) - * - * Hand one part to the gather service and block for its ACK. expected_producers - * is not repeated here: anser.producer_begin already stamped it on the - * channel, so we pass 0 to leave it unchanged. - */ -Datum -anser_publish(PG_FUNCTION_ARGS) -{ - int32 gp_session_id = PG_GETARG_INT32(0); - int32 gp_command_count = PG_GETARG_INT32(1); - int32 condition_id = PG_GETARG_INT32(2); - text *condition_key = PG_GETARG_TEXT_PP(3); - bytea *payload = PG_GETARG_BYTEA_PP(4); - bool cancelled = PG_GETARG_BOOL(5); - AnserChannelKey key; - - if (!anser_build_key(gp_session_id, gp_command_count, condition_id, - condition_key, &key)) - PG_RETURN_BOOL(false); - - PG_RETURN_BOOL(AnserProducerSubmit(&key, 0, - VARDATA_ANY(payload), - VARSIZE_ANY_EXHDR(payload), - cancelled, - GetUserId(), superuser())); -} - -/* - * anser_consume_wait(ssid, ccnt, cond_id, cond_key) -> bytea - * - * Subscribe, register a wait slot, and block on the proc latch until the send - * service delivers the payload or cancels this consumer. Returns the payload - * bytes on delivery, or NULL when the channel is cancelled/unreachable. Blocks - * the calling coordinator backend for the query's lifetime, per the "consumer - * waits, does not process further" semantics. - */ -Datum -anser_consume_wait(PG_FUNCTION_ARGS) -{ - int32 gp_session_id = PG_GETARG_INT32(0); - int32 gp_command_count = PG_GETARG_INT32(1); - int32 condition_id = PG_GETARG_INT32(2); - text *condition_key = PG_GETARG_TEXT_PP(3); - AnserChannelKey key; - void *payload = NULL; - Size payload_len = 0; - bool cancelled = false; - bytea *result; - - if (!anser_build_key(gp_session_id, gp_command_count, condition_id, - condition_key, &key)) - PG_RETURN_NULL(); - - if (!AnserConsumerWait(&key, &payload, &payload_len, &cancelled, - GetUserId(), superuser()) || - cancelled) - { - if (payload != NULL) - pfree(payload); - PG_RETURN_NULL(); - } - - result = (bytea *) palloc(VARHDRSZ + payload_len); - SET_VARSIZE(result, VARHDRSZ + payload_len); - if (payload_len > 0) - memcpy(VARDATA(result), payload, payload_len); - if (payload != NULL) - pfree(payload); - - PG_RETURN_BYTEA_P(result); -} - -/* - * anser_build_key(ssid, ccnt, cond_id, cond_key, key) - * - * Validate the caller-supplied channel key components and copy them into - * *key. Returns false (fail open) when a component is out of range. - */ -static bool -anser_build_key(int32 gp_session_id, int32 gp_command_count, - int32 condition_id, text *condition_key_text, - AnserChannelKey *key) -{ - char *condition_key = text_to_cstring(condition_key_text); - bool ok = true; - - if (condition_id < 0 || strlen(condition_key) >= ANSER_CONDITION_KEY_SIZE) - ok = false; - else - { - MemSet(key, 0, sizeof(AnserChannelKey)); - key->gp_session_id = gp_session_id; - key->gp_command_count = gp_command_count; - key->condition_id = (uint32) condition_id; - strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); - } - - pfree(condition_key); - return ok; -} diff --git a/gpcontrib/anser/src/anserinit.c b/gpcontrib/anser/src/anserinit.c index 572e970f639..5779ddf240e 100644 --- a/gpcontrib/anser/src/anserinit.c +++ b/gpcontrib/anser/src/anserinit.c @@ -18,17 +18,18 @@ * under the License. * * anserinit.c - * Module entry point: GUCs, shared memory, background services, and the - * core hooks the Anser subsystem hangs off. + * Module entry point: GUCs and the core hooks Anser hangs off. * * Anser is a shared_preload_libraries extension. Everything it needs from the * server is reached through an existing extensibility point: * - * shmem_request_hook / shmem_startup_hook the channel map and its LWLocks - * RegisterBackgroundWorker the gather and send services - * planner_hook runtime-filter injection - * RegisterCustomScanMethods the injected plan nodes - * CustomAuth*_hook segment -> QD token connections + * planner_hook runtime-filter injection + * RegisterCustomScanMethods the injected plan nodes + * cdbdisp_notify_hook parts arriving from segments + * ExecutorEnd_hook dropping a query's channels + * + * It must be preloaded, because a segment backend deserializing a dispatched + * plan has no opportunity to load the library first. * * IDENTIFICATION * gpcontrib/anser/src/anserinit.c @@ -37,33 +38,36 @@ */ #include "postgres.h" +#include "access/xact.h" #include "anser.h" #include "anserplan.h" +#include "ansersideband.h" +#include "cdb/cdbdisp.h" #include "cdb/cdbvars.h" -#include "libpq/auth.h" +#include "executor/executor.h" #include "miscadmin.h" #include "optimizer/planner.h" -#include "postmaster/bgworker.h" -#include "storage/ipc.h" -#include "storage/lwlock.h" -#include "storage/shmem.h" #include "utils/guc.h" PG_MODULE_MAGIC; void _PG_init(void); +bool gp_anser_enable = false; +bool gp_anser_runtime_filter = false; +int gp_anser_max_info_size = 64 * 1024 * 1024 + 1024 * 1024; +int gp_anser_timeout_ms = 1000; + static void anser_define_gucs(void); -static void anser_register_services(void); -static void anser_shmem_request(void); -static void anser_shmem_startup(void); static PlannedStmt *anser_planner(Query *parse, const char *query_string, int cursorOptions, ParamListInfo boundParams, OptimizerOptions *optimizer_options); -static shmem_request_hook_type prev_shmem_request_hook = NULL; -static shmem_startup_hook_type prev_shmem_startup_hook = NULL; static planner_hook_type prev_planner_hook = NULL; +static ExecutorEnd_hook_type prev_ExecutorEnd_hook = NULL; + +static void anser_executor_end(QueryDesc *queryDesc); +static void anser_xact_callback(XactEvent event, void *arg); void _PG_init(void) @@ -71,41 +75,38 @@ _PG_init(void) anser_define_gucs(); /* - * Only a preloaded library can request shared memory, register background - * workers, or be relied on to have installed its hooks in every backend. - * Loaded any other way, Anser stays inert: the GUCs exist (so a stray - * setting is not an error) but nothing is wired up. + * Only a preloaded library can be relied on to have installed its hooks in + * every backend. Loaded any other way, Anser stays inert: the GUCs exist + * (so a stray setting is not an error) but nothing is wired up. */ if (!process_shared_preload_libraries_in_progress) return; - prev_shmem_request_hook = shmem_request_hook; - shmem_request_hook = anser_shmem_request; - prev_shmem_startup_hook = shmem_startup_hook; - shmem_startup_hook = anser_shmem_startup; - prev_planner_hook = planner_hook; planner_hook = anser_planner; + /* + * Channels live in backend memory on both ends, so they need a point to be + * dropped. ExecutorEnd covers the normal path; the transaction callback + * catches queries that end by erroring. + */ + prev_ExecutorEnd_hook = ExecutorEnd_hook; + ExecutorEnd_hook = anser_executor_end; + RegisterXactCallback(anser_xact_callback, NULL); + + /* + * Handle Anser notifies arriving from QEs on the dispatch connections. + * Installed unconditionally: it is inert until a segment sends one, and a + * QE that never dispatches never calls it. + */ + cdbdisp_notify_hook = AnserDispatchNotifyHandler; + /* * The producer and consumer nodes travel to the segments inside dispatched * plans, so every backend must be able to resolve their CustomScan methods * by name. Registering here covers QD and QE alike. */ AnserRegisterRuntimeFilterMethods(); - - if (gp_anser_enable) - { - /* - * Own the authentication of incoming segment -> QD connections. With - * Anser disabled the hooks stay unset and such a connection is simply - * authenticated the ordinary way, through pg_hba. - */ - CustomAuthClaims_hook = AnserConnClaims; - CustomAuthCheckPassword_hook = AnserConnCheckPassword; - - anser_register_services(); - } } /* @@ -117,10 +118,10 @@ anser_define_gucs(void) { DefineCustomBoolVariable("anser.enable", "Enables the Anser adaptive information sharing subsystem.", - "When disabled, Anser does not allocate shared memory and its background services are not started.", + "When disabled, the plan pass never injects anything and no filters are exchanged.", &gp_anser_enable, false, - PGC_POSTMASTER, + PGC_SIGHUP, 0, NULL, NULL, NULL); @@ -133,26 +134,6 @@ anser_define_gucs(void) GUC_EXPLAIN, NULL, NULL, NULL); - DefineCustomBoolVariable("anser.conn", - "Specify this is a connection for the Anser runtime filter transport.", - NULL, - &gp_anser_conn, - false, - PGC_BACKEND, - GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_DISALLOW_IN_FILE, - NULL, NULL, NULL); - - DefineCustomIntVariable("anser.max_channels", - "Sets the maximum number of Anser channels.", - "This value sizes the fixed Anser shared-memory channel map at postmaster start. " - "0 (the default) auto-sizes it to max_connections * gp_max_slices, " - "falling back to a fixed per-connection budget when gp_max_slices is unbounded.", - &gp_anser_max_channels, - 0, 0, INT_MAX, - PGC_POSTMASTER, - 0, - NULL, NULL, NULL); - DefineCustomIntVariable("anser.max_info_size", "Sets the maximum byte size of one Anser information record.", "Per-record DSM payload cap for Anser information. The default holds a full 64 MB bloom-filter bitset plus its serialized-part header.", @@ -171,93 +152,9 @@ anser_define_gucs(void) GUC_UNIT_MS, NULL, NULL, NULL); - DefineCustomIntVariable("anser.max_consumers_per_channel", - "Sets the maximum number of waiting Anser consumers per channel.", - "This value sizes the fixed Anser consumer wait table at postmaster start " - "(anser.max_channels * this). Each channel has one consumer per segment, " - "so it should be set to the number of primary segments; the plan pass injects " - "at most one consumer per channel. It cannot be auto-derived because the " - "segment count is a catalog value unavailable at postmaster start. Over-sizing " - "only wastes shared memory; under-sizing makes surplus consumers fail open " - "(unfiltered), never wrong results.", - &gp_anser_max_consumers_per_channel, - 64, 1, INT_MAX, - PGC_POSTMASTER, - 0, - NULL, NULL, NULL); - MarkGUCPrefixReserved("anser"); } -/* - * Register the gather and send services. - * - * Both live on the coordinator only. The decision is made here rather than in - * a bgw_start_rule because the postmaster consults that field only for its own - * auxiliary process list, not for workers an extension registers. Gp_role is - * already settled at this point: the configuration files (which carry - * gp_contentid) are processed before shared_preload_libraries. - */ -static void -anser_register_services(void) -{ - BackgroundWorker worker; - int i; - - static const struct - { - const char *name; - const char *main_func; - } services[] = - { - {"anser gather service", "AnserGatherServiceMain"}, - {"anser send service", "AnserSendServiceMain"} - }; - - if (!AnserStartRule((Datum) 0)) - return; - - for (i = 0; i < lengthof(services); i++) - { - MemSet(&worker, 0, sizeof(worker)); - worker.bgw_flags = BGWORKER_SHMEM_ACCESS; - worker.bgw_start_time = BgWorkerStart_RecoveryFinished; - worker.bgw_restart_time = 1; - worker.bgw_notify_pid = 0; - snprintf(worker.bgw_name, BGW_MAXLEN, "%s", services[i].name); - snprintf(worker.bgw_type, BGW_MAXLEN, "%s", services[i].name); - snprintf(worker.bgw_library_name, BGW_MAXLEN, "anser"); - snprintf(worker.bgw_function_name, BGW_MAXLEN, "%s", - services[i].main_func); - - RegisterBackgroundWorker(&worker); - } -} - -static void -anser_shmem_request(void) -{ - if (prev_shmem_request_hook) - prev_shmem_request_hook(); - - if (!gp_anser_enable) - return; - - RequestAddinShmemSpace(AnserShmemSize()); - RequestNamedLWLockTranche(ANSER_LWLOCK_TRANCHE, ANSER_NUM_LWLOCKS); -} - -static void -anser_shmem_startup(void) -{ - if (prev_shmem_startup_hook) - prev_shmem_startup_hook(); - - LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); - AnserShmemInit(); - LWLockRelease(AddinShmemInitLock); -} - /* * Plan the query as usual, then hand the finished tree to the runtime-filter * pass. Wrapping the hook this way covers both optimizers, because ORCA is @@ -280,3 +177,39 @@ anser_planner(Query *parse, const char *query_string, int cursorOptions, return result; } + +/* + * Drop the transport's per-query state. + * + * Nested executor runs (a function body, say) must not clear state the outer + * query is still using, so only the outermost end resets. + */ +static void +anser_executor_end(QueryDesc *queryDesc) +{ + static int nesting_level = 0; + + nesting_level++; + PG_TRY(); + { + if (prev_ExecutorEnd_hook) + prev_ExecutorEnd_hook(queryDesc); + else + standard_ExecutorEnd(queryDesc); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); + + if (nesting_level == 0) + AnserSidebandResetAll(); +} + +static void +anser_xact_callback(XactEvent event, void *arg) +{ + if (event == XACT_EVENT_ABORT || event == XACT_EVENT_PARALLEL_ABORT) + AnserSidebandResetAll(); +} diff --git a/gpcontrib/anser/src/anserplan.c b/gpcontrib/anser/src/anserplan.c index 6f3a58d185a..c713970a72e 100644 --- a/gpcontrib/anser/src/anserplan.c +++ b/gpcontrib/anser/src/anserplan.c @@ -38,7 +38,6 @@ #include "anserplan.h" #include "cdb/cdbvars.h" #include "catalog/pg_type.h" -#include "commands/extension.h" #include "nodes/nodeFuncs.h" #include "nodes/pg_list.h" #include "utils/acl.h" @@ -58,13 +57,8 @@ typedef struct AnserInjectCtx List *consumer_keys; /* condition_keys already given a consumer node; * enforces one consumer per channel (see * anser_try_inject) */ - char *token; /* QD session token for the segment -> QD backward - * connections; lazily registered at the first - * injection, NULL when unavailable (fail open to - * pg_hba-driven authentication) */ } AnserInjectCtx; -static bool anser_transport_installed(void); static int anser_max_plan_node_id(Plan *plan); static bool anser_rf_size(double est_rows, int64 *total_elems, int64 *max_payload, int64 *planned_bytes); @@ -92,16 +86,6 @@ AnserApplyRuntimeFilters(PlannedStmt *stmt) if (stmt == NULL || stmt->commandType != CMD_SELECT || stmt->planTree == NULL) return; - /* - * The injected nodes are only useful if the segments can call back into the - * transport functions in *this* database, which means the extension has to - * be installed here. Without it we would dispatch a plan whose producers - * and consumers all fail their libpq calls and fail open one by one; skip - * the injection instead. - */ - if (!anser_transport_installed()) - return; - { AnserInjectCtx ctx; ListCell *lc; @@ -131,38 +115,10 @@ AnserApplyRuntimeFilters(PlannedStmt *stmt) ctx.next_plan_node_id = maxid + 1; ctx.consumer_keys = NIL; - /* - * Segment executors connect back to the QD to publish/consume bloom - * parts; they authenticate with this session's token (the - * parallel-retrieve-cursor model) instead of relying on pg_hba entries - * for the segment hosts. Keyed by the session user because that is - * the identity the QEs connect with. NULL means unavailable -- the - * connection then falls back to ordinary pg_hba authentication. - */ - ctx.token = AnserGetOrCreateSessionToken(GetSessionUserId()); - anser_inject_walk(stmt->planTree, &ctx); } } -/* - * Is "CREATE EXTENSION anser" present in the current database? - * - * A pg_extension lookup, deliberately not a lookup of anser.publish() itself: - * resolving a schema-qualified function name checks USAGE on the schema and - * raises when the planning role lacks it, which would turn a missing privilege - * into a failed query instead of an unfiltered one. - * - * The lookup runs per planned statement -- only for statements that already - * passed the GUC tests -- rather than being cached, so CREATE EXTENSION takes - * effect immediately, without an invalidation callback. - */ -static bool -anser_transport_installed(void) -{ - return OidIsValid(get_extension_oid("anser", true)); -} - /* * Largest plan_node_id in a plan subtree. Recurses the spine plus CustomScan * children; sufficient for the supported (simple) plan shape. @@ -411,14 +367,14 @@ anser_try_inject(HashJoin *hj, AnserInjectCtx *ctx) /* Producer wraps the build base scan; keyed by the mapped build attno. */ producer = AnserBuildBloomProducerScan(build_scan, build_attno, condition_id, condition_key, total_elems, max_payload, - planned_bytes, ctx->token); + planned_bytes); producer->scan.plan.plan_node_id = ctx->next_plan_node_id++; outerPlan(build_parent) = (Plan *) producer; /* Consumer wraps the probe scan; keyed by the outer (probe) attno. */ consumer = AnserBuildBloomConsumerScan(probe, outer_attno, condition_id, condition_key, total_elems, max_payload, - planned_bytes, ctx->token); + planned_bytes); consumer->scan.plan.plan_node_id = ctx->next_plan_node_id++; outerPlan(hj) = (Plan *) consumer; diff --git a/gpcontrib/anser/src/anserplanexec.c b/gpcontrib/anser/src/anserplanexec.c index 69a57706452..79fbc6f1fcf 100644 --- a/gpcontrib/anser/src/anserplanexec.c +++ b/gpcontrib/anser/src/anserplanexec.c @@ -67,7 +67,6 @@ typedef enum AnserRfPrivateIndex ANSER_RF_PRIV_MAX_PAYLOAD, /* Integer: bloom sizing (producer) */ ANSER_RF_PRIV_PLANNED_BYTES, /* Integer: planned bitset bytes (EXPLAIN) */ ANSER_RF_PRIV_CONDITION_KEY, /* String: channel condition_key */ - ANSER_RF_PRIV_TOKEN, /* String: QD session token (may be "") */ ANSER_RF_PRIV__COUNT } AnserRfPrivateIndex; @@ -84,7 +83,6 @@ typedef struct AnserBloomProduceScanState AnserBloomFilterProduceState *produce; AttrNumber key_attno; int64 planned_bytes; - char *token; /* QD session token, NULL when none */ bool published; } AnserBloomProduceScanState; @@ -99,7 +97,6 @@ typedef struct AnserBloomConsumeScanState bloom_filter *filter; /* NULL => fail open (pass everything) */ AttrNumber key_attno; int64 planned_bytes; - char *token; /* QD session token, NULL when none */ bool received; /* have we run the receive/union yet? */ bool pushed_down; /* filter handed to the child scan as an * SK_BLOOM_FILTER scan key (the scan filters, @@ -201,8 +198,7 @@ static CustomScan * anser_build_rf_scan(const CustomScanMethods *methods, Plan *child, AttrNumber key_attno, uint32 condition_id, const char *condition_key, int64 total_elems, - Size max_payload_bytes, int64 planned_bytes, - const char *token) + Size max_payload_bytes, int64 planned_bytes) { CustomScan *cs = makeNode(CustomScan); List *priv = NIL; @@ -214,7 +210,6 @@ anser_build_rf_scan(const CustomScanMethods *methods, Plan *child, priv = lappend(priv, makeInteger((int) max_payload_bytes)); priv = lappend(priv, makeInteger((int) planned_bytes)); priv = lappend(priv, makeString(pstrdup(condition_key))); - priv = lappend(priv, makeString(pstrdup(token != NULL ? token : ""))); cs->scan.plan.targetlist = anser_rf_identity_tlist(child->targetlist); cs->scan.plan.qual = NIL; @@ -242,22 +237,22 @@ CustomScan * AnserBuildBloomProducerScan(Plan *child, AttrNumber key_attno, uint32 condition_id, const char *condition_key, int64 total_elems, Size max_payload_bytes, - int64 planned_bytes, const char *token) + int64 planned_bytes) { return anser_build_rf_scan(&anser_produce_scan_methods, child, key_attno, condition_id, condition_key, total_elems, - max_payload_bytes, planned_bytes, token); + max_payload_bytes, planned_bytes); } CustomScan * AnserBuildBloomConsumerScan(Plan *child, AttrNumber key_attno, uint32 condition_id, const char *condition_key, int64 total_elems, Size max_payload_bytes, - int64 planned_bytes, const char *token) + int64 planned_bytes) { return anser_build_rf_scan(&anser_consume_scan_methods, child, key_attno, condition_id, condition_key, total_elems, - max_payload_bytes, planned_bytes, token); + max_payload_bytes, planned_bytes); } /* ---- shared helpers ---- */ @@ -276,15 +271,6 @@ anser_rf_build_key(CustomScan *cscan, AnserChannelKey *key) strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); } -/* Session token carried in custom_private; NULL when absent/empty. */ -static char * -anser_rf_token(CustomScan *cscan) -{ - char *token = strVal(list_nth(cscan->custom_private, ANSER_RF_PRIV_TOKEN)); - - return token[0] != '\0' ? token : NULL; -} - /* * Number of producing segments for this slice, and this backend's part index. * On a segment the slice runs on the whole gang; on the coordinator the filter @@ -346,15 +332,13 @@ anser_produce_begin(CustomScanState *node, EState *estate, int eflags) st->key_attno = (AttrNumber) intVal(list_nth(priv, ANSER_RF_PRIV_KEY_ATTNO)); st->planned_bytes = intVal(list_nth(priv, ANSER_RF_PRIV_PLANNED_BYTES)); - st->token = anser_rf_token(cscan); st->published = false; anser_rf_build_key(cscan, &key); anser_rf_part_info(&part_index, &total_parts); st->produce = ExecInitAnserBloomFilterProduce(&key, total_elems, max_payload, - part_index, total_parts, - st->token); + part_index, total_parts); node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); } @@ -463,7 +447,6 @@ anser_consume_begin(CustomScanState *node, EState *estate, int eflags) st->key_attno = (AttrNumber) intVal(list_nth(priv, ANSER_RF_PRIV_KEY_ATTNO)); st->planned_bytes = intVal(list_nth(priv, ANSER_RF_PRIV_PLANNED_BYTES)); - st->token = anser_rf_token(cscan); st->filter = NULL; st->received = false; st->pushed_down = false; @@ -472,7 +455,7 @@ anser_consume_begin(CustomScanState *node, EState *estate, int eflags) anser_rf_part_info(&part_index, &expected_parts); st->consume = ExecInitAnserBloomFilterConsume(&key, total_elems, max_payload, - expected_parts, st->token); + expected_parts); node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); } diff --git a/gpcontrib/anser/src/anserservice.c b/gpcontrib/anser/src/anserservice.c deleted file mode 100644 index 980583fa3c4..00000000000 --- a/gpcontrib/anser/src/anserservice.c +++ /dev/null @@ -1,203 +0,0 @@ -/*------------------------------------------------------------------------- - * - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * - * anserservice.c - * Coordinator-local Anser background services. - * - * IDENTIFICATION - * gpcontrib/anser/src/anserservice.c - * - *------------------------------------------------------------------------- - */ -#include "postgres.h" - -#include "anser.h" -#include "cdb/cdbvars.h" -#include "libpq/pqsignal.h" -#include "miscadmin.h" -#include "postmaster/bgworker.h" -#include "storage/fd.h" -#include "storage/ipc.h" -#include "storage/latch.h" -#include "storage/lwlock.h" -#include "utils/guc.h" -#include "utils/hsearch.h" -#include "utils/memutils.h" -#include "utils/ps_status.h" -#include "utils/resowner.h" -#include "utils/wait_event.h" - -static volatile sig_atomic_t anser_service_got_sigterm = false; -static volatile sig_atomic_t anser_service_got_sighup = false; - -static void AnserServiceLoop(const char *service_name, bool gather_service); -static void AnserServiceSigHup(SIGNAL_ARGS); -static void AnserServiceSigTerm(SIGNAL_ARGS); - -bool -AnserStartRule(Datum main_arg) -{ - return gp_anser_enable && Gp_role == GP_ROLE_DISPATCH; -} - -void -AnserGatherServiceMain(Datum main_arg) -{ - AnserServiceLoop("anser gather service", true); -} - -void -AnserSendServiceMain(Datum main_arg) -{ - AnserServiceLoop("anser send service", false); -} - -static void -AnserServiceLoop(const char *service_name, bool gather_service) -{ - sigjmp_buf local_sigjmp_buf; - MemoryContext service_ctx; - - pqsignal(SIGHUP, AnserServiceSigHup); - pqsignal(SIGTERM, AnserServiceSigTerm); - BackgroundWorkerUnblockSignals(); - - init_ps_display(service_name); - ereport(LOG, - (errmsg_internal("%s started", service_name))); - - /* - * Do all per-cycle work in a dedicated context so error recovery can reset - * it, and under a resource owner so a failed cycle's attached/created DSM - * segments are reclaimed rather than leaked. - */ - service_ctx = AllocSetContextCreate(TopMemoryContext, "Anser service", - ALLOCSET_DEFAULT_SIZES); - MemoryContextSwitchTo(service_ctx); - if (CurrentResourceOwner == NULL) - CurrentResourceOwner = ResourceOwnerCreate(NULL, service_name); - - AnserAttachServiceLatch(gather_service); - - /* - * If a cycle raises an error, resume here: log it, drop whatever the cycle - * held, and carry on rather than terminating the worker. Modeled on the - * shmem-only auxiliary processes (see bgwriter.c); the leftmost sigsetjmp - * stays active so we can even survive an error during recovery. - */ - if (sigsetjmp(local_sigjmp_buf, 1) != 0) - { - /* Not using PG_TRY, so reset the error stack by hand. */ - error_context_stack = NULL; - - HOLD_INTERRUPTS(); - - EmitErrorReport(); - - /* Minimal subset of AbortTransaction() for a shmem-only worker. */ - LWLockReleaseAll(); - if (CurrentResourceOwner != NULL) - { - ResourceOwnerRelease(CurrentResourceOwner, - RESOURCE_RELEASE_BEFORE_LOCKS, false, false); - ResourceOwnerRelease(CurrentResourceOwner, - RESOURCE_RELEASE_LOCKS, false, false); - ResourceOwnerRelease(CurrentResourceOwner, - RESOURCE_RELEASE_AFTER_LOCKS, false, false); - } - - /* - * Release any hash_seq_search scan and temp files abandoned when the - * error interrupted a cycle mid-scan. Missing the hash-table reset here - * leaks dynahash scan registrations across errors until hash_seq_init - * itself fails, permanently wedging the service (it could then never - * scan the channel map to deliver to consumers). - */ - AtEOXact_Files(false); - AtEOXact_HashTables(false); - - MemoryContextSwitchTo(service_ctx); - FlushErrorState(); - MemoryContextResetAndDeleteChildren(service_ctx); - - RESUME_INTERRUPTS(); - - /* Do not spin on a persistent error. */ - pg_usleep(1000000L); - } - - /* We can now handle ereport(ERROR). */ - PG_exception_stack = &local_sigjmp_buf; - - while (!anser_service_got_sigterm) - { - if (anser_service_got_sighup) - { - anser_service_got_sighup = false; - ProcessConfigFile(PGC_SIGHUP); - } - - /* - * Run this service's data-path pass, then the shared orphan sweep. - * The gather service drains producer submissions and enforces the - * produce timeout; the send service delivers ready/cancelled channels - * to waiting consumers. The timed wakeup bounds how long a stale - * COLLECTING channel or a dead-backend slot lingers between latches. - */ - if (gather_service) - AnserGatherServiceCycle(); - else - AnserSendServiceCycle(); - - AnserServiceMaintenance(); - AnserWaitServiceLatch(gather_service, ANSER_SERVICE_WAKEUP_INTERVAL_MS); - - /* Reclaim any transient allocations made during this cycle. */ - MemoryContextReset(service_ctx); - } - - PG_exception_stack = NULL; - AnserDetachServiceLatch(gather_service); - - proc_exit(0); -} - -static void -AnserServiceSigHup(SIGNAL_ARGS) -{ - int save_errno = errno; - - anser_service_got_sighup = true; - AnserWakeServiceLatch(true); - AnserWakeServiceLatch(false); - SetLatch(MyLatch); - errno = save_errno; -} - -static void -AnserServiceSigTerm(SIGNAL_ARGS) -{ - int save_errno = errno; - - anser_service_got_sigterm = true; - AnserWakeServiceLatch(true); - AnserWakeServiceLatch(false); - SetLatch(MyLatch); - errno = save_errno; -} diff --git a/gpcontrib/anser/src/ansersideband.c b/gpcontrib/anser/src/ansersideband.c new file mode 100644 index 00000000000..cd4a86027b0 --- /dev/null +++ b/gpcontrib/anser/src/ansersideband.c @@ -0,0 +1,425 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * ansersideband.c + * Segment side of the dispatch-connection transport. + * + * A producer sends its part as a NOTIFY and moves on; a consumer subscribes, + * then blocks on its own dispatch socket until the coordinator pushes the + * merged filter back. Both directions reuse the connection the dispatcher + * already owns, so there is no second connection to open and nothing to + * authenticate. + * + * Reading the frontend socket in the middle of executing a query is the + * pattern cdb_sequence_nextval_qe() established (commands/sequence.c); the + * loop below mirrors its use of pq_startmsgread/pq_getbyte_if_available, but + * sleeps on the socket instead of spinning. + * + * IDENTIFICATION + * gpcontrib/anser/src/ansersideband.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "ansersideband.h" +#include "cdb/cdbvars.h" +#include "commands/async.h" +#include "common/base64.h" +#include "libpq/libpq-be.h" +#include "libpq/libpq.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/latch.h" +#include "tcop/tcopprot.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" +#include "utils/wait_event.h" + +/* How long to sleep between wakeups while waiting for a delivery. */ +#define ANSER_SIDEBAND_POLL_MS 100L + +/* + * A delivery that arrived while we were waiting for a different channel. + * + * One slice can host consumers for more than one condition, and the + * coordinator pushes each channel as soon as it completes, so messages can + * arrive in an order we did not ask for. Rather than discard them (which + * would cost that consumer its filter), park them here and check the inbox + * before touching the socket. + */ +typedef struct AnserInboxEntry +{ + AnserChannelKey key; + char *payload; /* NULL when cancelled */ + Size payload_len; + bool cancelled; +} AnserInboxEntry; + +static List *AnserInbox = NIL; + +static bool anser_sideband_send(const char *payload); +static char *anser_sideband_format(const AnserChannelKey *channel_key, char kind, + uint32 part_index, uint32 total_parts, + int flags, const void *payload, + Size payload_len); +static bool anser_inbox_take(const AnserChannelKey *key, void **payload, + Size *payload_len, bool *cancelled); +static bool anser_sideband_read_one(long timeout_ms); + +/* + * Publish one part. Fire-and-forget: the coordinator does not acknowledge, + * because nothing on this side needs to wait for it. + */ +bool +AnserSidebandPublish(const AnserChannelKey *channel_key, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, bool cancelled) +{ + char *msg; + int flags = cancelled ? ANSER_WIRE_F_CANCELLED : 0; + bool ok; + + if (channel_key == NULL) + return false; + + if (!cancelled && payload_len > (Size) gp_anser_max_info_size) + { + /* Too large to ship; tell the coordinator so consumers stop waiting. */ + flags = ANSER_WIRE_F_CANCELLED; + payload = NULL; + payload_len = 0; + } + + msg = anser_sideband_format(channel_key, ANSER_WIRE_KIND_PART, + part_index, total_parts, flags, + (flags & ANSER_WIRE_F_CANCELLED) ? NULL : payload, + (flags & ANSER_WIRE_F_CANCELLED) ? 0 : payload_len); + ok = anser_sideband_send(msg); + pfree(msg); + + return ok; +} + +/* + * Subscribe, then wait for the merged payload. + * + * Returns true with *payload set, or false for "run unfiltered" -- including + * on timeout. The deadline exists because a producer that gets squelched + * never publishes anything: ExecSquelchNode does not call CustomScan + * callbacks, it only marks the node (execAmi.c), so without a deadline this + * wait could outlive the reason for it. + */ +bool +AnserSidebandConsumeWait(const AnserChannelKey *channel_key, + void **payload, Size *payload_len, + bool *cancelled, long timeout_ms) +{ + char *msg; + TimestampTz start; + + if (payload != NULL) + *payload = NULL; + if (payload_len != NULL) + *payload_len = 0; + if (cancelled != NULL) + *cancelled = false; + + if (channel_key == NULL || MyProcPort == NULL || + MyProcPort->sock == PGINVALID_SOCKET) + return false; + + /* It may already be here: the coordinator pushes as soon as it can. */ + if (anser_inbox_take(channel_key, payload, payload_len, cancelled)) + return payload != NULL && *payload != NULL; + + msg = anser_sideband_format(channel_key, ANSER_WIRE_KIND_SUBSCRIBE, + 0, 0, 0, NULL, 0); + if (!anser_sideband_send(msg)) + { + pfree(msg); + return false; + } + pfree(msg); + + start = GetCurrentTimestamp(); + for (;;) + { + CHECK_FOR_INTERRUPTS(); + + if (anser_inbox_take(channel_key, payload, payload_len, cancelled)) + return payload != NULL && *payload != NULL; + + if (timeout_ms >= 0 && + TimestampDifferenceExceeds(start, GetCurrentTimestamp(), timeout_ms)) + return false; + + /* + * Any message we read lands in the inbox; the loop then rechecks + * whether it was the one we wanted. A read failure means the + * connection is gone, which the interconnect will report far more + * usefully than we can -- stop waiting and let the query run + * unfiltered. + */ + if (!anser_sideband_read_one(ANSER_SIDEBAND_POLL_MS)) + return false; + } +} + +/* + * Wait briefly for one sideband message and stash it in the inbox. + * + * Returns false only when the connection is unusable; a timeout with nothing + * to read is a normal true. + */ +static bool +anser_sideband_read_one(long timeout_ms) +{ + unsigned char qtype; + int retval; + StringInfoData buf; + AnserInboxEntry *entry; + MemoryContext oldcxt; + int condid; + int flags; + int keylen; + int paylen; + const char *keyptr; + const char *payptr; + + pq_startmsgread(); + retval = pq_getbyte_if_available(&qtype); + if (retval == 0) + { + /* Nothing buffered: sleep on the socket rather than spinning. */ + pq_endmsgread(); + + ResetLatch(MyLatch); + (void) WaitLatchOrSocket(MyLatch, + WL_LATCH_SET | WL_SOCKET_READABLE | + WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + MyProcPort->sock, + timeout_ms, + PG_WAIT_EXTENSION); + return true; + } + + if (retval == EOF) + { + elog(LOG, "anser: dispatch connection closed while awaiting a filter"); + return false; + } + + if (qtype != GP_SIDEBAND_MESSAGE) + { + /* + * Nothing else should reach us here. Do not try to interpret or skip + * it: message-boundary sync is at stake, so leave it for the command + * loop and give up on the filter. + */ + pq_endmsgread(); + elog(LOG, "anser: unexpected message type '%c' while awaiting a filter", + (char) qtype); + return false; + } + + initStringInfo(&buf); + if (pq_getmessage(&buf, gp_anser_max_info_size + ANSER_CONDITION_KEY_SIZE + 64) != 0) + { + /* + * pq_getmessage clears the reading-message flag when it succeeds, but + * not on its EOF paths; clear it by hand so we do not trip the + * assertion in a later pq_startmsgread. + */ + pq_endmsgread(); + pfree(buf.data); + elog(LOG, "anser: could not read filter message"); + return false; + } + + condid = pq_getmsgint(&buf, 4); + flags = pq_getmsgint(&buf, 4); + keylen = pq_getmsgint(&buf, 4); + if (keylen < 0 || keylen >= ANSER_CONDITION_KEY_SIZE) + { + pfree(buf.data); + elog(LOG, "anser: filter message has a bad condition key"); + return false; + } + keyptr = pq_getmsgbytes(&buf, keylen); + paylen = pq_getmsgint(&buf, 4); + if (paylen < 0 || paylen > gp_anser_max_info_size) + { + pfree(buf.data); + elog(LOG, "anser: filter message has a bad length"); + return false; + } + payptr = paylen > 0 ? pq_getmsgbytes(&buf, paylen) : NULL; + + /* + * The inbox outlives this call and the memory context it was reached in, + * so anchor it somewhere stable; AnserSidebandResetAll drops it. + */ + oldcxt = MemoryContextSwitchTo(TopMemoryContext); + entry = palloc0(sizeof(AnserInboxEntry)); + entry->key.gp_session_id = gp_session_id; + entry->key.gp_command_count = gp_command_count; + entry->key.condition_id = (uint32) condid; + memcpy(entry->key.condition_key, keyptr, keylen); + entry->key.condition_key[keylen] = '\0'; + entry->cancelled = (flags & ANSER_WIRE_F_CANCELLED) != 0; + if (!entry->cancelled && paylen > 0) + { + entry->payload = palloc(paylen); + memcpy(entry->payload, payptr, paylen); + entry->payload_len = paylen; + } + AnserInbox = lappend(AnserInbox, entry); + MemoryContextSwitchTo(oldcxt); + + pfree(buf.data); + return true; +} + +/* Claim a delivery for this channel, if one has arrived. */ +static bool +anser_inbox_take(const AnserChannelKey *key, void **payload, + Size *payload_len, bool *cancelled) +{ + ListCell *lc; + + foreach(lc, AnserInbox) + { + AnserInboxEntry *entry = (AnserInboxEntry *) lfirst(lc); + + if (entry->key.condition_id != key->condition_id || + strncmp(entry->key.condition_key, key->condition_key, + ANSER_CONDITION_KEY_SIZE) != 0) + continue; + + if (cancelled != NULL) + *cancelled = entry->cancelled; + if (!entry->cancelled && entry->payload != NULL) + { + if (payload != NULL) + { + *payload = palloc(entry->payload_len); + memcpy(*payload, entry->payload, entry->payload_len); + } + if (payload_len != NULL) + *payload_len = entry->payload_len; + } + + AnserInbox = foreach_delete_current(AnserInbox, lc); + if (entry->payload != NULL) + pfree(entry->payload); + pfree(entry); + return true; + } + + return false; +} + +/* Drop any deliveries nobody claimed. */ +void +AnserSidebandResetInbox(void) +{ + ListCell *lc; + + foreach(lc, AnserInbox) + { + AnserInboxEntry *entry = (AnserInboxEntry *) lfirst(lc); + + if (entry->payload != NULL) + pfree(entry->payload); + pfree(entry); + } + list_free(AnserInbox); + AnserInbox = NIL; +} + +void +AnserSidebandResetAll(void) +{ + AnserSidebandResetInbox(); + AnserDispatchReset(); +} + +/* Build a QE -> QD payload; see anser_disp_parse() for the layout. */ +static char * +anser_sideband_format(const AnserChannelKey *channel_key, char kind, + uint32 part_index, uint32 total_parts, int flags, + const void *payload, Size payload_len) +{ + StringInfoData buf; + int keylen = (int) strlen(channel_key->condition_key); + int bodylen = 0; + char *body = NULL; + + if (payload != NULL && payload_len > 0) + { + int maxlen = pg_b64_enc_len((int) payload_len); + + body = palloc(maxlen + 1); + bodylen = pg_b64_encode((const char *) payload, (int) payload_len, + body, maxlen); + if (bodylen < 0) + { + pfree(body); + body = NULL; + bodylen = 0; + flags |= ANSER_WIRE_F_CANCELLED; + } + } + + initStringInfo(&buf); + appendStringInfo(&buf, ANSER_WIRE_TAG " %c %d %d %u %u %u %d %d %d\n", + kind, channel_key->gp_session_id, + channel_key->gp_command_count, channel_key->condition_id, + part_index, total_parts, flags, keylen, bodylen); + appendBinaryStringInfo(&buf, channel_key->condition_key, keylen); + if (bodylen > 0) + appendBinaryStringInfo(&buf, body, bodylen); + + if (body != NULL) + pfree(body); + + return buf.data; +} + +/* + * Hand a payload to the coordinator. + * + * NotifyMyFrontEnd enforces no length limit of its own -- the ~8 KB cap + * applies to the SQL-level NOTIFY, which has to fit its queue page -- so a + * multi-megabyte part is fine here. The payload is base64, hence free of the + * NUL that would truncate it in pq_sendstring(). + */ +static bool +anser_sideband_send(const char *payload) +{ + if (whereToSendOutput != DestRemote) + return false; + + NotifyMyFrontEnd(ANSER_NOTIFY_CHANNEL, payload, gp_session_id); + pq_flush(); + return true; +} diff --git a/src/backend/cdb/dispatcher/cdbdisp_async.c b/src/backend/cdb/dispatcher/cdbdisp_async.c index eb8e4714396..b8250f9c9fd 100644 --- a/src/backend/cdb/dispatcher/cdbdisp_async.c +++ b/src/backend/cdb/dispatcher/cdbdisp_async.c @@ -116,6 +116,12 @@ static void cdbdisp_waitDispatchFinish_async(struct CdbDispatcherState *ds); static bool cdbdisp_checkForCancel_async(struct CdbDispatcherState *ds); static int *cdbdisp_getWaitSocketFds_async(struct CdbDispatcherState *ds, int *nsocks); +/* + * Lets an extension handle its own NOTIFY channels arriving from QEs; see the + * declaration in cdbdisp.h for the contract. + */ +cdbdisp_notify_hook_type cdbdisp_notify_hook = NULL; + DispatcherInternalFuncs DispatcherAsyncFuncs = { cdbdisp_checkForCancel_async, @@ -1208,6 +1214,11 @@ processResults(CdbDispatchResult *dispatchResult) /* Don't free the notify here since it in queue now */ qnotifies = NULL; } + else if (cdbdisp_notify_hook != NULL && + cdbdisp_notify_hook(dispatchResult, qnotifies)) + { + /* Consumed by an extension; nothing further to do here. */ + } else { /* Got an unknown PGnotify, just record it in log */ diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 9fcb196a33d..b6021e169dd 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -256,13 +256,6 @@ static int PerformRadiusTransaction(const char *server, const char *secret, cons */ ClientAuthentication_hook_type ClientAuthentication_hook = NULL; -/* - * These hooks let an extension authenticate its own internal connections - * before pg_hba.conf is consulted; see custom_conn_authentication() below. - */ -CustomAuthClaims_hook_type CustomAuthClaims_hook = NULL; -CustomAuthCheckPassword_hook_type CustomAuthCheckPassword_hook = NULL; - /* * Tell the user the authentication failed, but not (much about) why. * @@ -566,33 +559,6 @@ retrieve_conn_authentication(Port *port) FakeClientAuthentication(port); } -/* - * A connection claimed by an extension via CustomAuthClaims_hook uses the - * password it sends as an extension-defined credential, bypassing pg_hba -- - * the same model as retrieve_conn_authentication() above. Only the check is - * delegated: the extension decides whether the credential entitles the client - * to connect as port->user_name, and on success the connection becomes an - * ordinary backend for that user. - */ -static void -custom_conn_authentication(Port *port) -{ - char *passwd; - const char *msg1 = "Failed to retrieve the authentication password"; - const char *msg2 = "Authentication failure (invalid credential)"; - - sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); - passwd = recv_password_packet(port); - if (passwd == NULL) - ereport(FATAL, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("%s", msg1))); - - if (CustomAuthCheckPassword_hook == NULL || - !CustomAuthCheckPassword_hook(port, passwd)) - ereport(FATAL, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("%s", msg2))); - - FakeClientAuthentication(port); -} - /* * Special client authentication for QD to QE connections. This is run at the * QE. This is non-trivial because a QE some times runs at the master (i.e., an @@ -752,17 +718,6 @@ ClientAuthentication(Port *port) return; } - /* - * An extension may own the authentication of its own internal connections - * (identified by a marker option in the startup packet), likewise before - * pg_hba is consulted. - */ - if (CustomAuthClaims_hook != NULL && CustomAuthClaims_hook(port)) - { - custom_conn_authentication(port); - return; - } - /* * If this is a QD to QE connection, we might be able to short circuit * client authentication. diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8eba8a6d227..213b5daa398 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -595,6 +595,11 @@ SocketBackend(StringInfo inBuf) doing_extended_query_message = false; break; + case GP_SIDEBAND_MESSAGE: /* Cloudberry QD -> QE sideband data */ + maxmsglen = PQ_LARGE_MESSAGE_LIMIT; + doing_extended_query_message = false; + break; + default: /* @@ -6417,6 +6422,18 @@ PostgresMain(const char *dbname, const char *username) */ break; + case GP_SIDEBAND_MESSAGE: /* Cloudberry QD -> QE sideband data */ + /* + * Accept but ignore this message. A QE reads sideband data + * explicitly at the point it expects it; reaching the command + * loop means nobody is waiting for it any more -- typically + * the waiter was interrupted (query cancel) after the QD had + * already written the message. Same reasoning as '?' above, + * but a wider window, since a sideband waiter may block for + * the lifetime of a query. + */ + break; + default: ereport(FATAL, (errcode(ERRCODE_PROTOCOL_VIOLATION), diff --git a/src/include/cdb/cdbdisp.h b/src/include/cdb/cdbdisp.h index 9fac725c5fd..3c7179fffe6 100644 --- a/src/include/cdb/cdbdisp.h +++ b/src/include/cdb/cdbdisp.h @@ -22,11 +22,34 @@ #define CDB_MOTION_LOST_CONTACT_STRING "Interconnect error master lost contact with segment." struct CdbDispatchResults; /* #include "cdb/cdbdispatchresult.h" */ +struct CdbDispatchResult; /* #include "cdb/cdbdispatchresult.h" */ struct CdbPgResults; struct Gang; /* #include "cdb/cdbgang.h" */ struct ResourceOwnerData; +struct pgNotify; /* #include "libpq-fe.h" */ enum GangType; +/* + * Hook for an extension to consume a NOTIFY sent by a QE on its dispatch + * connection, alongside the sequence and endpoint-ack channels the dispatcher + * handles itself. Return true if the notify was consumed, false to let the + * dispatcher fall through to its own handling. + * + * 'dispatchResult' identifies the sending QE and, through its meleeResults, + * gives access to every connection of the current dispatch -- which is what + * lets a handler answer on other QEs' connections (see GP_SIDEBAND_MESSAGE). + * + * The hook runs inside the QD's dispatch-result processing, which is reached + * from the interconnect wait loop while the query is executing. It must + * therefore be quick, must not re-enter the dispatcher, and should not throw + * for conditions it can recover from: an error here propagates into the + * running query. The PGnotify belongs to the caller and is freed after the + * hook returns, so a handler must copy anything it wants to keep. + */ +typedef bool (*cdbdisp_notify_hook_type) (struct CdbDispatchResult *dispatchResult, + struct pgNotify *notify); +extern PGDLLIMPORT cdbdisp_notify_hook_type cdbdisp_notify_hook; + /* * Types of message to QE when we wait for it. */ diff --git a/src/include/cdb/cdbvars.h b/src/include/cdb/cdbvars.h index b06f3480938..7ac077b0213 100644 --- a/src/include/cdb/cdbvars.h +++ b/src/include/cdb/cdbvars.h @@ -805,6 +805,19 @@ extern const char * lookup_autostats_mode_by_value(GpAutoStatsModeValue val); */ #define CDB_NOTIFY_ENDPOINT_ACK "ack_notify" +/* + * Message type for QD -> QE sideband data sent on the dispatch connection + * outside the normal query protocol, in the same spirit as the '?' sequence + * response. The QE reads it explicitly where it expects it; a message that + * arrives unexpectedly (typically because the QE was cancelled while waiting + * for one) is accepted and discarded by the command loop, so it cannot break + * message-boundary sync. + * + * Unlike '?', a sideband message may be large: it is read with + * PQ_LARGE_MESSAGE_LIMIT. + */ +#define GP_SIDEBAND_MESSAGE '!' + typedef enum WarehouseStatus { WAREHOUSE_STATUS_CREATING, diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index fd1c5c1df89..50f80f48970 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -30,22 +30,6 @@ extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, typedef void (*ClientAuthentication_hook_type) (Port *, int); extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook; -/* - * Hooks for an extension that maintains its own internal connections, such as - * a segment -> coordinator connection carrying a per-session token. The claims - * hook is consulted before pg_hba.conf and answers whether this connection - * belongs to the extension, normally by looking for a marker option in - * port->cmdline_options / port->guc_options. When it claims the connection, - * the backend asks the client for a password and hands it to the check hook, - * which returns true if the connection may proceed as port->user_name. The - * wire exchange stays in auth.c; the extension only supplies the two answers. - */ -typedef bool (*CustomAuthClaims_hook_type) (Port *port); -extern PGDLLIMPORT CustomAuthClaims_hook_type CustomAuthClaims_hook; -typedef bool (*CustomAuthCheckPassword_hook_type) (Port *port, - const char *passwd); -extern PGDLLIMPORT CustomAuthCheckPassword_hook_type CustomAuthCheckPassword_hook; - /* * Support for time-based authentication * From cda95a50b0d4c22fd48ec8a97e03820e0a9f6549 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 7 Sep 2026 12:11:45 +0300 Subject: [PATCH 06/15] Fix compile issues after code rewrite --- gpcontrib/anser/src/anser_test.c | 10 +++++----- gpcontrib/anser/src/anserdispatch.c | 1 + gpcontrib/anser/src/anserinit.c | 8 ++++---- gpcontrib/anser/src/ansersideband.c | 1 + 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/gpcontrib/anser/src/anser_test.c b/gpcontrib/anser/src/anser_test.c index 5fa36eeb75a..552fc684b97 100644 --- a/gpcontrib/anser/src/anser_test.c +++ b/gpcontrib/anser/src/anser_test.c @@ -288,12 +288,12 @@ anser_test_node_roundtrip(PG_FUNCTION_ARGS) key.gp_session_id = gp_session_id; key.gp_command_count = gp_command_count; key.condition_id = 77; - strlcpy(key.condition_key, "sideband_roundtrip", ANSER_CONDITION_KEY_SIZE); + strlcpy(key.condition_key, "node_roundtrip", ANSER_CONDITION_KEY_SIZE); PG_TRY(); { - producer = ExecInitAnserBloomFilterProduce(&key, 32, 1024 * 1024, 0, 1, - NULL); + producer = ExecInitAnserBloomFilterProduce(&key, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, 0, 1); if (producer == NULL) ok = false; else @@ -305,8 +305,8 @@ anser_test_node_roundtrip(PG_FUNCTION_ARGS) if (ok) { - consumer = ExecInitAnserBloomFilterConsume(&key, 32, 1024 * 1024, 1, - NULL); + consumer = ExecInitAnserBloomFilterConsume(&key, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, 1); if (consumer == NULL) ok = false; else diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c index 90f9e7cbe3f..c8e01bf6d93 100644 --- a/gpcontrib/anser/src/anserdispatch.c +++ b/gpcontrib/anser/src/anserdispatch.c @@ -46,6 +46,7 @@ #include "anser.h" #include "anserfilter.h" #include "ansersideband.h" +#include "cdb/cdbconn.h" #include "cdb/cdbdisp.h" #include "cdb/cdbdispatchresult.h" #include "cdb/cdbvars.h" diff --git a/gpcontrib/anser/src/anserinit.c b/gpcontrib/anser/src/anserinit.c index 5779ddf240e..a21020fe4ee 100644 --- a/gpcontrib/anser/src/anserinit.c +++ b/gpcontrib/anser/src/anserinit.c @@ -136,16 +136,16 @@ anser_define_gucs(void) DefineCustomIntVariable("anser.max_info_size", "Sets the maximum byte size of one Anser information record.", - "Per-record DSM payload cap for Anser information. The default holds a full 64 MB bloom-filter bitset plus its serialized-part header.", + "Caps the serialized payload a channel may carry, and with it the effective bloom-filter size. The default holds a full 64 MB bitset plus its serialized-part header.", &gp_anser_max_info_size, 64 * 1024 * 1024 + 1024 * 1024, 1, INT_MAX, - PGC_POSTMASTER, + PGC_USERSET, 0, NULL, NULL, NULL); DefineCustomIntVariable("anser.timeout_ms", - "Sets how long Anser consumers wait for producer registration.", - "After producer registration, consumers wait for data without this timeout and rely on query cancellation or channel cancellation.", + "Sets how long an Anser consumer waits for its filter.", + "On expiry the consumer runs unfiltered. The deadline matters because a producer that gets squelched never publishes at all.", &gp_anser_timeout_ms, 1000, 0, INT_MAX, PGC_USERSET, diff --git a/gpcontrib/anser/src/ansersideband.c b/gpcontrib/anser/src/ansersideband.c index cd4a86027b0..577b272f2ef 100644 --- a/gpcontrib/anser/src/ansersideband.c +++ b/gpcontrib/anser/src/ansersideband.c @@ -49,6 +49,7 @@ #include "miscadmin.h" #include "nodes/pg_list.h" #include "storage/latch.h" +#include "tcop/dest.h" #include "tcop/tcopprot.h" #include "utils/memutils.h" #include "utils/timestamp.h" From 8acfa5b6835cf0ff659fc35c6a6d325b6b7eff44 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 7 Sep 2026 12:23:03 +0300 Subject: [PATCH 07/15] Do not use frontend PQ api We just use frontend API for check status, but compile with using frontend API is not safe. Switch to using our own functions. --- gpcontrib/anser/Makefile | 5 ++++- gpcontrib/anser/src/anserdispatch.c | 35 +++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/gpcontrib/anser/Makefile b/gpcontrib/anser/Makefile index 9f9250870a7..3560eb65e8f 100644 --- a/gpcontrib/anser/Makefile +++ b/gpcontrib/anser/Makefile @@ -50,7 +50,10 @@ REGRESS = anser_test anser_runtime_filter # src/anserdispatch.c writes sideband messages onto the dispatcher's own libpq # connections, so it needs libpq's internal headers (as the dispatcher itself -# does); the symbols resolve against the backend. +# does). Deliberately NOT linked against libpq: the pq* writers we use stay +# global in the postgres binary, while the public PQ* API is made local there +# on purpose (src/backend/Makefile), so linking libpq.so would put a second +# copy of libpq on connections the backend's copy created. PG_CPPFLAGS = -I$(srcdir)/include -I$(libpq_srcdir) SHLIB_PREREQS = submake-libpq diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c index c8e01bf6d93..50cb788a398 100644 --- a/gpcontrib/anser/src/anserdispatch.c +++ b/gpcontrib/anser/src/anserdispatch.c @@ -33,6 +33,17 @@ * parts are folded in place as they arrive, so only the final fold is on the * critical path. * + * A note on libpq linkage. The connections we write to were created by the + * copy of libpq that is statically linked into the postgres binary, and every + * symbol listed in libpq's exports.txt is deliberately made *local* in that + * binary (see the version-script hack in src/backend/Makefile) precisely so a + * module cannot end up driving one connection through two copies of libpq. + * So this file must not call the public PQ* API: linking libpq.so to obtain it + * would create exactly the mixture that hack exists to prevent. The internal + * pq* writers are not in exports.txt and so remain global in the backend, + * which is how pqPutMsgStart() and friends resolve here; the two accessors we + * would otherwise want are inlined below, straight off the struct. + * * IDENTIFICATION * gpcontrib/anser/src/anserdispatch.c * @@ -94,6 +105,26 @@ static void anser_disp_apply_part(AnserDispChannel *chan, const void *payload, static void anser_disp_deliver(AnserDispChannel *chan); static bool anser_disp_push(PGconn *conn, AnserDispChannel *chan); +/* + * PQstatus() / PQerrorMessage() equivalents. See the linkage note above for + * why these are not the real thing; libpq-int.h gives us the full struct. + */ +static inline bool +anser_conn_ok(const PGconn *conn) +{ + return conn != NULL && conn->status == CONNECTION_OK; +} + +static inline const char * +anser_conn_error(const PGconn *conn) +{ + if (conn == NULL) + return "connection pointer is NULL"; + if (PQExpBufferBroken(&conn->errorMessage)) + return "out of memory"; + return conn->errorMessage.data; +} + /* * Per-query state lives in its own context so it can be dropped wholesale. * Channels are keyed by (session, command, condition), so entries from an @@ -339,7 +370,7 @@ anser_disp_push(PGconn *conn, AnserDispChannel *chan) int keylen = (int) strlen(chan->key.condition_key); int paylen = chan->cancelled ? 0 : (int) chan->payload_len; - if (conn == NULL || PQstatus(conn) != CONNECTION_OK) + if (!anser_conn_ok(conn)) return false; /* @@ -357,7 +388,7 @@ anser_disp_push(PGconn *conn, AnserDispChannel *chan) pqFlush(conn) < 0) { elog(LOG, "anser: could not deliver filter for condition %u: %s", - chan->key.condition_id, PQerrorMessage(conn)); + chan->key.condition_id, anser_conn_error(conn)); return false; } From 997ac760d8cd965ccf6ea37450b94be52cf75b89 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 7 Sep 2026 12:48:50 +0300 Subject: [PATCH 08/15] Add debug option to anser --- gpcontrib/anser/include/anser.h | 16 ++++++++++++++++ gpcontrib/anser/src/anserdispatch.c | 15 ++++++++++++++- gpcontrib/anser/src/anserinit.c | 10 ++++++++++ gpcontrib/anser/src/ansersideband.c | 14 ++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/gpcontrib/anser/include/anser.h b/gpcontrib/anser/include/anser.h index 63c13356163..5bfa5d772e8 100644 --- a/gpcontrib/anser/include/anser.h +++ b/gpcontrib/anser/include/anser.h @@ -62,7 +62,23 @@ typedef struct AnserChannelKey /* GUCs (defined in anserinit.c). */ extern bool gp_anser_enable; extern bool gp_anser_runtime_filter; +extern bool gp_anser_debug; extern int gp_anser_max_info_size; extern int gp_anser_timeout_ms; +/* + * Trace the handoff between producers, the coordinator and consumers. + * + * Every step of the exchange is invisible in EXPLAIN until it is over -- a + * consumer that waits and gets nothing looks exactly like one whose producers + * never published -- so the path is traceable on demand rather than only + * reconstructable from a debugger. LOG level, so it lands in the log of + * whichever process it happened in. + */ +#define ANSER_DEBUG(...) \ + do { \ + if (gp_anser_debug) \ + elog(LOG, __VA_ARGS__); \ + } while (0) + #endif /* ANSER_H */ diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c index 50cb788a398..738aca74520 100644 --- a/gpcontrib/anser/src/anserdispatch.c +++ b/gpcontrib/anser/src/anserdispatch.c @@ -216,7 +216,8 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, if (n->extra == NULL || !anser_disp_parse(n->extra, &msg)) { - elog(LOG, "anser: ignoring malformed message from a segment"); + elog(LOG, "anser: ignoring malformed message from a segment (len=%zu)", + n->extra != NULL ? strlen(n->extra) : (size_t) 0); return true; } @@ -239,6 +240,9 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, * producers on other segments may well have finished first -- so * deliver immediately in that case rather than recording interest. */ + ANSER_DEBUG("anser: QD subscribe cond=%u (channel %s)", + msg.key.condition_id, + chan->complete ? "complete, delivering now" : "still collecting"); if (chan->complete) (void) anser_disp_push(conn, chan); else @@ -267,6 +271,11 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, anser_disp_apply_part(chan, raw, (Size) raw_len, msg.total_parts, (msg.flags & ANSER_WIRE_F_CANCELLED) != 0); + ANSER_DEBUG("anser: QD part cond=%u %d/%d bytes=%d -> %s", + msg.key.condition_id, chan->parts_received, + chan->expected_parts, raw_len, + chan->cancelled ? "cancelled" : + chan->complete ? "complete" : "collecting"); if (raw != NULL) pfree(raw); } @@ -348,6 +357,8 @@ anser_disp_deliver(AnserDispChannel *chan) { ListCell *lc; + ANSER_DEBUG("anser: QD delivering cond=%u to %d subscriber(s)", + chan->key.condition_id, list_length(chan->subscribers)); foreach(lc, chan->subscribers) (void) anser_disp_push((PGconn *) lfirst(lc), chan); @@ -392,6 +403,8 @@ anser_disp_push(PGconn *conn, AnserDispChannel *chan) return false; } + ANSER_DEBUG("anser: QD pushed cond=%u bytes=%d cancelled=%d", + chan->key.condition_id, paylen, chan->cancelled ? 1 : 0); return true; } diff --git a/gpcontrib/anser/src/anserinit.c b/gpcontrib/anser/src/anserinit.c index a21020fe4ee..ff64d1fc44c 100644 --- a/gpcontrib/anser/src/anserinit.c +++ b/gpcontrib/anser/src/anserinit.c @@ -55,6 +55,7 @@ void _PG_init(void); bool gp_anser_enable = false; bool gp_anser_runtime_filter = false; +bool gp_anser_debug = false; int gp_anser_max_info_size = 64 * 1024 * 1024 + 1024 * 1024; int gp_anser_timeout_ms = 1000; @@ -134,6 +135,15 @@ anser_define_gucs(void) GUC_EXPLAIN, NULL, NULL, NULL); + DefineCustomBoolVariable("anser.debug", + "Logs each step of the Anser filter exchange.", + "Traces publish, merge, delivery and receive in the log of the process each happens in.", + &gp_anser_debug, + false, + PGC_USERSET, + 0, + NULL, NULL, NULL); + DefineCustomIntVariable("anser.max_info_size", "Sets the maximum byte size of one Anser information record.", "Caps the serialized payload a channel may carry, and with it the effective bloom-filter size. The default holds a full 64 MB bitset plus its serialized-part header.", diff --git a/gpcontrib/anser/src/ansersideband.c b/gpcontrib/anser/src/ansersideband.c index 577b272f2ef..8532d1dde21 100644 --- a/gpcontrib/anser/src/ansersideband.c +++ b/gpcontrib/anser/src/ansersideband.c @@ -115,6 +115,10 @@ AnserSidebandPublish(const AnserChannelKey *channel_key, (flags & ANSER_WIRE_F_CANCELLED) ? NULL : payload, (flags & ANSER_WIRE_F_CANCELLED) ? 0 : payload_len); ok = anser_sideband_send(msg); + ANSER_DEBUG("anser: seg%d published cond=%u part=%u/%u bytes=%zu cancelled=%d sent=%d", + GpIdentity.segindex, channel_key->condition_id, part_index, + total_parts, payload_len, + (flags & ANSER_WIRE_F_CANCELLED) ? 1 : 0, ok ? 1 : 0); pfree(msg); return ok; @@ -160,6 +164,8 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, return false; } pfree(msg); + ANSER_DEBUG("anser: seg%d subscribed cond=%u, waiting up to %ld ms", + GpIdentity.segindex, channel_key->condition_id, timeout_ms); start = GetCurrentTimestamp(); for (;;) @@ -171,7 +177,12 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, if (timeout_ms >= 0 && TimestampDifferenceExceeds(start, GetCurrentTimestamp(), timeout_ms)) + { + ANSER_DEBUG("anser: seg%d gave up on cond=%u after %ld ms", + GpIdentity.segindex, channel_key->condition_id, + timeout_ms); return false; + } /* * Any message we read lands in the inbox; the loop then rechecks @@ -295,6 +306,9 @@ anser_sideband_read_one(long timeout_ms) } AnserInbox = lappend(AnserInbox, entry); MemoryContextSwitchTo(oldcxt); + ANSER_DEBUG("anser: seg%d received cond=%u bytes=%zu cancelled=%d", + GpIdentity.segindex, (uint32) condid, entry->payload_len, + entry->cancelled ? 1 : 0); pfree(buf.data); return true; From b0870ef71b4a88a5a2fc2fd16c78b03078b446c9 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 7 Sep 2026 13:02:55 +0300 Subject: [PATCH 09/15] Fix bug that channel canceled stright away, add diagnostic to producer --- gpcontrib/anser/src/anserbloomproduce.c | 22 ++++++++++++++---- gpcontrib/anser/src/anserplanexec.c | 31 ++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/gpcontrib/anser/src/anserbloomproduce.c b/gpcontrib/anser/src/anserbloomproduce.c index 0ee3b3856f9..88e98a23c05 100644 --- a/gpcontrib/anser/src/anserbloomproduce.c +++ b/gpcontrib/anser/src/anserbloomproduce.c @@ -112,10 +112,16 @@ ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state) bool ok; if (state == NULL || state->published) + { + ANSER_DEBUG("anser: publish skipped (%s)", + state == NULL ? "no producer state" : "already published"); return false; + } - if (state->cancelled) + if (state->cancelled || state->filter == NULL) { + ANSER_DEBUG("anser: publishing a cancel (%s)", + state->cancelled ? "producer cancelled" : "no filter"); state->published = true; return AnserProducePublishPart(state, NULL, 0, true); } @@ -137,6 +143,9 @@ ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state) &payload_len); if (ok) ok = AnserProducePublishPart(state, payload, payload_len, false); + else + ANSER_DEBUG("anser: publish skipped: could not serialize part (size=%zu)", + payload_size); pfree(payload); state->published = true; @@ -154,15 +163,20 @@ ExecAnserBloomFilterProduceCancel(AnserBloomFilterProduceState *state) return AnserProducePublishPart(state, NULL, 0, true); } +/* + * Free the producer state. + * + * Deliberately does NOT publish a cancel for an unpublished producer: whether + * silence means "ran and was abandoned" or "never ran at all" is knowable only + * to the node, and the two need opposite handling (see anser_produce_end in + * anserplanexec.c). + */ void ExecEndAnserBloomFilterProduce(AnserBloomFilterProduceState *state) { if (state == NULL) return; - if (!state->published) - (void) ExecAnserBloomFilterProduceCancel(state); - if (state->filter != NULL) bloom_free(state->filter); pfree(state); diff --git a/gpcontrib/anser/src/anserplanexec.c b/gpcontrib/anser/src/anserplanexec.c index 79fbc6f1fcf..96089ff2fac 100644 --- a/gpcontrib/anser/src/anserplanexec.c +++ b/gpcontrib/anser/src/anserplanexec.c @@ -83,6 +83,7 @@ typedef struct AnserBloomProduceScanState AnserBloomFilterProduceState *produce; AttrNumber key_attno; int64 planned_bytes; + bool started; /* this process actually executed the node */ bool published; } AnserBloomProduceScanState; @@ -332,6 +333,7 @@ anser_produce_begin(CustomScanState *node, EState *estate, int eflags) st->key_attno = (AttrNumber) intVal(list_nth(priv, ANSER_RF_PRIV_KEY_ATTNO)); st->planned_bytes = intVal(list_nth(priv, ANSER_RF_PRIV_PLANNED_BYTES)); + st->started = false; st->published = false; anser_rf_build_key(cscan, &key); @@ -339,6 +341,9 @@ anser_produce_begin(CustomScanState *node, EState *estate, int eflags) st->produce = ExecInitAnserBloomFilterProduce(&key, total_elems, max_payload, part_index, total_parts); + ANSER_DEBUG("anser: producer init cond=%u part=%u/%u elems=" INT64_FORMAT " payload=%zu state=%s", + key.condition_id, part_index, total_parts, total_elems, + max_payload, st->produce != NULL ? "ok" : "NULL"); node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); } @@ -352,12 +357,22 @@ anser_produce_next(CustomScanState *node) { AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) node; PlanState *child = (PlanState *) linitial(node->custom_ps); - TupleTableSlot *slot = ExecProcNode(child); + TupleTableSlot *slot; + + /* + * Every process that deserializes the plan builds this node, but only the + * ones running its slice ever execute it; the rest must stay silent. + */ + st->started = true; + + slot = ExecProcNode(child); if (TupIsNull(slot)) { if (!st->published) { + ANSER_DEBUG("anser: producer child exhausted, publishing (state=%s)", + st->produce != NULL ? "ok" : "NULL"); (void) ExecAnserBloomFilterProducePublish(st->produce); st->published = true; } @@ -388,6 +403,20 @@ anser_produce_end(CustomScanState *node) { AnserBloomProduceScanState *st = (AnserBloomProduceScanState *) node; + /* + * A producer that ran but never published was abandoned mid-scan (squelch, + * or an error upstream), and consumers must be told so rather than left + * waiting for a part that will never come. A producer that never ran at + * all belongs to another slice: cancelling here would destroy a channel + * this process has no part in -- which is exactly what the coordinator + * used to do to every runtime filter. + */ + if (st->started && !st->published && st->produce != NULL) + { + (void) ExecAnserBloomFilterProduceCancel(st->produce); + st->published = true; + } + if (st->produce != NULL) ExecEndAnserBloomFilterProduce(st->produce); st->produce = NULL; From 620190deeacd266da7a48cd6e62b763f85c603dc Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 7 Sep 2026 13:12:46 +0300 Subject: [PATCH 10/15] More logs to find out what happens on producer site --- gpcontrib/anser/src/anserdispatch.c | 33 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c index 738aca74520..ce6bc9091b8 100644 --- a/gpcontrib/anser/src/anserdispatch.c +++ b/gpcontrib/anser/src/anserdispatch.c @@ -206,6 +206,9 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, struct pgNotify *notify) { PGnotify *n = (PGnotify *) notify; + CdbDispatchResult *dr = (CdbDispatchResult *) dispatchResult; + int sender = (dr != NULL && dr->segdbDesc != NULL) + ? dr->segdbDesc->segindex : -99; AnserWireMsg msg; AnserDispChannel *chan; MemoryContext oldcxt; @@ -233,15 +236,15 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, if (msg.kind == ANSER_WIRE_KIND_SUBSCRIBE) { - PGconn *conn = ((CdbDispatchResult *) dispatchResult)->segdbDesc->conn; + PGconn *conn = dr->segdbDesc->conn; /* * A consumer can subscribe after the channel is already complete -- * producers on other segments may well have finished first -- so * deliver immediately in that case rather than recording interest. */ - ANSER_DEBUG("anser: QD subscribe cond=%u (channel %s)", - msg.key.condition_id, + ANSER_DEBUG("anser: QD subscribe cond=%u from seg%d (channel %s)", + msg.key.condition_id, sender, chan->complete ? "complete, delivering now" : "still collecting"); if (chan->complete) (void) anser_disp_push(conn, chan); @@ -271,8 +274,9 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, anser_disp_apply_part(chan, raw, (Size) raw_len, msg.total_parts, (msg.flags & ANSER_WIRE_F_CANCELLED) != 0); - ANSER_DEBUG("anser: QD part cond=%u %d/%d bytes=%d -> %s", - msg.key.condition_id, chan->parts_received, + ANSER_DEBUG("anser: QD part cond=%u from seg%d (says part %d of %d) %d/%d bytes=%d -> %s", + msg.key.condition_id, sender, msg.part_index, + msg.total_parts, chan->parts_received, chan->expected_parts, raw_len, chan->cancelled ? "cancelled" : chan->complete ? "complete" : "collecting"); @@ -301,8 +305,20 @@ anser_disp_apply_part(AnserDispChannel *chan, const void *payload, if (chan->cancelled) return; /* already dead; nothing to do */ - if (total_parts > 0 && chan->expected_parts == 0) + if (total_parts > chan->expected_parts) + { + /* + * Take the largest count any producer claims. Letting a later part + * lower it would complete the channel early and deliver a filter + * missing another segment's keys -- a false negative, which drops + * joinable rows. Disagreement means the producers computed their + * slice width differently and is worth seeing. + */ + if (chan->expected_parts != 0) + elog(LOG, "anser: cond=%u producer count changed %d -> %d", + chan->key.condition_id, chan->expected_parts, total_parts); chan->expected_parts = total_parts; + } if (cancelled) { @@ -498,6 +514,11 @@ AnserDispatchLocalPublish(const AnserChannelKey *channel_key, { anser_disp_apply_part(chan, payload, payload_len, (int) total_parts, cancelled); + ANSER_DEBUG("anser: QD local part cond=%u (part %u of %u) %d/%d bytes=%zu -> %s", + channel_key->condition_id, part_index, total_parts, + chan->parts_received, chan->expected_parts, payload_len, + chan->cancelled ? "cancelled" : + chan->complete ? "complete" : "collecting"); if (chan->complete) anser_disp_deliver(chan); } From 921cadcfd1d55e4bf2e359ad648a8a37ed7357ae Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 7 Sep 2026 14:17:33 +0300 Subject: [PATCH 11/15] dispatcher drop remaining command on return There could be command in a queue, but we drop them and return. Should try to process them instead --- gpcontrib/anser/src/ansersideband.c | 10 +- src/backend/cdb/dispatcher/cdbdisp_async.c | 171 ++++++++++++--------- 2 files changed, 107 insertions(+), 74 deletions(-) diff --git a/gpcontrib/anser/src/ansersideband.c b/gpcontrib/anser/src/ansersideband.c index 8532d1dde21..8a1ac201cee 100644 --- a/gpcontrib/anser/src/ansersideband.c +++ b/gpcontrib/anser/src/ansersideband.c @@ -140,6 +140,7 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, { char *msg; TimestampTz start; + int reads = 0; if (payload != NULL) *payload = NULL; @@ -178,9 +179,13 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, if (timeout_ms >= 0 && TimestampDifferenceExceeds(start, GetCurrentTimestamp(), timeout_ms)) { - ANSER_DEBUG("anser: seg%d gave up on cond=%u after %ld ms", + /* + * Report what we saw, not just that we waited: "nothing arrived" + * and "something arrived for another channel" are different bugs. + */ + ANSER_DEBUG("anser: seg%d gave up on cond=%u after %ld ms (read %d message(s), %d unclaimed)", GpIdentity.segindex, channel_key->condition_id, - timeout_ms); + timeout_ms, reads, list_length(AnserInbox)); return false; } @@ -193,6 +198,7 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, */ if (!anser_sideband_read_one(ANSER_SIDEBAND_POLL_MS)) return false; + reads = list_length(AnserInbox); } } diff --git a/src/backend/cdb/dispatcher/cdbdisp_async.c b/src/backend/cdb/dispatcher/cdbdisp_async.c index b8250f9c9fd..a2551658b86 100644 --- a/src/backend/cdb/dispatcher/cdbdisp_async.c +++ b/src/backend/cdb/dispatcher/cdbdisp_async.c @@ -142,6 +142,8 @@ static void checkDispatchResult(CdbDispatcherState *ds, int timeout_sec); static bool processResults(CdbDispatchResult *dispatchResult); +static void processNotifies(CdbDispatchResult *dispatchResult); + static void signalQEs(CdbDispatchCmdAsync *pParms); @@ -994,6 +996,96 @@ send_sequence_response(PGconn *conn, Oid oid, int64 last, int64 cached, int64 in elog(ERROR, "Failed to send sequence response: %s", PQerrorMessage(conn)); } +/* + * Hand over any notifications this QE has sent us. + * + * Kept separate from processResults() because it must run on every path that + * consumed input, not only the one where the QE still has work to do. A + * notification that arrives in the same read as the QE's command completion is + * already parsed out of the socket and queued on the connection, but that + * connection is about to be dropped from the poll set (stillRunning goes + * false), so nothing would ever wake us for it again -- the notification would + * be silently lost. nextval() never hit this because its QE blocks for a + * reply, leaving the command incomplete; a fire-and-forget sender does hit it. + */ +static void +processNotifies(CdbDispatchResult *dispatchResult) +{ + SegmentDatabaseDescriptor *segdbDesc = dispatchResult->segdbDesc; + + PGnotify *qnotifies = PQnotifies(segdbDesc->conn); + while(qnotifies && elog_geterrcode() == 0) + { + if (strcmp(qnotifies->relname, CDB_NOTIFY_NEXTVAL) == 0) + { + /* + * If there was nextval request then respond back on this libpq + * connection with the next value. Check and process nextval + * message only if QD has not already hit the error. Since QD could + * have hit the error while processing the previous nextval_qd() + * request itself and since full error handling is not complete yet + * (ex: releasing all the locks, etc.), shouldn't attempt to call + * nextval_qd() again. + */ + + CHECK_FOR_INTERRUPTS(); + + int64 last; + int64 cached; + int64 increment; + bool overflow; + Oid dbid; + Oid seq_oid; + + if (sscanf(qnotifies->extra, "%u:%u", &dbid, &seq_oid) != 2) + elog(ERROR, "invalid nextval message"); + + if (dbid != MyDatabaseId) + elog(ERROR, "nextval message database id:%u doesn't match my database id:%u", + dbid, MyDatabaseId); + + PG_TRY(); + { + nextval_qd(seq_oid, &last, &cached, &increment, &overflow); + } + PG_CATCH(); + { + send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, true /* error */); + PG_RE_THROW(); + } + PG_END_TRY(); + /* respond back on this libpq connection with the next value */ + send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, false /* error */); + } + else if (strcmp(qnotifies->relname, CDB_NOTIFY_ENDPOINT_ACK) == 0) + { + qnotifies->next = (struct pgNotify *) dispatchResult->ackPGNotifies; + dispatchResult->ackPGNotifies = qnotifies; + + /* Don't free the notify here since it in queue now */ + qnotifies = NULL; + } + else if (cdbdisp_notify_hook != NULL && + cdbdisp_notify_hook(dispatchResult, qnotifies)) + { + /* Consumed by an extension; nothing further to do here. */ + } + else + { + /* Got an unknown PGnotify, just record it in log */ + if (qnotifies->relname) + elog(LOG, "got an unknown notify message : %s", qnotifies->relname); + } + + if (qnotifies) + PQfreemem(qnotifies); + qnotifies = PQnotifies(segdbDesc->conn); + } + + forwardQENotices(); + +} + /* * Receive and process input from one QE. * @@ -1064,7 +1156,12 @@ processResults(CdbDispatchResult *dispatchResult) if (!pRes) { ELOG_DISPATCHER_DEBUG("%s -> idle", segdbDesc->whoami); - /* this is normal end of command */ + /* + * Normal end of command. Take any notifications with us: this + * connection is about to leave the poll set, so this is the last + * chance to see them. + */ + processNotifies(dispatchResult); return true; } @@ -1161,77 +1258,7 @@ processResults(CdbDispatchResult *dispatchResult) } forwardQENotices(); - - PGnotify *qnotifies = PQnotifies(segdbDesc->conn); - while(qnotifies && elog_geterrcode() == 0) - { - if (strcmp(qnotifies->relname, CDB_NOTIFY_NEXTVAL) == 0) - { - /* - * If there was nextval request then respond back on this libpq - * connection with the next value. Check and process nextval - * message only if QD has not already hit the error. Since QD could - * have hit the error while processing the previous nextval_qd() - * request itself and since full error handling is not complete yet - * (ex: releasing all the locks, etc.), shouldn't attempt to call - * nextval_qd() again. - */ - - CHECK_FOR_INTERRUPTS(); - - int64 last; - int64 cached; - int64 increment; - bool overflow; - Oid dbid; - Oid seq_oid; - - if (sscanf(qnotifies->extra, "%u:%u", &dbid, &seq_oid) != 2) - elog(ERROR, "invalid nextval message"); - - if (dbid != MyDatabaseId) - elog(ERROR, "nextval message database id:%u doesn't match my database id:%u", - dbid, MyDatabaseId); - - PG_TRY(); - { - nextval_qd(seq_oid, &last, &cached, &increment, &overflow); - } - PG_CATCH(); - { - send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, true /* error */); - PG_RE_THROW(); - } - PG_END_TRY(); - /* respond back on this libpq connection with the next value */ - send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, false /* error */); - } - else if (strcmp(qnotifies->relname, CDB_NOTIFY_ENDPOINT_ACK) == 0) - { - qnotifies->next = (struct pgNotify *) dispatchResult->ackPGNotifies; - dispatchResult->ackPGNotifies = qnotifies; - - /* Don't free the notify here since it in queue now */ - qnotifies = NULL; - } - else if (cdbdisp_notify_hook != NULL && - cdbdisp_notify_hook(dispatchResult, qnotifies)) - { - /* Consumed by an extension; nothing further to do here. */ - } - else - { - /* Got an unknown PGnotify, just record it in log */ - if (qnotifies->relname) - elog(LOG, "got an unknown notify message : %s", qnotifies->relname); - } - - if (qnotifies) - PQfreemem(qnotifies); - qnotifies = PQnotifies(segdbDesc->conn); - } - - forwardQENotices(); + processNotifies(dispatchResult); return false; /* we must keep on monitoring this socket */ } From f48822999cf340ad4e059e4d3bf9b92d4b5a06dd Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Fri, 11 Sep 2026 15:17:59 +0300 Subject: [PATCH 12/15] Fix README --- gpcontrib/anser/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/gpcontrib/anser/README.md b/gpcontrib/anser/README.md index beee1d61e44..4458a98d4ea 100644 --- a/gpcontrib/anser/README.md +++ b/gpcontrib/anser/README.md @@ -120,6 +120,34 @@ by up to that long. | `anser.runtime_filter` | `off` | USERSET | Enables the post-planning pass that injects bloom-filter producer/consumer nodes into a matching plan. Requires `anser.enable`. | | `anser.max_info_size` | `65 MB` | POSTMASTER | Maximum serialized payload (merged bloom filter + part header) a channel may hold; caps the effective bloom-filter size. The default is `64 MB + 1 MB` so a full 64 MB power-of-two bitset fits with its header; `bloom_create` also floors every bitset at 1 MB. | | `anser.timeout_ms` | `1000` | USERSET | How long a consumer waits for its filter before running unfiltered. The deadline matters because a producer that gets squelched never publishes at all: `ExecSquelchNode` only marks a `CustomScanState`, it does not call the node back. | +| `anser.debug` | `off` | USERSET | Traces the exchange — publish, merge, delivery, receive — in the log of the process each step happens in. See below. | + +### Tracing an exchange + +Until a filter is either used or timed out, none of the handoff is visible in +`EXPLAIN`: a consumer that waited and got nothing looks exactly like one whose +producers never published. `anser.debug` makes each step log where it happened, +which is normally the fastest way to find where an exchange broke: + +``` +seg0 producer init cond=0 part=0/3 elems=3334 payload=67108928 state=ok +seg0 producer child exhausted, publishing (state=ok) +seg0 published cond=0 part=0/3 bytes=1048592 cancelled=0 sent=1 +QD part cond=0 from seg0 (says part 0 of 3) 1/3 bytes=1048592 -> collecting +QD part cond=0 from seg1 (says part 1 of 3) 2/3 bytes=1048592 -> collecting +QD part cond=0 from seg2 (says part 2 of 3) 3/3 bytes=1048592 -> complete +QD delivering cond=0 to 3 subscriber(s) +QD pushed cond=0 bytes=1048592 cancelled=0 +seg0 received cond=0 bytes=1048592 cancelled=0 +``` + +Set it in `postgresql.conf` (`gpconfig -c anser.debug -v on`) rather than with +`SET` if you need to see the producer gang: a session-level `SET` does not +reliably reach every gang, and the producers are the half you usually want. + +A consumer that gives up reports what it saw — `read 0 message(s), 0 +unclaimed` means nothing arrived at all, whereas unclaimed messages mean +something arrived for a channel it was not waiting on. ## Data flow: producer → merge (bitwise union) → consumer From a32b84cce40ca5733701134cc610050eedbee7c6 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Fri, 11 Sep 2026 19:01:57 +0300 Subject: [PATCH 13/15] Change protocol to uses CRC We sent data on network and flip one bytes from 1 to 0 means wrong answer. TCP checksums in a practise won't help. So we should protect our data using additional CRC. --- gpcontrib/anser/Makefile | 1 + gpcontrib/anser/README.md | 108 +++++- gpcontrib/anser/anser_test--1.0.sql | 73 ++++ gpcontrib/anser/expected/anser_test.out | 251 ++++++++++++++ gpcontrib/anser/include/anserbloom.h | 1 + gpcontrib/anser/include/anserfilter.h | 50 +++ gpcontrib/anser/include/anserplan.h | 11 + gpcontrib/anser/include/ansersideband.h | 147 +++++++- gpcontrib/anser/sql/anser_test.sql | 164 +++++++++ gpcontrib/anser/src/anser_test.c | 440 +++++++++++++++++++++++- gpcontrib/anser/src/anserbloomconsume.c | 10 +- gpcontrib/anser/src/anserbloomproduce.c | 50 ++- gpcontrib/anser/src/anserdispatch.c | 290 ++++++++++++---- gpcontrib/anser/src/anserfilter.c | 105 +++++- gpcontrib/anser/src/anserplan.c | 57 ++- gpcontrib/anser/src/anserplanexec.c | 23 +- gpcontrib/anser/src/ansersideband.c | 221 +++++++++--- 17 files changed, 1834 insertions(+), 168 deletions(-) diff --git a/gpcontrib/anser/Makefile b/gpcontrib/anser/Makefile index 3560eb65e8f..a675e3d58e0 100644 --- a/gpcontrib/anser/Makefile +++ b/gpcontrib/anser/Makefile @@ -32,6 +32,7 @@ OBJS = \ src/anserdispatch.o \ src/anserfilter.o \ src/anserinit.o \ + src/anserpayload.o \ src/anserplan.o \ src/anserplanexec.o \ src/ansersideband.o \ diff --git a/gpcontrib/anser/README.md b/gpcontrib/anser/README.md index 4458a98d4ea..862b994a84e 100644 --- a/gpcontrib/anser/README.md +++ b/gpcontrib/anser/README.md @@ -51,16 +51,73 @@ AnserChannelKey = { gp_session_id, gp_command_count, condition_id, condition_key - `gp_session_id` + `gp_command_count` scope the channel to one query execution, so keys never collide across sessions or across statements in a session. - `condition_id` distinguishes multiple filters within the same query. -- `condition_key` is an opaque string describing the filtered condition (today a - synthetic `rf:.=.` string). Both sides derive it - independently and must agree — it is what makes a producer and a consumer meet - on the same channel. +- `condition_key` is an opaque string describing the filtered condition — today + just `anser_rf_`, since the planner stamps the same key into + both plan nodes at injection time. It exists for the case where the key has to + be derived from the build's semantic identity instead (so that producer and + consumer can find each other without having been planned together), which is + why the wire format treats it as arbitrary bytes rather than as an identifier. Channels live in a hash in the coordinator backend, created on first use and dropped at `ExecutorEnd` (or on transaction abort). Since the merge and the delivery both happen in that one process, the accumulator is an ordinary `palloc`'d buffer. +### Giving up early + +A bloom filter that is too small for its key count matches almost everything: it +costs a hash and `k` probes per probe row and eliminates nothing. Anser checks +for that at the three points where new information becomes available, and always +fails open — giving up means the query runs unfiltered, never that it runs wrong. + +| Where | Knows | Check | Cost of giving up | +| --- | --- | --- | --- | +| Planner (`anserplan.c`) | the row estimate | bitset bits / estimated keys ≥ 4 | nothing — the nodes are never injected | +| Producer init (`anserfilter.c`) | the plan parameters | the filter fits the payload cap, and still ≥ 4 bits/key | one `palloc0`, freed immediately; cancel published on the first tuple, before the build side is scanned | +| Producer publish (`anserbloomproduce.c`) | the filter, fully built | estimated FPR (`fill^k`) ≤ 50% | the build scan, already spent; saves the network and every consumer's probing | +| Coordinator merge (`anserdispatch.c`) | the merged payload | fill ≤ 95% | the merge, already spent; saves delivery and probing | + +The planner gate is the one that matters, because it is the only one that costs +nothing. The rest exist because a row estimate can be wrong, and the last one +exists because **folding changes the answer**: OR-ing three parts that are each +60% full gives a merged filter that is 94% full, so no producer can tell whether +the result will be useful. + +One subtlety worth knowing if you touch the sizing: do **not** clamp the element +estimate to make a filter fit. `total_elems` is also what `optimal_k()` derives +the hash count from, so understating it misconfigures the filter — a 128M-row +build side declared as 33.5M gets `k=10` where the optimum is `k=3`, turning a +13% false positive rate into 38%. The keys all go in regardless of what was +written down. Clamp the *size*; keep the count honest. + +### Payload types + +A channel carries one **payload type**, declared on the wire and registered in +`anserpayload.c`. The transport itself moves opaque bytes and never branches on +what they mean; everything type-specific lives in one descriptor +(`AnserPayloadOps`): + +| | `fold()` | `checksum_body` | +| --- | --- | --- | +| `B` — bloom filter | bitwise OR of equal-sized bitsets | yes | +| `-` — none (a subscription) | n/a | n/a | + +`fold()` is how the coordinator reduces one part per producer to a single +payload; there is no generic answer, which is why each type supplies it. A row +count, for instance, would fold by summing. + +`checksum_body` is about consequence, not size. A bloom filter fails +asymmetrically — a bit flipped 1 → 0 removes a key, so a joinable row is +rejected and the query silently returns too few rows — whereas a row count only +feeds a planning decision, where corruption costs a worse plan and never a wrong +answer. Types of the second kind opt out and pay nothing. (The header and +condition key are checksummed either way; that is ~100 ns and it is what +protects the routing fields.) + +Adding a type is three steps, none of which touch the framing: define a code, +add a row to `AnserPayloadTable`, and pass the code from the producer and +consumer nodes. `anserpayload.h` has the details. + ### How it attaches to the server Everything is reached through an existing extensibility point, so the server @@ -91,19 +148,36 @@ applies to the SQL-level `NOTIFY`, which must fit a queue page — but it delive through `pq_sendstring`, so the payload must be a NUL-free string: ``` -anser1 \n - +anser3 K T SSSSSSSSSS CCCCCCCCCC DDDDDDDDDD PPPPPPPPPP TTTTTTTTTT FFFF KKKK BBBBBBBBBB XXXXXXXX +^tag ^ ^ session command condition part total flags keylen bodylen crc + | \ payload type + \ kind + ``` -`kind` is `P` (a producer's part) or `S` (a consumer subscribing). The header -holds only numbers and one character, so it cannot contain the newline that ends -it; key and body are taken by length, so neither needs escaping. +`kind` is `P` (a producer's part) or `S` (a consumer subscribing) — what the +message *does*, as opposed to the payload type, which is what it *carries*. The +header is **95 bytes of fixed-width ASCII**, so the key and the body begin at +offsets that nothing in their own contents can shift, and the two lengths must +account for the message exactly — a short, long or misaligned message is rejected +before any of it is used. There is deliberately no delimiter anywhere in the +format: the earlier version ended its header with a newline and found it with +`strchr()`, which was correct only while the key could not itself contain a +newline, an invariant nothing enforced. **Coordinator → segment** is a `GP_SIDEBAND_MESSAGE`, written with `pqPutnchar`, which performs no conversion — so the merged filter travels as **raw binary**, with no base64 tax. That is the direction that matters most, since the merged payload is sent once *per consumer* while each part is sent once. +Both directions carry a **CRC32C**, and both discard a message that fails it. +The transport is TCP, not the UDP interconnect, so this is not about a lossy +link — TCP's 16-bit checksum is simply thin cover for a megabyte, and hardware +CRC32C costs about 0.05 ms/MB. The header and key are always covered; whether +the body is covered too is the payload type's decision, for the reasons in +[Payload types](#payload-types) above. Where it applies, the segment → +coordinator CRC covers the pre-base64 bytes, so it validates the decode as well. + The coordinator services these while it is blocked receiving tuples: the interconnect adds every dispatch socket to its wait set (`ic_udpifc.c`, `ic_tcp.c`), and a readable one leads to @@ -195,7 +269,8 @@ Step by step: open. 3. **Deliver (coordinator → every consumer).** Once every expected part is - folded, the merged bitset is pushed to each subscriber. Delivery is per + folded, the merged payload is judged once (see [Giving up + early](#giving-up-early)) and then pushed to each subscriber. Delivery is per consumer: a failed write costs that one segment its filter and leaves the others alone. A consumer that subscribes *after* the channel completed — which happens routinely, since producers on other segments may finish first — is @@ -228,3 +303,16 @@ never in an error raised into the query: - query cancellation → the consumer's wait is a `CHECK_FOR_INTERRUPTS` loop, and a delivery that arrives after nobody is waiting is discarded by the QE command loop (`GP_SIDEBAND_MESSAGE` is accepted and ignored there). + +## Tests + +`make installcheck` runs two suites: + +- `anser_test` — the payload protocol (serialize, fold, reject a mismatched + part) and the four give-up decisions, each walked along its boundary: the + estimate either side of 4 bits/key, the payload cap either side of "1 MB plus + a header", fill either side of the 95% limit, and a producer whose filter + saturates. The case tables live in the `.sql` file, so the expected output + records real sizes rather than a bare `t`. +- `anser_runtime_filter` — plan-tree integration end to end: the nodes are + injected, and query results are identical with the feature on and off. diff --git a/gpcontrib/anser/anser_test--1.0.sql b/gpcontrib/anser/anser_test--1.0.sql index 1772e748653..3c2f8d14cfd 100644 --- a/gpcontrib/anser/anser_test--1.0.sql +++ b/gpcontrib/anser/anser_test--1.0.sql @@ -24,3 +24,76 @@ CREATE FUNCTION anser_test_node_roundtrip(value int4) RETURNS bool AS 'MODULE_PATHNAME' LANGUAGE C STRICT; + +-- The planner's give-up decision: NULL means no filter is injected. +CREATE FUNCTION anser_test_rf_size( + est_rows float8, + OUT injected bool, + OUT total_elems bigint, + OUT max_payload bigint, + OUT planned_bytes bigint) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- What AnserBloomCreate builds, or NULL when it declines to build anything. +CREATE FUNCTION anser_test_bloom_shape( + total_elems bigint, + cap bigint, + OUT built bool, + OUT bits bigint, + OUT serialized bigint, + OUT bits_per_key float8) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The coordinator's decision about a merged payload, on a synthetic part. +CREATE FUNCTION anser_test_worth_delivering( + bitset_bytes int4, + bytes_set int4, + damage text) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- Producer end to end: ":". +CREATE FUNCTION anser_test_producer_decision( + total_elems bigint, + cap bigint, + n_keys int4) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The formatted QE -> QD message, verbatim. +CREATE FUNCTION anser_test_wire_format( + kind "char", + payload_type "char", + flags int4, + condition_key text, + body bytea) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- Format, alter one byte, parse: reports what the reader made of it. +CREATE FUNCTION anser_test_wire_roundtrip( + condition_key text, + body bytea, + tamper text, + payload_type "char") +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The QD -> QE checksum, so a test can compare two of them. +CREATE FUNCTION anser_test_push_crc( + payload_type "char", + condition_id int4, + flags int4, + condition_key text, + body bytea) +RETURNS int8 +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; diff --git a/gpcontrib/anser/expected/anser_test.out b/gpcontrib/anser/expected/anser_test.out index 0a65887d5e1..9e663672a02 100644 --- a/gpcontrib/anser/expected/anser_test.out +++ b/gpcontrib/anser/expected/anser_test.out @@ -32,4 +32,255 @@ SELECT anser_test_node_roundtrip(168); t (1 row) +-- --------------------------------------------------------------------------- +-- Sizing, and the decision not to bother. +-- +-- Anser can decline to build a filter at four points, and these cases walk the +-- boundary of each. Declining is always safe: it costs the query its filter, +-- never its correctness. The payload cap is pinned here because every size +-- below follows from it. +-- --------------------------------------------------------------------------- +SET anser.max_info_size = 68157440; +-- 1. The planner's gate -- the only one that costs nothing, since nothing is +-- injected and no consumer is left waiting. A 64 MB bitset is 536870912 bits, +-- so 134217728 estimated keys is exactly 4 bits/key and one more key is not. +-- A zero or negative estimate is treated as one key, not as "no filter". +SELECT t.est_rows, s.injected, s.total_elems, s.planned_bytes +FROM (VALUES (0), (-5), (1), (1000), (1000000), (10000000), + (134217728), (134217729), (150000000), (1000000000)) + AS t(est_rows), + LATERAL anser_test_rf_size(t.est_rows) AS s; + est_rows | injected | total_elems | planned_bytes +------------+----------+-------------+--------------- + 0 | t | 1 | 1048576 + -5 | t | 1 | 1048576 + 1 | t | 1 | 1048576 + 1000 | t | 1000 | 1048576 + 1000000 | t | 1000000 | 1048576 + 10000000 | t | 10000000 | 16777216 + 134217728 | t | 134217728 | 67108864 + 134217729 | f | | + 150000000 | f | | + 1000000000 | f | | +(10 rows) + +-- 2. Filter construction. bloom_create floors every bitset at 1 MB and that +-- floor overrides the work_mem cap, so a cap below "1 MB + header" can only +-- yield a filter too large to send -- which is why a flat 1 MB cap is refused +-- while 1048592 (the floor plus exactly one header) is accepted and serializes +-- to precisely the cap. fits_cap is the invariant that must never be false. +SELECT t.what, t.elems, t.cap, s.built, s.bits, s.serialized, + round(s.bits_per_key::numeric, 3) AS bits_per_key, + (NOT s.built OR s.serialized <= t.cap) AS fits_cap +FROM (VALUES + ('no keys', 0::bigint, 1048640::bigint), + ('negative keys', -1, 1048640), + ('cap equals the header', 32, 16), + ('cap one byte over the header', 32, 17), + ('cap is 1 MB, no header room', 32, 1048576), + ('cap is 1 MB plus a header', 32, 1048592), + ('cap one byte short of that', 32, 1048591), + ('a single key', 1, 1048640), + ('exactly 4 bits/key', 2097152, 1048640), + ('one key too many', 2097153, 1048640), + ('exactly 4 bits/key at 2 MB', 4194304, 2097216), + ('one key too many at 2 MB', 4194305, 2097216), + ('a billion keys', 1000000000, 68157440)) + AS t(what, elems, cap), + LATERAL anser_test_bloom_shape(t.elems, t.cap) AS s; + what | elems | cap | built | bits | serialized | bits_per_key | fits_cap +------------------------------+------------+----------+-------+----------+------------+--------------+---------- + no keys | 0 | 1048640 | f | | | | t + negative keys | -1 | 1048640 | f | | | | t + cap equals the header | 32 | 16 | f | | | | t + cap one byte over the header | 32 | 17 | f | | | | t + cap is 1 MB, no header room | 32 | 1048576 | f | | | | t + cap is 1 MB plus a header | 32 | 1048592 | t | 8388608 | 1048592 | 262144.000 | t + cap one byte short of that | 32 | 1048591 | f | | | | t + a single key | 1 | 1048640 | t | 8388608 | 1048592 | 8388608.000 | t + exactly 4 bits/key | 2097152 | 1048640 | t | 8388608 | 1048592 | 4.000 | t + one key too many | 2097153 | 1048640 | f | | | | t + exactly 4 bits/key at 2 MB | 4194304 | 2097216 | t | 16777216 | 2097168 | 4.000 | t + one key too many at 2 MB | 4194305 | 2097216 | f | | | | t + a billion keys | 1000000000 | 68157440 | f | | | | t +(13 rows) + +-- 3. The coordinator's gate on a merged payload. The fill is fixed exactly by +-- setting whole bytes rather than by inserting keys, which would be both slow +-- and only statistically precise. The rule is "reject above 95%", so a payload +-- sitting exactly on the limit is still delivered. Malformed framing is not +-- worth delivering either, since it cannot be a filter at all. +SELECT t.what, t.bitset_bytes, t.bytes_set, t.damage, + anser_test_worth_delivering(t.bitset_bytes, t.bytes_set, t.damage) + AS worth_delivering +FROM (VALUES + ('empty', 1000, 0, ''), + ('half full', 1000, 500, ''), + ('just under the limit', 1000, 949, ''), + ('exactly on the limit', 1000, 950, ''), + ('just over the limit', 1000, 951, ''), + ('completely full', 1000, 1000, ''), + ('no bitset at all', 0, 0, ''), + ('corrupt magic', 1000, 0, 'magic'), + ('corrupt version', 1000, 0, 'version'), + ('zero part count', 1000, 0, 'parts'), + ('null payload', 1000, 0, 'null')) + AS t(what, bitset_bytes, bytes_set, damage); + what | bitset_bytes | bytes_set | damage | worth_delivering +----------------------+--------------+-----------+---------+------------------ + empty | 1000 | 0 | | t + half full | 1000 | 500 | | t + just under the limit | 1000 | 949 | | t + exactly on the limit | 1000 | 950 | | t + just over the limit | 1000 | 951 | | f + completely full | 1000 | 1000 | | f + no bitset at all | 0 | 0 | | f + corrupt magic | 1000 | 0 | magic | f + corrupt version | 1000 | 0 | version | f + zero part count | 1000 | 0 | parts | f + null payload | 1000 | 0 | null | f +(11 rows) + +-- 4. The producer driven end to end on the coordinator-local path: build, +-- insert, publish, consume. "no-filter:cancelled" is the case that matters +-- most -- construction declined, so the cancel goes out before the build side +-- is scanned and no consumer waits for its timeout. The last row is the one +-- the planner cannot predict: a filter sized for 32 keys that receives three +-- million, which saturates and so publishes a cancel instead of a payload. +SELECT t.what, + anser_test_producer_decision(t.elems, t.cap, t.n_keys) AS decision +FROM (VALUES + ('sparse filter', 32::bigint, 1048640::bigint, 100), + ('cap with no header room', 32, 1048576, 100), + ('no keys declared', 0, 1048640, 0), + ('one key past the floor', 2097153, 1048640, 0), + ('exactly 4 bits/key', 2097152, 1048640, 100000), + ('estimate of 32, three million inserted', 32, 1048640, 3000000)) + AS t(what, elems, cap, n_keys); + what | decision +----------------------------------------+--------------------- + sparse filter | built:delivered + cap with no header room | no-filter:cancelled + no keys declared | no-filter:cancelled + one key past the floor | no-filter:cancelled + exactly 4 bits/key | built:delivered + estimate of 32, three million inserted | built:cancelled +(6 rows) + +-- --------------------------------------------------------------------------- +-- The wire protocol. +-- +-- Not exhaustive; a fuzzer would be the right tool for that. These cover the +-- properties that are cheap to check and expensive to lose: a fixed-width +-- header with the fields where they are documented, a payload that cannot be +-- mistaken for framing, a length cross-check that rejects a message which does +-- not add up, and a checksum that rejects one altered byte anywhere it covers. +-- --------------------------------------------------------------------------- +-- The bytes, exactly. Session/command/condition are 42/7/3 and part is 1 of 3, +-- so these are golden values: any change to the layout, the field widths or +-- what the checksum covers shows up here as a diff. Note the third row -- a +-- cancelled message carries no body even though one was passed, and its +-- checksum must be computed over what is actually sent. +-- +-- That the message comes back as text is itself the check that it is NUL-free, +-- which a NOTIFY payload has to be: the fourth row's body is 00 ff 00. +SELECT t.what, length(m.msg) AS len, m.msg +FROM (VALUES + ('part with a 3-byte body', 'P', 'B', 0, '\x616263'::bytea), + ('subscription, no body', 'S', '-', 0, '\x'::bytea), + ('cancelled: body suppressed', 'P', 'B', 1, '\x616263'::bytea), + ('body of NUL and 0xff bytes', 'P', 'B', 0, '\x00ff00'::bytea)) + AS t(what, kind, ptype, flags, body), + LATERAL (SELECT anser_test_wire_format(t.kind::"char", t.ptype::"char", + t.flags, 'anser_rf_3', t.body) + AS msg) m; + what | len | msg +----------------------------+-----+--------------------------------------------------------------------------------------------------------------- + part with a 3-byte body | 109 | anser3 P B 0000000042 0000000007 0000000003 0000000001 0000000003 0000 0010 0000000004 f7b3c0d5anser_rf_3YWJj + subscription, no body | 105 | anser3 S - 0000000042 0000000007 0000000003 0000000001 0000000003 0000 0010 0000000000 ecd98662anser_rf_3 + cancelled: body suppressed | 105 | anser3 P B 0000000042 0000000007 0000000003 0000000001 0000000003 0001 0010 0000000000 e88148e9anser_rf_3 + body of NUL and 0xff bytes | 109 | anser3 P B 0000000042 0000000007 0000000003 0000000001 0000000003 0000 0010 0000000004 027f789banser_rf_3AP8A +(4 rows) + +-- Alter exactly one byte (or one length) and see what the reader does. +-- +-- The split matters: "malformed" is rejected by framing, before a byte is +-- allocated or a channel created, while "checksum-mismatch" got past framing +-- and was caught by the CRC. Corrupting the kind byte is the interesting one +-- -- the message stays perfectly well-formed, and only the checksum notices, +-- which is why the checksum covers the header and not just the payload. +SELECT t.tamper, + anser_test_wire_roundtrip('anser_rf_3', '\x616263'::bytea, t.tamper, + 'B'::"char") AS outcome +FROM (VALUES (''), ('truncate'), ('append'), ('bodylen'), ('tag'), ('kind'), + ('type'), ('crc'), ('key'), ('body')) AS t(tamper); + tamper | outcome +----------+------------------- + | ok + truncate | malformed + append | malformed + bodylen | malformed + tag | malformed + kind | checksum-mismatch + type | unknown-type + crc | checksum-mismatch + key | checksum-mismatch + body | checksum-mismatch +(10 rows) + +-- Keys and bodies that must survive the trip unchanged. "ok" means every +-- field and the decoded body came back identical, so each of these is a case +-- where a naive framing would have gone wrong: the key travels as raw bytes +-- and is taken by length, which is what lets it contain a newline, a space, or +-- the protocol's own tag without any escaping. +SELECT t.what, + anser_test_wire_roundtrip(t.key, t.body, '', 'B'::"char") AS outcome +FROM (VALUES + ('plain key', 'anser_rf_3' , '\x616263'::bytea), + ('key containing a newline', E'rf:a\nb' , '\x616263'::bytea), + ('key that looks like a header', 'anser3 P B 0000000000', '\x616263'::bytea), + ('key with spaces and quotes', 'rf:"a b".c' , '\x616263'::bytea), + ('key at the 63-byte limit', repeat('k', 63) , '\x616263'::bytea), + ('empty key', '' , '\x616263'::bytea), + ('empty body', 'anser_rf_3' , '\x'::bytea), + ('body of a single NUL', 'anser_rf_3' , '\x00'::bytea), + ('body of framing-hostile bytes', 'anser_rf_3' , '\x00ff0a0d5c22'::bytea), + ('1 KB body', 'anser_rf_3' , decode(repeat('00ff', 512), 'hex'))) + AS t(what, key, body); + what | outcome +-------------------------------+--------- + plain key | ok + key containing a newline | ok + key that looks like a header | ok + key with spaces and quotes | ok + key at the 63-byte limit | ok + empty key | ok + empty body | ok + body of a single NUL | ok + body of framing-hostile bytes | ok + 1 KB body | ok +(10 rows) + +-- The QD -> QE checksum. The value itself does not matter; which inputs +-- change it does. Every routing field and the key are always covered, and the +-- body only for a payload type that asks for it -- the fifth column is that +-- deliberate exemption, not an oversight. +SELECT + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 2, 0, 'k', '\x01') AS condition_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 1, 'k', '\x01') AS flags_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'j', '\x01') AS key_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'k', '\x02') AS bloom_body_covered, + anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') + = anser_test_push_crc('-'::"char", 1, 0, 'k', '\x02') AS other_body_exempt, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') AS type_covered; + condition_covered | flags_covered | key_covered | bloom_body_covered | other_body_exempt | type_covered +-------------------+---------------+-------------+--------------------+-------------------+-------------- + t | t | t | t | t | t +(1 row) + DROP EXTENSION anser_test; diff --git a/gpcontrib/anser/include/anserbloom.h b/gpcontrib/anser/include/anserbloom.h index b6b42703906..24c7eaba7f6 100644 --- a/gpcontrib/anser/include/anserbloom.h +++ b/gpcontrib/anser/include/anserbloom.h @@ -55,6 +55,7 @@ extern AnserBloomFilterProduceState *ExecInitAnserBloomFilterProduce( uint32 total_parts); extern void ExecAnserBloomFilterProduceAddDatum(AnserBloomFilterProduceState *state, Datum value, bool isnull); +extern bool ExecAnserBloomFilterProduceHasFilter(AnserBloomFilterProduceState *state); extern bool ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state); extern bool ExecAnserBloomFilterProduceCancel(AnserBloomFilterProduceState *state); extern void ExecEndAnserBloomFilterProduce(AnserBloomFilterProduceState *state); diff --git a/gpcontrib/anser/include/anserfilter.h b/gpcontrib/anser/include/anserfilter.h index 06bd832798e..7e50e25c042 100644 --- a/gpcontrib/anser/include/anserfilter.h +++ b/gpcontrib/anser/include/anserfilter.h @@ -52,7 +52,57 @@ typedef struct AnserBloomPartHeader uint32 total_parts; } AnserBloomPartHeader; +/* + * When a bloom filter stops being worth building, or sending once built. + * Three checks, at the three points where new information arrives. + * + * ANSER_BLOOM_MIN_BITS_PER_KEY -- a floor on the *planned* density: bitset bits + * divided by the number of distinct keys we expect. Below it no amount of care + * in building the filter helps, so the filter is not built and, at plan time, + * the nodes are not even injected. At 4 bits/key the optimal hash count is 3 + * and the false positive rate is already ~15%; below that it collapses (2 + * bits/key is ~40%). + * + * ANSER_BLOOM_MAX_FPR -- the check on the *realized* filter, at publish time. + * It catches what a row estimate cannot: the estimate was too low, so the + * filter saturated anyway. This one is expressed as a false positive rate + * rather than a fill fraction on purpose. FPR is fill^k, so a single fill + * limit is not a single quality bar: 80% full is a 51% FPR at k=3 but only + * 10.7% at k=10, and a rule that cancelled the latter would be throwing away a + * filter that eliminates nine probe rows in ten. The producer has the filter + * and therefore its k, so it can just ask. + * + * ANSER_BLOOM_MERGED_MAX_FILL -- the same question asked by the coordinator of + * the merged payload, which is the only place it can be asked about what + * consumers will actually receive. It has to fall back on fill, because k is + * derived from plan parameters and is not carried in the serialized part. It + * is therefore set where even k=10 is past saving (0.95^10 = 60% FPR), so that + * it only ever rejects the hopeless. Putting k in AnserBloomPartHeader would + * let this use the FPR too. + * + * All three numbers are first cuts. Calibrating them is the "stop building + * filters that add nothing" part of the bloom-performance work; the debug trace + * logs fill and FPR at every decision point so that study has data. + */ +#define ANSER_BLOOM_MIN_BITS_PER_KEY 4.0 +#define ANSER_BLOOM_MAX_FPR 0.50 +#define ANSER_BLOOM_MERGED_MAX_FILL 0.95 + +/* + * Is a serialized part (or a merged accumulator of them) still worth + * delivering? False when too many of its bits are set for it to reject + * anything useful. Works on the wire form, so the coordinator can ask this of + * a merged payload without rebuilding a filter. + */ +extern bool AnserBloomPartWorthSending(const void *payload, Size payload_len); + extern uint64 AnserBloomSeed(const char *condition_key); + +/* + * Build an empty filter, or return NULL when AnserBloomShapeFor says it is not + * worth building. A NULL return is not an error: the producer turns it into an + * immediate cancel, so consumers stop waiting instead of timing out. + */ extern bloom_filter *AnserBloomCreate(int64 total_elems, Size max_payload_bytes, uint64 seed); diff --git a/gpcontrib/anser/include/anserplan.h b/gpcontrib/anser/include/anserplan.h index 3f4556adb1f..bbfab22f066 100644 --- a/gpcontrib/anser/include/anserplan.h +++ b/gpcontrib/anser/include/anserplan.h @@ -67,4 +67,15 @@ extern CustomScan *AnserBuildBloomConsumerScan(Plan *child, AttrNumber key_attno Size max_payload_bytes, int64 planned_bytes); +/* + * Bloom sizing for one join, from its estimated build cardinality. False means + * no filter is worth injecting -- see the density rule in anserfilter.h. + * + * Exposed rather than static because it is the cheapest of Anser's give-up + * decisions and therefore the one most worth testing directly at its boundary + * (anser_test.c); nothing but the injection pass and the tests should call it. + */ +extern bool AnserRuntimeFilterSize(double est_rows, int64 *total_elems, + int64 *max_payload, int64 *planned_bytes); + #endif /* ANSERPLAN_H */ diff --git a/gpcontrib/anser/include/ansersideband.h b/gpcontrib/anser/include/ansersideband.h index e671b57d3a4..2cbea9336cd 100644 --- a/gpcontrib/anser/include/ansersideband.h +++ b/gpcontrib/anser/include/ansersideband.h @@ -33,6 +33,19 @@ * merged filter travels as raw binary. That matters: the merged payload is * sent once per consumer, while each part is sent once. * + * Both directions carry a CRC32C. Not because the transport is lossy -- it is + * TCP, or a Unix socket; the UDP interconnect is a different channel entirely + * -- but because some payloads fail asymmetrically and TCP's 16-bit checksum is + * thin cover for a megabyte. The header and condition key are always covered; + * whether the body is covered too is the payload type's decision + * (AnserPayloadOps.checksum_body, see anserpayload.h), because that is the part + * that costs 0.05 ms/MB and only some payloads can turn corruption into a wrong + * answer. The QE -> QD checksum covers the raw pre-base64 bytes, so where it + * applies it validates the decode as well. + * + * Neither direction branches on the payload type beyond consulting that one + * flag: everything else about what the bytes mean belongs in the registry. + * * IDENTIFICATION * gpcontrib/anser/include/ansersideband.h * @@ -42,6 +55,8 @@ #define ANSERSIDEBAND_H #include "anser.h" +#include "anserpayload.h" +#include "port/pg_crc32c.h" struct CdbDispatchResult; /* #include "cdb/cdbdispatchresult.h" */ struct pgNotify; /* #include "libpq-fe.h" */ @@ -53,31 +68,149 @@ struct pgNotify; /* #include "libpq-fe.h" */ #define ANSER_NOTIFY_CHANNEL "anser_rf" /* First token of every QE -> QD payload; bump when the format changes. */ -#define ANSER_WIRE_TAG "anser1" +#define ANSER_WIRE_TAG "anser3" -/* Message kinds (QE -> QD). */ +/* + * Message kinds (QE -> QD): what the message does. What it carries is a + * separate field, the payload type -- see anserpayload.h. + */ #define ANSER_WIRE_KIND_PART 'P' /* a producer's serialized part */ #define ANSER_WIRE_KIND_SUBSCRIBE 'S' /* a consumer registering interest */ /* Flag bits, shared by both directions. */ #define ANSER_WIRE_F_CANCELLED 0x0001 +/* + * QE -> QD header: fixed width, so the key and body start at known offsets and + * nothing about their contents can affect parsing. + * + * anser3 K T SSSSSSSSSS CCCCCCCCCC DDDDDDDDDD PPPPPPPPPP TTTTTTTTTT FFFF KKKK BBBBBBBBBB XXXXXXXX + * ^tag ^ ^ session command condition part total ^ ^ bodylen crc + * | \ payload type flags keylen + * \ kind + * + * followed immediately by key bytes and body bytes. Every + * field is zero-padded (flags and CRC are hex, the rest decimal), so the header + * is pure ASCII of a constant length and holds no delimiter that payload bytes + * could imitate. The two lengths are then a cross-check: the total payload + * length must be exactly the header plus both, or the message is rejected. + * + * The signed session and command ids travel as uint32 and are cast back on + * receipt, which round-trips them exactly while keeping the width fixed -- + * "%010d" of a negative value is eleven characters, which would have made the + * header variable-width again. + * + * The previous format ended its header with a newline and found it with + * strchr(). That was correct only because the header could not itself contain + * a newline -- an invariant nothing enforced, and one that a key derived from + * relation names (quoted identifiers may contain anything) would have broken + * silently. + */ +#define ANSER_WIRE_HDR_FORMAT \ + ANSER_WIRE_TAG " %c %c %010u %010u %010u %010u %010u %04x %04u %010u %08x" + +/* + * Read format for the same header. The widths are what make the parse safe: + * every conversion stops after exactly its field, so no value can consume the + * separator or run into the key bytes that follow the header. + */ +#define ANSER_WIRE_HDR_SCANF \ + ANSER_WIRE_TAG " %c %c %10u %10u %10u %10u %10u %4x %4u %10u %8x" + +/* 6 + the 11 fields above, each preceded by its separating space. */ +#define ANSER_WIRE_HDR_LEN (6 + (1 + 1) + (1 + 1) + (1 + 10) + (1 + 10) \ + + (1 + 10) + (1 + 10) + (1 + 10) + (1 + 4) \ + + (1 + 4) + (1 + 10) + (1 + 8)) + +/* Offset of the 8-hex-digit CRC; the checksum covers everything before it. */ +#define ANSER_WIRE_CRC_OFFSET (ANSER_WIRE_HDR_LEN - 8) + +/* + * Largest key and body a header can describe. Checked before formatting so a + * field can never overflow its width and shift every field after it. + */ +#define ANSER_WIRE_MAX_KEYLEN 9999 +#define ANSER_WIRE_MAX_BODYLEN 999999999 + +/* + * The codec. + * + * Four functions, exported rather than static because together they *are* the + * protocol: the two directions have to agree byte for byte about offsets and + * about what the checksum covers, and that agreement is only auditable -- and + * only testable -- if both halves are reachable by name. sql/anser_test.sql + * drives them directly. + * + * They are implemented on the side that writes: AnserWireFormat in + * ansersideband.c (only a QE formats), AnserWireParse and AnserWireCheckCrc in + * anserdispatch.c (only the QD parses). + * + * AnserWireFormat returns a palloc'd NUL-terminated string. AnserWireParse + * fills 'out' and returns false for anything it will not vouch for; its + * contract and the checks it makes are documented at the definition. + * AnserWireCheckCrc takes the *decoded* body, or NULL/0 when there is none. + */ +typedef struct AnserWireMsg +{ + const char *wire; /* the message itself; the CRC covers its head */ + char kind; + char payload_type; /* resolved against the registry by the caller */ + AnserChannelKey key; + int key_len; + int part_index; + int total_parts; + int flags; + const char *body; /* base64, not NUL-terminated */ + int body_len; + uint32 crc; /* of the header, key and decoded body */ +} AnserWireMsg; + +extern char *AnserWireFormat(const AnserChannelKey *channel_key, char kind, + char payload_type, uint32 part_index, + uint32 total_parts, int flags, + const void *payload, Size payload_len); +extern bool AnserWireParse(const char *msg, AnserWireMsg *out); +extern bool AnserWireCheckCrc(const AnserWireMsg *msg, const void *body, + Size body_len); + +/* + * QD -> QE push: raw binary, written with pqPutInt/pqPutnchar. + * + * payload_type | crc32c | condition_id | flags | keylen | key | bodylen | body + * + * with each field a 4-byte integer except the key and body. The CRC covers all + * of the other fields in that order, the integers in network byte order, so it + * validates the routing information as well as the payload; the body is + * included only when the payload type says so. Both ends compute it through + * this one function so the two cannot drift apart. It is implemented in + * ansersideband.c, beside the reader that verifies it. + */ +extern pg_crc32c AnserWirePushCrc(char payload_type, uint32 condition_id, + uint32 flags, const char *key, int keylen, + const void *body, int bodylen); + /* * QE side (ansersideband.c). * * AnserSidebandPublish is fire-and-forget: unlike the libpq transport it does - * not wait for the coordinator to acknowledge the part. + * not wait for the coordinator to acknowledge the part. 'payload_type' is + * stamped on the wire even when cancelled, since it is what tells the + * coordinator how to fold the channel's parts. * * AnserSidebandConsumeWait blocks on this backend's own dispatch socket until * the merged payload arrives, the channel is cancelled, or timeout_ms elapses. * On success *payload is palloc'd in the caller's context. A false return - * always means "run unfiltered", never an error. + * always means "run unfiltered", never an error -- including when what arrived + * is not of 'payload_type', which would mean two different kinds of information + * had collided on one channel. */ extern bool AnserSidebandPublish(const AnserChannelKey *channel_key, + char payload_type, uint32 part_index, uint32 total_parts, const void *payload, Size payload_len, bool cancelled); extern bool AnserSidebandConsumeWait(const AnserChannelKey *channel_key, + char payload_type, void **payload, Size *payload_len, bool *cancelled, long timeout_ms); @@ -88,15 +221,19 @@ extern bool AnserSidebandConsumeWait(const AnserChannelKey *channel_key, * arriving parts and pushes the merged payload to subscribers. The Local * variants serve producers and consumers running on the coordinator itself, * which have no dispatch connection to themselves and so operate on the same - * per-query channel table directly. + * per-query channel table directly. They take the same payload type for the + * same reasons, so that a coordinator-local producer and a segment producer are + * interchangeable on one channel. */ extern bool AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, struct pgNotify *notify); extern bool AnserDispatchLocalPublish(const AnserChannelKey *channel_key, + char payload_type, uint32 part_index, uint32 total_parts, const void *payload, Size payload_len, bool cancelled); extern bool AnserDispatchLocalConsume(const AnserChannelKey *channel_key, + char payload_type, void **payload, Size *payload_len, bool *cancelled); diff --git a/gpcontrib/anser/sql/anser_test.sql b/gpcontrib/anser/sql/anser_test.sql index bd2c3417f03..0c6e3585c89 100644 --- a/gpcontrib/anser/sql/anser_test.sql +++ b/gpcontrib/anser/sql/anser_test.sql @@ -17,4 +17,168 @@ SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; -- the coordinator side merge it, then receive and query the filter. SELECT anser_test_node_roundtrip(168); +-- --------------------------------------------------------------------------- +-- Sizing, and the decision not to bother. +-- +-- Anser can decline to build a filter at four points, and these cases walk the +-- boundary of each. Declining is always safe: it costs the query its filter, +-- never its correctness. The payload cap is pinned here because every size +-- below follows from it. +-- --------------------------------------------------------------------------- +SET anser.max_info_size = 68157440; + +-- 1. The planner's gate -- the only one that costs nothing, since nothing is +-- injected and no consumer is left waiting. A 64 MB bitset is 536870912 bits, +-- so 134217728 estimated keys is exactly 4 bits/key and one more key is not. +-- A zero or negative estimate is treated as one key, not as "no filter". +SELECT t.est_rows, s.injected, s.total_elems, s.planned_bytes +FROM (VALUES (0), (-5), (1), (1000), (1000000), (10000000), + (134217728), (134217729), (150000000), (1000000000)) + AS t(est_rows), + LATERAL anser_test_rf_size(t.est_rows) AS s; + +-- 2. Filter construction. bloom_create floors every bitset at 1 MB and that +-- floor overrides the work_mem cap, so a cap below "1 MB + header" can only +-- yield a filter too large to send -- which is why a flat 1 MB cap is refused +-- while 1048592 (the floor plus exactly one header) is accepted and serializes +-- to precisely the cap. fits_cap is the invariant that must never be false. +SELECT t.what, t.elems, t.cap, s.built, s.bits, s.serialized, + round(s.bits_per_key::numeric, 3) AS bits_per_key, + (NOT s.built OR s.serialized <= t.cap) AS fits_cap +FROM (VALUES + ('no keys', 0::bigint, 1048640::bigint), + ('negative keys', -1, 1048640), + ('cap equals the header', 32, 16), + ('cap one byte over the header', 32, 17), + ('cap is 1 MB, no header room', 32, 1048576), + ('cap is 1 MB plus a header', 32, 1048592), + ('cap one byte short of that', 32, 1048591), + ('a single key', 1, 1048640), + ('exactly 4 bits/key', 2097152, 1048640), + ('one key too many', 2097153, 1048640), + ('exactly 4 bits/key at 2 MB', 4194304, 2097216), + ('one key too many at 2 MB', 4194305, 2097216), + ('a billion keys', 1000000000, 68157440)) + AS t(what, elems, cap), + LATERAL anser_test_bloom_shape(t.elems, t.cap) AS s; + +-- 3. The coordinator's gate on a merged payload. The fill is fixed exactly by +-- setting whole bytes rather than by inserting keys, which would be both slow +-- and only statistically precise. The rule is "reject above 95%", so a payload +-- sitting exactly on the limit is still delivered. Malformed framing is not +-- worth delivering either, since it cannot be a filter at all. +SELECT t.what, t.bitset_bytes, t.bytes_set, t.damage, + anser_test_worth_delivering(t.bitset_bytes, t.bytes_set, t.damage) + AS worth_delivering +FROM (VALUES + ('empty', 1000, 0, ''), + ('half full', 1000, 500, ''), + ('just under the limit', 1000, 949, ''), + ('exactly on the limit', 1000, 950, ''), + ('just over the limit', 1000, 951, ''), + ('completely full', 1000, 1000, ''), + ('no bitset at all', 0, 0, ''), + ('corrupt magic', 1000, 0, 'magic'), + ('corrupt version', 1000, 0, 'version'), + ('zero part count', 1000, 0, 'parts'), + ('null payload', 1000, 0, 'null')) + AS t(what, bitset_bytes, bytes_set, damage); + +-- 4. The producer driven end to end on the coordinator-local path: build, +-- insert, publish, consume. "no-filter:cancelled" is the case that matters +-- most -- construction declined, so the cancel goes out before the build side +-- is scanned and no consumer waits for its timeout. The last row is the one +-- the planner cannot predict: a filter sized for 32 keys that receives three +-- million, which saturates and so publishes a cancel instead of a payload. +SELECT t.what, + anser_test_producer_decision(t.elems, t.cap, t.n_keys) AS decision +FROM (VALUES + ('sparse filter', 32::bigint, 1048640::bigint, 100), + ('cap with no header room', 32, 1048576, 100), + ('no keys declared', 0, 1048640, 0), + ('one key past the floor', 2097153, 1048640, 0), + ('exactly 4 bits/key', 2097152, 1048640, 100000), + ('estimate of 32, three million inserted', 32, 1048640, 3000000)) + AS t(what, elems, cap, n_keys); + +-- --------------------------------------------------------------------------- +-- The wire protocol. +-- +-- Not exhaustive; a fuzzer would be the right tool for that. These cover the +-- properties that are cheap to check and expensive to lose: a fixed-width +-- header with the fields where they are documented, a payload that cannot be +-- mistaken for framing, a length cross-check that rejects a message which does +-- not add up, and a checksum that rejects one altered byte anywhere it covers. +-- --------------------------------------------------------------------------- + +-- The bytes, exactly. Session/command/condition are 42/7/3 and part is 1 of 3, +-- so these are golden values: any change to the layout, the field widths or +-- what the checksum covers shows up here as a diff. Note the third row -- a +-- cancelled message carries no body even though one was passed, and its +-- checksum must be computed over what is actually sent. +-- +-- That the message comes back as text is itself the check that it is NUL-free, +-- which a NOTIFY payload has to be: the fourth row's body is 00 ff 00. +SELECT t.what, length(m.msg) AS len, m.msg +FROM (VALUES + ('part with a 3-byte body', 'P', 'B', 0, '\x616263'::bytea), + ('subscription, no body', 'S', '-', 0, '\x'::bytea), + ('cancelled: body suppressed', 'P', 'B', 1, '\x616263'::bytea), + ('body of NUL and 0xff bytes', 'P', 'B', 0, '\x00ff00'::bytea)) + AS t(what, kind, ptype, flags, body), + LATERAL (SELECT anser_test_wire_format(t.kind::"char", t.ptype::"char", + t.flags, 'anser_rf_3', t.body) + AS msg) m; + +-- Alter exactly one byte (or one length) and see what the reader does. +-- +-- The split matters: "malformed" is rejected by framing, before a byte is +-- allocated or a channel created, while "checksum-mismatch" got past framing +-- and was caught by the CRC. Corrupting the kind byte is the interesting one +-- -- the message stays perfectly well-formed, and only the checksum notices, +-- which is why the checksum covers the header and not just the payload. +SELECT t.tamper, + anser_test_wire_roundtrip('anser_rf_3', '\x616263'::bytea, t.tamper, + 'B'::"char") AS outcome +FROM (VALUES (''), ('truncate'), ('append'), ('bodylen'), ('tag'), ('kind'), + ('type'), ('crc'), ('key'), ('body')) AS t(tamper); + +-- Keys and bodies that must survive the trip unchanged. "ok" means every +-- field and the decoded body came back identical, so each of these is a case +-- where a naive framing would have gone wrong: the key travels as raw bytes +-- and is taken by length, which is what lets it contain a newline, a space, or +-- the protocol's own tag without any escaping. +SELECT t.what, + anser_test_wire_roundtrip(t.key, t.body, '', 'B'::"char") AS outcome +FROM (VALUES + ('plain key', 'anser_rf_3' , '\x616263'::bytea), + ('key containing a newline', E'rf:a\nb' , '\x616263'::bytea), + ('key that looks like a header', 'anser3 P B 0000000000', '\x616263'::bytea), + ('key with spaces and quotes', 'rf:"a b".c' , '\x616263'::bytea), + ('key at the 63-byte limit', repeat('k', 63) , '\x616263'::bytea), + ('empty key', '' , '\x616263'::bytea), + ('empty body', 'anser_rf_3' , '\x'::bytea), + ('body of a single NUL', 'anser_rf_3' , '\x00'::bytea), + ('body of framing-hostile bytes', 'anser_rf_3' , '\x00ff0a0d5c22'::bytea), + ('1 KB body', 'anser_rf_3' , decode(repeat('00ff', 512), 'hex'))) + AS t(what, key, body); + +-- The QD -> QE checksum. The value itself does not matter; which inputs +-- change it does. Every routing field and the key are always covered, and the +-- body only for a payload type that asks for it -- the fifth column is that +-- deliberate exemption, not an oversight. +SELECT + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 2, 0, 'k', '\x01') AS condition_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 1, 'k', '\x01') AS flags_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'j', '\x01') AS key_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'k', '\x02') AS bloom_body_covered, + anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') + = anser_test_push_crc('-'::"char", 1, 0, 'k', '\x02') AS other_body_exempt, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') AS type_covered; + DROP EXTENSION anser_test; diff --git a/gpcontrib/anser/src/anser_test.c b/gpcontrib/anser/src/anser_test.c index 552fc684b97..c1eccc586de 100644 --- a/gpcontrib/anser/src/anser_test.c +++ b/gpcontrib/anser/src/anser_test.c @@ -27,12 +27,17 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "anser.h" #include "anserbloom.h" #include "anserfilter.h" +#include "anserpayload.h" +#include "anserplan.h" #include "ansersideband.h" #include "cdb/cdbvars.h" +#include "common/base64.h" #include "fmgr.h" +#include "funcapi.h" #include "lib/bloomfilter.h" #include "miscadmin.h" #include "utils/builtins.h" @@ -45,7 +50,14 @@ * parameters in the node rather than on the wire). */ #define ANSER_TEST_ELEMS 32 -#define ANSER_TEST_MAX_PAYLOAD (1024 * 1024) + +/* + * bloom_create floors every bitset at 1 MB, so a payload cap must be 1 MB + * *plus* room for the serialized-part header -- the same allowance the planner + * makes (ANSER_RF_HEADER_ROOM). Passing a flat 1 MB asks AnserBloomCreate for + * a filter that cannot fit the cap it was given, and it now declines. + */ +#define ANSER_TEST_MAX_PAYLOAD (1024 * 1024 + 64) PG_FUNCTION_INFO_V1(anser_test_bloom_roundtrip); PG_FUNCTION_INFO_V1(anser_test_bloom_fold_inplace); @@ -69,7 +81,7 @@ anser_test_bloom_roundtrip(PG_FUNCTION_ARGS) uint32 total_parts = 0; bool lacks; - filter = AnserBloomCreate(32, 1024 * 1024, seed); + filter = AnserBloomCreate(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, seed); if (filter == NULL) PG_RETURN_BOOL(false); @@ -127,8 +139,8 @@ anser_test_bloom_fold_inplace(PG_FUNCTION_ARGS) bool mismatch_rejected; /* Two same-parameter parts: acc is the running merged part, part folds in. */ - left = AnserBloomCreate(32, 1024 * 1024, seed); - right = AnserBloomCreate(32, 1024 * 1024, seed); + left = AnserBloomCreate(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, seed); + right = AnserBloomCreate(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, seed); if (left == NULL || right == NULL) PG_RETURN_BOOL(false); bloom_add_element(left, (unsigned char *) &left_value, sizeof(Datum)); @@ -330,3 +342,423 @@ anser_test_node_roundtrip(PG_FUNCTION_ARGS) PG_RETURN_BOOL(ok); } + +/* + * --------------------------------------------------------------------------- + * Sizing and give-up decisions. + * + * These four take their inputs as arguments and return what Anser decided, so + * the case tables live in sql/anser_test.sql where they are readable and the + * expected output records real sizes rather than a bare "ok". Between them + * they cover each of the four points where Anser can decide not to bother: + * the planner, filter construction, publication, and the merged result. + * --------------------------------------------------------------------------- + */ + +PG_FUNCTION_INFO_V1(anser_test_rf_size); +PG_FUNCTION_INFO_V1(anser_test_bloom_shape); +PG_FUNCTION_INFO_V1(anser_test_worth_delivering); +PG_FUNCTION_INFO_V1(anser_test_producer_decision); + +/* + * The planner gate: what AnserRuntimeFilterSize decides for an estimated build + * cardinality. NULL means no filter would be injected at all. + */ +Datum +anser_test_rf_size(PG_FUNCTION_ARGS) +{ + double est_rows = PG_GETARG_FLOAT8(0); + int64 total_elems = 0; + int64 max_payload = 0; + int64 planned_bytes = 0; + bool injected; + Datum values[4]; + bool nulls[4] = {false, false, false, false}; + TupleDesc tupdesc; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "anser_test_rf_size: expected a composite return type"); + tupdesc = BlessTupleDesc(tupdesc); + + injected = AnserRuntimeFilterSize(est_rows, &total_elems, &max_payload, + &planned_bytes); + + values[0] = BoolGetDatum(injected); + values[1] = Int64GetDatum(total_elems); + values[2] = Int64GetDatum(max_payload); + values[3] = Int64GetDatum(planned_bytes); + nulls[1] = nulls[2] = nulls[3] = !injected; + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * Filter construction: what AnserBloomCreate makes of (total_elems, cap). + * NULL means it declined -- either the smallest possible filter does not fit + * the cap, or there are too many keys for it to be worth building. + * + * Returns the realized sizes so the expected output pins down bloom_create's + * actual behaviour (its 1 MB floor, its power-of-two rounding) and not merely + * our verdict on it. + */ +Datum +anser_test_bloom_shape(PG_FUNCTION_ARGS) +{ + int64 total_elems = PG_GETARG_INT64(0); + int64 cap = PG_GETARG_INT64(1); + bloom_filter *filter; + Datum values[4]; + bool nulls[4] = {false, false, false, false}; + TupleDesc tupdesc; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "anser_test_bloom_shape: expected a composite return type"); + tupdesc = BlessTupleDesc(tupdesc); + + /* A negative cap would be a Size wraparound; the SQL side never sends one. */ + if (cap < 0) + elog(ERROR, "anser_test_bloom_shape: negative cap"); + + filter = AnserBloomCreate(total_elems, (Size) cap, AnserBloomSeed("shape")); + + values[0] = BoolGetDatum(filter != NULL); + nulls[1] = nulls[2] = nulls[3] = (filter == NULL); + if (filter != NULL) + { + values[1] = Int64GetDatum((int64) bloom_total_bits(filter)); + values[2] = Int64GetDatum((int64) AnserBloomSerializedSize(filter)); + values[3] = Float8GetDatum((double) bloom_total_bits(filter) / + (double) total_elems); + bloom_free(filter); + } + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * The coordinator's gate, on a synthetic part. + * + * Builds a part whose bitset has 'bytes_set' of its 'bitset_bytes' bytes fully + * set, which pins the fill fraction exactly -- reaching a given fill by + * inserting keys would be both slow and only statistically precise. 'damage' + * corrupts the framing instead: 'magic', 'version', 'parts' or 'null'. + */ +Datum +anser_test_worth_delivering(PG_FUNCTION_ARGS) +{ + int32 bitset_bytes = PG_GETARG_INT32(0); + int32 bytes_set = PG_GETARG_INT32(1); + const char *damage = text_to_cstring(PG_GETARG_TEXT_PP(2)); + AnserBloomPartHeader *header; + char *payload; + Size payload_len; + + if (strcmp(damage, "null") == 0) + PG_RETURN_BOOL(AnserBloomPartWorthSending(NULL, 1024)); + + if (bitset_bytes < 0 || bytes_set < 0 || bytes_set > bitset_bytes) + elog(ERROR, "anser_test_worth_delivering: bad bitset arguments"); + + payload_len = sizeof(AnserBloomPartHeader) + (Size) bitset_bytes; + payload = palloc0(payload_len); + header = (AnserBloomPartHeader *) payload; + header->magic = ANSER_BLOOM_PART_MAGIC; + header->version = ANSER_BLOOM_PART_VERSION; + header->part_index = 0; + header->total_parts = 1; + + if (strcmp(damage, "magic") == 0) + header->magic = ANSER_BLOOM_PART_MAGIC + 1; + else if (strcmp(damage, "version") == 0) + header->version = ANSER_BLOOM_PART_VERSION + 1; + else if (strcmp(damage, "parts") == 0) + header->total_parts = 0; + else if (damage[0] != '\0') + elog(ERROR, "anser_test_worth_delivering: unknown damage \"%s\"", damage); + + if (bytes_set > 0) + memset(payload + sizeof(AnserBloomPartHeader), 0xff, (Size) bytes_set); + + PG_RETURN_BOOL(AnserBloomPartWorthSending(payload, payload_len)); +} + +/* + * Producer end to end, on the coordinator-local path: build a filter for + * (total_elems, cap), insert 'n_keys' distinct keys, publish, then consume. + * + * Returns ":", where construction is built/no-filter + * and delivery is delivered/cancelled/missing. Every combination that can + * occur says something different: + * + * built:delivered the normal case + * built:cancelled the filter saturated, so publication became a cancel + * no-filter:cancelled construction declined, cancelled before any scan + * + * Each call takes a fresh condition_id, since several rows of one query share a + * session and command counter and would otherwise collide on one channel. + */ +Datum +anser_test_producer_decision(PG_FUNCTION_ARGS) +{ + static uint32 next_condition_id = 1000; + + int64 total_elems = PG_GETARG_INT64(0); + int64 cap = PG_GETARG_INT64(1); + int32 n_keys = PG_GETARG_INT32(2); + AnserChannelKey key; + AnserBloomFilterProduceState *producer; + const char *construction; + const char *delivery; + void *payload = NULL; + Size payload_len = 0; + bool cancelled = false; + int32 i; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = gp_session_id; + key.gp_command_count = gp_command_count; + key.condition_id = next_condition_id++; + snprintf(key.condition_key, ANSER_CONDITION_KEY_SIZE, "anser_rf_%u", + key.condition_id); + + producer = ExecInitAnserBloomFilterProduce(&key, total_elems, (Size) cap, + 0, 1); + if (producer == NULL) + PG_RETURN_TEXT_P(cstring_to_text("no-producer:missing")); + + construction = ExecAnserBloomFilterProduceHasFilter(producer) + ? "built" : "no-filter"; + + for (i = 0; i < n_keys; i++) + { + Datum value = Int32GetDatum(i); + + ExecAnserBloomFilterProduceAddDatum(producer, value, false); + } + + (void) ExecAnserBloomFilterProducePublish(producer); + ExecEndAnserBloomFilterProduce(producer); + + if (!AnserDispatchLocalConsume(&key, ANSER_PAYLOAD_BLOOM, &payload, + &payload_len, &cancelled)) + delivery = cancelled ? "cancelled" : "missing"; + else + delivery = "delivered"; + + if (payload != NULL) + pfree(payload); + + PG_RETURN_TEXT_P(cstring_to_text(psprintf("%s:%s", construction, delivery))); +} + +/* + * --------------------------------------------------------------------------- + * The wire protocol. + * + * Not exhaustive -- a fuzzer would be the right tool for that, and pg_regress + * is not one. What these cover is the part of the format that is easy to get + * wrong and expensive to get wrong: that the header is a fixed width with the + * fields where they are documented, that a payload cannot be mistaken for + * framing (the reason the newline-delimited format was replaced), that the + * length cross-check rejects a message that does not add up, and that the + * checksum rejects a single altered byte anywhere it is supposed to cover -- + * and does not reject one where it deliberately does not. + * --------------------------------------------------------------------------- + */ + +PG_FUNCTION_INFO_V1(anser_test_wire_format); +PG_FUNCTION_INFO_V1(anser_test_wire_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_push_crc); + +/* Fixed channel coordinates, so the golden headers below are stable. */ +#define ANSER_TEST_WIRE_SESSION 42 +#define ANSER_TEST_WIRE_COMMAND 7 +#define ANSER_TEST_WIRE_CONDITION 3 +#define ANSER_TEST_WIRE_PART 1 +#define ANSER_TEST_WIRE_TOTAL 3 + +static void +anser_test_wire_key(AnserChannelKey *key, const char *condition_key) +{ + MemSet(key, 0, sizeof(*key)); + key->gp_session_id = ANSER_TEST_WIRE_SESSION; + key->gp_command_count = ANSER_TEST_WIRE_COMMAND; + key->condition_id = ANSER_TEST_WIRE_CONDITION; + strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); +} + +/* + * The formatted message, verbatim. + * + * Returning it as text is itself a check: a NOTIFY payload travels through + * pq_sendstring and so must be free of NUL bytes, and a text Datum cannot + * carry one -- which is why the body is base64 even though the header is not. + * Pass a body containing NULs and the length in the expected output proves it. + */ +Datum +anser_test_wire_format(PG_FUNCTION_ARGS) +{ + char kind = PG_GETARG_CHAR(0); + char payload_type = PG_GETARG_CHAR(1); + int32 flags = PG_GETARG_INT32(2); + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); + bytea *body = PG_GETARG_BYTEA_PP(4); + AnserChannelKey key; + char *msg; + + anser_test_wire_key(&key, condition_key); + msg = AnserWireFormat(&key, kind, payload_type, ANSER_TEST_WIRE_PART, + ANSER_TEST_WIRE_TOTAL, flags, + VARDATA_ANY(body), VARSIZE_ANY_EXHDR(body)); + + PG_RETURN_TEXT_P(cstring_to_text(msg)); +} + +/* + * Format a message, optionally alter one byte of it, then parse it back and + * report what the reader made of it. + * + * The offsets touched are derived from the format macros, never hardcoded, so + * this keeps working if a field is added. Outcomes mirror what the notify + * handler does with each: "malformed" is rejected by framing before anything + * is allocated, "unknown-type" has no registry entry, "checksum-mismatch" is + * well-framed but altered, and "ok" means every field and the decoded body + * came back exactly as they went in. + */ +Datum +anser_test_wire_roundtrip(PG_FUNCTION_ARGS) +{ + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(0)); + bytea *body = PG_GETARG_BYTEA_PP(1); + char *tamper = text_to_cstring(PG_GETARG_TEXT_PP(2)); + char payload_type = PG_GETARG_CHAR(3); + AnserChannelKey key; + AnserWireMsg parsed; + char *msg; + Size msg_len; + const char *raw_body = VARDATA_ANY(body); + int raw_len = VARSIZE_ANY_EXHDR(body); + char *decoded = NULL; + int decoded_len = 0; + + anser_test_wire_key(&key, condition_key); + msg = AnserWireFormat(&key, ANSER_WIRE_KIND_PART, payload_type, + ANSER_TEST_WIRE_PART, ANSER_TEST_WIRE_TOTAL, 0, + raw_len > 0 ? raw_body : NULL, (Size) raw_len); + msg_len = strlen(msg); + + /* One byte, or one length, altered -- see the case table in the test. */ + if (strcmp(tamper, "") == 0) + /* no damage */ ; + else if (strcmp(tamper, "truncate") == 0) + msg[msg_len - 1] = '\0'; + else if (strcmp(tamper, "append") == 0) + { + char *longer = palloc(msg_len + 2); + + memcpy(longer, msg, msg_len); + longer[msg_len] = 'x'; + longer[msg_len + 1] = '\0'; + msg = longer; + } + else if (strcmp(tamper, "bodylen") == 0) + { + /* Last digit of the bodylen field: it ends one space before the CRC. */ + char *digit = msg + ANSER_WIRE_CRC_OFFSET - 2; + + *digit = (*digit == '9') ? '8' : (char) (*digit + 1); + } + else if (strcmp(tamper, "tag") == 0) + msg[0] = 'x'; + else if (strcmp(tamper, "kind") == 0) + msg[sizeof(ANSER_WIRE_TAG)] = ANSER_WIRE_KIND_SUBSCRIBE; + else if (strcmp(tamper, "type") == 0) + msg[sizeof(ANSER_WIRE_TAG) + 2] = 'Z'; + else if (strcmp(tamper, "crc") == 0) + { + char *digit = msg + ANSER_WIRE_HDR_LEN - 1; + + *digit = (*digit == '0') ? '1' : '0'; + } + else if (strcmp(tamper, "key") == 0) + { + char *first = msg + ANSER_WIRE_HDR_LEN; + + *first = (*first == 'a') ? 'b' : 'a'; + } + else if (strcmp(tamper, "body") == 0) + { + /* Stay inside the base64 alphabet so this tests the CRC, not decoding. */ + char *first = msg + ANSER_WIRE_HDR_LEN + strlen(condition_key); + + *first = (*first == 'A') ? 'B' : 'A'; + } + else + elog(ERROR, "anser_test_wire_roundtrip: unknown tamper \"%s\"", tamper); + + if (!AnserWireParse(msg, &parsed)) + PG_RETURN_TEXT_P(cstring_to_text("malformed")); + + if (AnserPayloadLookup(parsed.payload_type) == NULL) + PG_RETURN_TEXT_P(cstring_to_text("unknown-type")); + + if (parsed.body_len > 0) + { + int maxlen = pg_b64_dec_len(parsed.body_len); + + decoded = palloc(maxlen); + decoded_len = pg_b64_decode(parsed.body, parsed.body_len, decoded, + maxlen); + if (decoded_len < 0) + PG_RETURN_TEXT_P(cstring_to_text("undecodable")); + } + + if (!AnserWireCheckCrc(&parsed, decoded, (Size) decoded_len)) + PG_RETURN_TEXT_P(cstring_to_text("checksum-mismatch")); + + /* Framed and vouched for: now every field must have survived the trip. */ + if (parsed.kind != ANSER_WIRE_KIND_PART) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: kind")); + if (parsed.payload_type != payload_type) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: payload_type")); + if (parsed.key.gp_session_id != ANSER_TEST_WIRE_SESSION || + parsed.key.gp_command_count != ANSER_TEST_WIRE_COMMAND || + parsed.key.condition_id != ANSER_TEST_WIRE_CONDITION) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: channel")); + if (parsed.part_index != ANSER_TEST_WIRE_PART || + parsed.total_parts != ANSER_TEST_WIRE_TOTAL) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: part")); + if (parsed.flags != 0) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: flags")); + if (strcmp(parsed.key.condition_key, condition_key) != 0) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: condition_key")); + if (decoded_len != raw_len || + (raw_len > 0 && memcmp(decoded, raw_body, raw_len) != 0)) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: body")); + + PG_RETURN_TEXT_P(cstring_to_text("ok")); +} + +/* + * The QD -> QE checksum, as an integer so the test can compare two of them. + * + * What matters is not the value but which inputs change it: every routing field + * and the key always, and the body only for a payload type that asks for its + * body to be covered. + */ +Datum +anser_test_push_crc(PG_FUNCTION_ARGS) +{ + char payload_type = PG_GETARG_CHAR(0); + int32 condition_id = PG_GETARG_INT32(1); + int32 flags = PG_GETARG_INT32(2); + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); + bytea *body = PG_GETARG_BYTEA_PP(4); + + PG_RETURN_INT64((int64) (uint32) + AnserWirePushCrc(payload_type, (uint32) condition_id, + (uint32) flags, condition_key, + (int) strlen(condition_key), + VARDATA_ANY(body), + (int) VARSIZE_ANY_EXHDR(body))); +} diff --git a/gpcontrib/anser/src/anserbloomconsume.c b/gpcontrib/anser/src/anserbloomconsume.c index 64271097c82..2da6dd5c01a 100644 --- a/gpcontrib/anser/src/anserbloomconsume.c +++ b/gpcontrib/anser/src/anserbloomconsume.c @@ -30,6 +30,7 @@ #include "anser.h" #include "anserbloom.h" #include "anserfilter.h" +#include "anserpayload.h" #include "ansersideband.h" #include "cdb/cdbvars.h" @@ -106,11 +107,12 @@ ExecAnserBloomFilterConsumeSideband(AnserBloomFilterConsumeState *state, bool got; if (Gp_role == GP_ROLE_EXECUTE) - got = AnserSidebandConsumeWait(&state->channel_key, &payload, - &payload_len, &cancelled, timeout_ms); + got = AnserSidebandConsumeWait(&state->channel_key, ANSER_PAYLOAD_BLOOM, + &payload, &payload_len, &cancelled, + timeout_ms); else - got = AnserDispatchLocalConsume(&state->channel_key, &payload, - &payload_len, &cancelled); + got = AnserDispatchLocalConsume(&state->channel_key, ANSER_PAYLOAD_BLOOM, + &payload, &payload_len, &cancelled); if (!got || cancelled) { diff --git a/gpcontrib/anser/src/anserbloomproduce.c b/gpcontrib/anser/src/anserbloomproduce.c index 88e98a23c05..1bf5d584439 100644 --- a/gpcontrib/anser/src/anserbloomproduce.c +++ b/gpcontrib/anser/src/anserbloomproduce.c @@ -30,6 +30,7 @@ #include "anser.h" #include "anserbloom.h" #include "anserfilter.h" +#include "anserpayload.h" #include "ansersideband.h" #include "cdb/cdbvars.h" @@ -61,13 +62,13 @@ AnserProducePublishPart(AnserBloomFilterProduceState *state, const void *payload, Size payload_len, bool cancelled) { if (Gp_role == GP_ROLE_EXECUTE) - return AnserSidebandPublish(&state->channel_key, state->part_index, - state->total_parts, payload, payload_len, - cancelled); + return AnserSidebandPublish(&state->channel_key, ANSER_PAYLOAD_BLOOM, + state->part_index, state->total_parts, + payload, payload_len, cancelled); - return AnserDispatchLocalPublish(&state->channel_key, state->part_index, - state->total_parts, payload, payload_len, - cancelled); + return AnserDispatchLocalPublish(&state->channel_key, ANSER_PAYLOAD_BLOOM, + state->part_index, state->total_parts, + payload, payload_len, cancelled); } AnserBloomFilterProduceState * @@ -97,12 +98,24 @@ void ExecAnserBloomFilterProduceAddDatum(AnserBloomFilterProduceState *state, Datum value, bool isnull) { - if (state == NULL || state->published || isnull) + /* + * A NULL filter means AnserBloomCreate declined to build one -- too many + * keys for the payload cap. There is nothing to add to, and the node has + * already published the cancel. + */ + if (state == NULL || state->filter == NULL || state->published || isnull) return; bloom_add_element(state->filter, (unsigned char *) &value, sizeof(Datum)); } +/* Does this producer have a filter to fill? False means "already hopeless". */ +bool +ExecAnserBloomFilterProduceHasFilter(AnserBloomFilterProduceState *state) +{ + return state != NULL && state->filter != NULL; +} + bool ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state) { @@ -126,6 +139,29 @@ ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state) return AnserProducePublishPart(state, NULL, 0, true); } + /* + * Too saturated to be worth anything? Cancel rather than send. + * + * This is the check the planner cannot make: it decided the filter was + * worth building from a row estimate, and estimates are wrong. A filter + * whose bits are nearly all set matches nearly every probe row, so shipping + * it would buy the consumers a hash and k probes per row in exchange for + * almost no rows eliminated -- pure overhead on both sides. + */ + if (bloom_false_positive_rate(state->filter) > ANSER_BLOOM_MAX_FPR) + { + ANSER_DEBUG("anser: publishing a cancel: filter is %.1f%% full, est. FPR %.1f%% above the %.0f%% limit", + bloom_prop_bits_set(state->filter) * 100.0, + bloom_false_positive_rate(state->filter) * 100.0, + ANSER_BLOOM_MAX_FPR * 100.0); + state->published = true; + return AnserProducePublishPart(state, NULL, 0, true); + } + + ANSER_DEBUG("anser: filter is %.1f%% full, est. FPR %.2f%%", + bloom_prop_bits_set(state->filter) * 100.0, + bloom_false_positive_rate(state->filter) * 100.0); + /* * Serialize as a self-contained single part (index 0 of 1). The coordinator * stores the first part verbatim and OR-folds each later part, bumping the diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c index ce6bc9091b8..a48e3a0d1f7 100644 --- a/gpcontrib/anser/src/anserdispatch.c +++ b/gpcontrib/anser/src/anserdispatch.c @@ -33,6 +33,11 @@ * parts are folded in place as they arrive, so only the final fold is on the * critical path. * + * Nothing here knows what a part contains. Merging is delegated to the payload + * type's fold() (anserpayload.h), and the only other thing this file asks about + * a type is whether its body is covered by the message checksum -- which is why + * it does not include anserfilter.h at all. + * * A note on libpq linkage. The connections we write to were created by the * copy of libpq that is statically linked into the postgres binary, and every * symbol listed in libpq's exports.txt is deliberately made *local* in that @@ -55,7 +60,7 @@ #include "libpq-int.h" #include "anser.h" -#include "anserfilter.h" +#include "anserpayload.h" #include "ansersideband.h" #include "cdb/cdbconn.h" #include "cdb/cdbdisp.h" @@ -74,6 +79,7 @@ typedef struct AnserDispChannel { AnserChannelKey key; + const AnserPayloadOps *ops; /* what this carries; NULL until a part says */ char *payload; /* merged part, or NULL before the first one */ Size payload_len; int parts_received; @@ -83,25 +89,14 @@ typedef struct AnserDispChannel List *subscribers; /* PGconn * of QEs awaiting delivery */ } AnserDispChannel; -/* Parsed QE -> QD message. */ -typedef struct AnserWireMsg -{ - char kind; - AnserChannelKey key; - int part_index; - int total_parts; - int flags; - const char *body; /* base64, not NUL-terminated */ - int body_len; -} AnserWireMsg; - static HTAB *AnserDispChannels = NULL; static MemoryContext AnserDispContext = NULL; static AnserDispChannel *anser_disp_lookup(const AnserChannelKey *key, bool create); -static bool anser_disp_parse(const char *msg, AnserWireMsg *out); -static void anser_disp_apply_part(AnserDispChannel *chan, const void *payload, - Size payload_len, int total_parts, bool cancelled); +static void anser_disp_apply_part(AnserDispChannel *chan, + const AnserPayloadOps *ops, + const void *payload, Size payload_len, + int total_parts, bool cancelled); static void anser_disp_deliver(AnserDispChannel *chan); static bool anser_disp_push(PGconn *conn, AnserDispChannel *chan); @@ -181,6 +176,7 @@ anser_disp_lookup(const AnserChannelKey *key, bool create) if (create && !found) { /* hash_search only fills the key; initialize the rest. */ + chan->ops = NULL; chan->payload = NULL; chan->payload_len = 0; chan->parts_received = 0; @@ -210,6 +206,7 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, int sender = (dr != NULL && dr->segdbDesc != NULL) ? dr->segdbDesc->segindex : -99; AnserWireMsg msg; + const AnserPayloadOps *ops; AnserDispChannel *chan; MemoryContext oldcxt; @@ -217,13 +214,39 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, strcmp(n->relname, ANSER_NOTIFY_CHANNEL) != 0) return false; - if (n->extra == NULL || !anser_disp_parse(n->extra, &msg)) + if (n->extra == NULL || !AnserWireParse(n->extra, &msg)) { elog(LOG, "anser: ignoring malformed message from a segment (len=%zu)", n->extra != NULL ? strlen(n->extra) : (size_t) 0); return true; } + /* + * Resolve what the message carries before doing anything with it. An + * unregistered type means a producer was taught to send something this + * coordinator does not know how to merge -- almost certainly a payload type + * added to anserpayload.h but not to AnserPayloadTable. + */ + ops = AnserPayloadLookup(msg.payload_type); + if (ops == NULL) + { + elog(LOG, "anser: unknown payload type '%c' from seg%d (cond=%u); ignoring", + msg.payload_type, sender, msg.key.condition_id); + return true; + } + + /* + * A message with no body can be checked here, before anything is + * allocated; one with a body is checked in the PART branch below, once it + * has been decoded. + */ + if (msg.body_len == 0 && !AnserWireCheckCrc(&msg, NULL, 0)) + { + elog(LOG, "anser: ignoring corrupted message from seg%d (cond=%u kind=%c)", + sender, msg.key.condition_id, msg.kind); + return true; + } + anser_disp_init(); oldcxt = MemoryContextSwitchTo(AnserDispContext); @@ -262,9 +285,19 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, raw = palloc(maxlen); raw_len = pg_b64_decode(msg.body, msg.body_len, raw, maxlen); - if (raw_len < 0 || raw_len > gp_anser_max_info_size) + + /* + * Undecodable, oversized or corrupted: cancel the channel rather + * than guess. Consumers then run unfiltered, which is slower but + * correct -- folding in a part we cannot vouch for risks clearing a + * bit that should be set, and a filter missing a key silently drops + * joinable rows. + */ + if (raw_len < 0 || raw_len > gp_anser_max_info_size || + !AnserWireCheckCrc(&msg, raw, (Size) raw_len)) { - /* Undecodable or oversized: cancel rather than guess. */ + elog(LOG, "anser: unusable part for condition %u from seg%d; cancelling channel", + msg.key.condition_id, sender); pfree(raw); raw = NULL; raw_len = 0; @@ -272,10 +305,10 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, } } - anser_disp_apply_part(chan, raw, (Size) raw_len, msg.total_parts, + anser_disp_apply_part(chan, ops, raw, (Size) raw_len, msg.total_parts, (msg.flags & ANSER_WIRE_F_CANCELLED) != 0); - ANSER_DEBUG("anser: QD part cond=%u from seg%d (says part %d of %d) %d/%d bytes=%d -> %s", - msg.key.condition_id, sender, msg.part_index, + ANSER_DEBUG("anser: QD %s part cond=%u from seg%d (says part %d of %d) %d/%d bytes=%d -> %s", + ops->name, msg.key.condition_id, sender, msg.part_index, msg.total_parts, chan->parts_received, chan->expected_parts, raw_len, chan->cancelled ? "cancelled" : @@ -295,16 +328,37 @@ AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, * Fold one part into the channel's accumulator. * * The first part is kept verbatim and becomes the accumulator; later parts are - * OR'd into it in place (AnserBloomFoldPartInPlace), so no part is ever copied - * twice and the accumulator is never reallocated. + * merged into it in place by the payload type's fold(), so no part is ever + * copied twice and the accumulator is never reallocated. Nothing here knows + * what the bytes are -- for a bloom filter the fold is a bitwise OR, for a row + * count it would be a sum, and this function reads the same either way. */ static void -anser_disp_apply_part(AnserDispChannel *chan, const void *payload, - Size payload_len, int total_parts, bool cancelled) +anser_disp_apply_part(AnserDispChannel *chan, const AnserPayloadOps *ops, + const void *payload, Size payload_len, int total_parts, + bool cancelled) { if (chan->cancelled) return; /* already dead; nothing to do */ + if (chan->ops == NULL) + chan->ops = ops; + else if (chan->ops != ops) + { + /* + * Two producers disagree about what this channel carries. Merging + * across types is meaningless, so give up on the channel: consumers run + * unfiltered, which is always correct. + */ + elog(LOG, "anser: cond=%u carries both '%s' and '%s'; cancelling channel", + chan->key.condition_id, chan->ops->name, ops->name); + chan->cancelled = true; + chan->complete = true; + chan->payload = NULL; + chan->payload_len = 0; + return; + } + if (total_parts > chan->expected_parts) { /* @@ -341,21 +395,22 @@ anser_disp_apply_part(AnserDispChannel *chan, const void *payload, chan->payload_len = payload_len; chan->parts_received++; } - else if (AnserBloomFoldPartInPlace(chan->payload, chan->payload_len, - payload, payload_len)) + else if (ops->fold != NULL && + ops->fold(chan->payload, chan->payload_len, payload, payload_len)) { chan->parts_received++; } else { /* - * Sizes or parameters disagree, so the parts cannot be unioned. That - * should not happen (every part on a channel is built from the same - * plan parameters), but if it does the only safe answer is to give up - * on the channel. + * The parts cannot be merged -- for a bloom filter, sizes or filter + * parameters disagree. That should not happen (every part on a channel + * is built from the same plan parameters), but if it does the only safe + * answer is to give up on the channel. A type with no fold() reaches + * here too, which is right: it should never have carried a body. */ - elog(LOG, "anser: incompatible part for condition %u; cancelling channel", - chan->key.condition_id); + elog(LOG, "anser: incompatible '%s' part for condition %u; cancelling channel", + ops->name, chan->key.condition_id); chan->cancelled = true; chan->complete = true; chan->payload = NULL; @@ -364,7 +419,24 @@ anser_disp_apply_part(AnserDispChannel *chan, const void *payload, } if (chan->expected_parts > 0 && chan->parts_received >= chan->expected_parts) + { chan->complete = true; + + /* + * Every part is in, so this is the first and last chance to judge the + * merged result. A type that declines it here saves each consumer both + * the delivery and the per-row probing it would have paid for. + */ + if (chan->payload != NULL && ops->worth_delivering != NULL && + !ops->worth_delivering(chan->payload, chan->payload_len)) + { + elog(LOG, "anser: merged '%s' payload for condition %u is not worth delivering; cancelling channel", + ops->name, chan->key.condition_id); + chan->cancelled = true; + chan->payload = NULL; + chan->payload_len = 0; + } + } } /* Push the finished channel to everyone waiting, then forget them. */ @@ -396,15 +468,30 @@ anser_disp_push(PGconn *conn, AnserDispChannel *chan) int flags = chan->cancelled ? ANSER_WIRE_F_CANCELLED : 0; int keylen = (int) strlen(chan->key.condition_key); int paylen = chan->cancelled ? 0 : (int) chan->payload_len; + pg_crc32c crc; + char paytype; if (!anser_conn_ok(conn)) return false; + /* + * A channel that was cancelled before any part arrived has no type. That + * is fine: the delivery carries no body, and a consumer only checks the + * type of a body it actually received. + */ + paytype = chan->ops != NULL ? chan->ops->code : ANSER_PAYLOAD_NONE; + + crc = AnserWirePushCrc(paytype, chan->key.condition_id, (uint32) flags, + chan->key.condition_key, keylen, + chan->payload, paylen); + /* * Raw binary: pqPutnchar performs no encoding conversion, so unlike the * QE -> QD direction this needs no base64. */ if (pqPutMsgStart(GP_SIDEBAND_MESSAGE, conn) < 0 || + pqPutInt((int) paytype, 4, conn) < 0 || + pqPutInt((int) crc, 4, conn) < 0 || pqPutInt((int) chan->key.condition_id, 4, conn) < 0 || pqPutInt(flags, 4, conn) < 0 || pqPutInt(keylen, 4, conn) < 0 || @@ -419,73 +506,106 @@ anser_disp_push(PGconn *conn, AnserDispChannel *chan) return false; } - ANSER_DEBUG("anser: QD pushed cond=%u bytes=%d cancelled=%d", - chan->key.condition_id, paylen, chan->cancelled ? 1 : 0); + ANSER_DEBUG("anser: QD pushed cond=%u type=%c bytes=%d cancelled=%d", + chan->key.condition_id, paytype, paylen, + chan->cancelled ? 1 : 0); return true; } /* - * Parse a QE -> QD payload. - * - * Layout: a single-line text header, then the condition key, then the body. + * Parse a QE -> QD payload: fixed-width header, then the condition key, then + * the base64 body. ansersideband.h documents the layout and the reasoning; the + * checks here are what make it trustworthy: * - * anser1 \n - * + * - the tag must match, so a message from a different format is not + * misinterpreted as this one; + * - the header is a constant length, so the key and the body start at offsets + * that no payload byte can influence; + * - the two lengths must account for the message exactly, which catches a + * truncated or over-long message before any of it is used; + * - the CRC is verified once the body has been decoded, in the caller. * - * The header holds only numbers and one character, so it cannot contain the - * newline that terminates it; key and body are taken by length, so neither - * needs escaping or a delimiter of its own. + * The payload type is carried through as the raw byte; resolving it against the + * registry is the caller's job, so that an unregistered type is reported as + * exactly that rather than as a framing error. */ -static bool -anser_disp_parse(const char *msg, AnserWireMsg *out) +bool +AnserWireParse(const char *msg, AnserWireMsg *out) { - const char *nl; - const char *rest; + Size msglen = strlen(msg); char kind; - int ssid, + char paytype; + uint32 ssid, ccnt, condid, part, total, flags, keylen, - bodylen; + bodylen, + crc; - nl = strchr(msg, '\n'); - if (nl == NULL) + if (msglen < ANSER_WIRE_HDR_LEN) + return false; + if (strncmp(msg, ANSER_WIRE_TAG " ", sizeof(ANSER_WIRE_TAG)) != 0) return false; - if (sscanf(msg, ANSER_WIRE_TAG " %c %d %d %d %d %d %d %d %d", - &kind, &ssid, &ccnt, &condid, &part, &total, &flags, - &keylen, &bodylen) != 9) + if (sscanf(msg, ANSER_WIRE_HDR_SCANF, + &kind, &paytype, &ssid, &ccnt, &condid, &part, &total, &flags, + &keylen, &bodylen, &crc) != 11) return false; if (kind != ANSER_WIRE_KIND_PART && kind != ANSER_WIRE_KIND_SUBSCRIBE) return false; - if (condid < 0 || keylen < 0 || bodylen < 0 || - keylen >= ANSER_CONDITION_KEY_SIZE) + if (keylen >= ANSER_CONDITION_KEY_SIZE) return false; - - rest = nl + 1; - if ((int) strlen(rest) != keylen + bodylen) + if (msglen != ANSER_WIRE_HDR_LEN + (Size) keylen + (Size) bodylen) return false; MemSet(out, 0, sizeof(*out)); + out->wire = msg; out->kind = kind; - out->key.gp_session_id = ssid; - out->key.gp_command_count = ccnt; - out->key.condition_id = (uint32) condid; - memcpy(out->key.condition_key, rest, keylen); + out->payload_type = paytype; + out->key.gp_session_id = (int) ssid; + out->key.gp_command_count = (int) ccnt; + out->key.condition_id = condid; + memcpy(out->key.condition_key, msg + ANSER_WIRE_HDR_LEN, keylen); out->key.condition_key[keylen] = '\0'; - out->part_index = part; - out->total_parts = total; - out->flags = flags; - out->body = rest + keylen; - out->body_len = bodylen; + out->key_len = (int) keylen; + out->part_index = (int) part; + out->total_parts = (int) total; + out->flags = (int) flags; + out->body = msg + ANSER_WIRE_HDR_LEN + keylen; + out->body_len = (int) bodylen; + out->crc = crc; return true; } +/* + * Verify a parsed message against its checksum. + * + * 'body' is the decoded body, which is what the producer checksummed -- so + * where it is covered this validates the base64 round trip as well. Pass + * NULL/0 for a message that has no body, and note that a type which opts out of + * checksumming its body still has its header and key checked. + */ +bool +AnserWireCheckCrc(const AnserWireMsg *msg, const void *body, Size body_len) +{ + pg_crc32c crc; + + INIT_CRC32C(crc); + COMP_CRC32C(crc, msg->wire, ANSER_WIRE_CRC_OFFSET); + COMP_CRC32C(crc, msg->key.condition_key, msg->key_len); + if (body != NULL && body_len > 0 && + AnserPayloadChecksumsBody(msg->payload_type)) + COMP_CRC32C(crc, body, body_len); + FIN_CRC32C(crc); + + return (uint32) crc == msg->crc; +} + /* * Coordinator-local producer. * @@ -493,11 +613,12 @@ anser_disp_parse(const char *msg, AnserWireMsg *out) * folds straight into the same channel table the hook uses. */ bool -AnserDispatchLocalPublish(const AnserChannelKey *channel_key, +AnserDispatchLocalPublish(const AnserChannelKey *channel_key, char payload_type, uint32 part_index, uint32 total_parts, const void *payload, Size payload_len, bool cancelled) { + const AnserPayloadOps *ops; AnserDispChannel *chan; MemoryContext oldcxt; @@ -506,16 +627,24 @@ AnserDispatchLocalPublish(const AnserChannelKey *channel_key, if (!cancelled && payload_len > (Size) gp_anser_max_info_size) cancelled = true; + ops = AnserPayloadLookup(payload_type); + if (ops == NULL) + { + elog(LOG, "anser: unknown payload type '%c' for condition %u; ignoring", + payload_type, channel_key->condition_id); + return false; + } + anser_disp_init(); oldcxt = MemoryContextSwitchTo(AnserDispContext); chan = anser_disp_lookup(channel_key, true); if (chan != NULL) { - anser_disp_apply_part(chan, payload, payload_len, (int) total_parts, - cancelled); - ANSER_DEBUG("anser: QD local part cond=%u (part %u of %u) %d/%d bytes=%zu -> %s", - channel_key->condition_id, part_index, total_parts, + anser_disp_apply_part(chan, ops, payload, payload_len, + (int) total_parts, cancelled); + ANSER_DEBUG("anser: QD local %s part cond=%u (part %u of %u) %d/%d bytes=%zu -> %s", + ops->name, channel_key->condition_id, part_index, total_parts, chan->parts_received, chan->expected_parts, payload_len, chan->cancelled ? "cancelled" : chan->complete ? "complete" : "collecting"); @@ -535,7 +664,7 @@ AnserDispatchLocalPublish(const AnserChannelKey *channel_key, * complete or it never will be (a squelched producer, say) and we fail open. */ bool -AnserDispatchLocalConsume(const AnserChannelKey *channel_key, +AnserDispatchLocalConsume(const AnserChannelKey *channel_key, char payload_type, void **payload, Size *payload_len, bool *cancelled) { AnserDispChannel *chan; @@ -561,6 +690,17 @@ AnserDispatchLocalConsume(const AnserChannelKey *channel_key, return false; } + /* Same check the segment path makes on delivery; see anser_inbox_take. */ + if (chan->ops == NULL || chan->ops->code != payload_type) + { + elog(WARNING, "anser: condition %u holds payload type '%c', expected '%c'", + channel_key->condition_id, + chan->ops != NULL ? chan->ops->code : '?', payload_type); + if (cancelled != NULL) + *cancelled = true; + return false; + } + if (payload != NULL) { *payload = palloc(chan->payload_len); diff --git a/gpcontrib/anser/src/anserfilter.c b/gpcontrib/anser/src/anserfilter.c index 4ea09952aaf..88933861748 100644 --- a/gpcontrib/anser/src/anserfilter.c +++ b/gpcontrib/anser/src/anserfilter.c @@ -27,6 +27,7 @@ */ #include "postgres.h" +#include "anser.h" #include "anserfilter.h" #include "common/hashfn.h" #include "port/pg_bitutils.h" @@ -66,18 +67,108 @@ AnserBloomSeed(const char *condition_key) * is why the serialized part header does not need to carry the bitset parameters: * the reconstructing side already knows them. * - * We defer sizing to the standard bloom_create, which targets ~2 bytes per - * element, rounds the bitset down to a power of two, and floors it at 1 MB. - * max_payload_bytes bounds the bitset from above (minus header room), expressed - * as bloom_create's work_mem budget in KB. + * Sizing is bloom_create's, but the decision to keep the result is ours. Two + * ways it can come back unusable: + * + * - Too large to send. bloom_create ends with Max(1 MB, bitset), a floor + * that overrides the work_mem cap, so a payload cap below 1 MB yields a + * bitset that cannot be shipped. + * - Too thin to help. bloom_create has no minimum density (unlike + * bloom_create_aggresive, which refuses below 1.6 bits/key), so a large + * enough total_elems against a fixed cap gets you a filter that matches + * almost everything. + * + * Both are judged from the filter's own accessors rather than by re-deriving + * bloom_create's arithmetic here: my_bloom_power() and the floor are private to + * lib/bloomfilter.c, and a copy of them would go quietly out of date on a + * kernel rebase. The cost of learning the answer this way is one palloc0 that + * is immediately freed -- against a build-side scan, which is what returning + * NULL here avoids, that is nothing. */ bloom_filter * AnserBloomCreate(int64 total_elems, Size max_payload_bytes, uint64 seed) { - /* Internal callers always size the payload to hold a header + bitset. */ - Assert(max_payload_bytes > sizeof(AnserBloomPartHeader)); + bloom_filter *filter; + Size serialized; + double bits_per_key; + + /* + * These were an Assert on the grounds that internal callers always pass a + * payload cap with room for a header. They are checks because the caller + * is a plan node, and its parameters arrived from the coordinator in + * custom_private -- not somewhere an assertion belongs. The cap also + * cannot merely be >= the header: AnserBloomWorkMemKb subtracts the header + * from a Size, so a smaller cap would underflow to an enormous work_mem. + */ + if (total_elems <= 0 || max_payload_bytes <= sizeof(AnserBloomPartHeader)) + return NULL; + + filter = bloom_create(total_elems, AnserBloomWorkMemKb(max_payload_bytes), + seed); + serialized = AnserBloomSerializedSize(filter); + bits_per_key = (double) bloom_total_bits(filter) / (double) total_elems; + + if (serialized > max_payload_bytes) + { + ANSER_DEBUG("anser: not building a filter: smallest bitset serializes to %zu bytes, cap is %zu", + serialized, max_payload_bytes); + bloom_free(filter); + return NULL; + } + + if (bits_per_key < ANSER_BLOOM_MIN_BITS_PER_KEY) + { + ANSER_DEBUG("anser: not building a filter for %ld key(s) in %zu bytes: %.2f bits/key, below the %.1f floor", + (long) total_elems, max_payload_bytes, bits_per_key, + ANSER_BLOOM_MIN_BITS_PER_KEY); + bloom_free(filter); + return NULL; + } + + return filter; +} + +/* + * Is this serialized payload still selective enough to be worth delivering? + * + * Counts the set bits in the wire form, so the coordinator can ask it of a + * merged accumulator without rebuilding a filter -- which is the point, because + * the union of N parts is denser than any one of them. Three parts at 60% fill + * OR together to as much as 94%, so the producers' own checks do not protect + * consumers from the merged result; this is the check that does. + * + * Fill, not false positive rate: the serialized part carries no hash count, so + * fill is all there is to go on here. ANSER_BLOOM_MERGED_MAX_FILL is set + * accordingly -- high enough that it cannot reject a filter that is still good + * at a large k, which makes it a backstop rather than a tuning knob. + */ +bool +AnserBloomPartWorthSending(const void *payload, Size payload_len) +{ + const char *bits; + Size bitset_bytes; + uint64 bits_set; + double fill; - return bloom_create(total_elems, AnserBloomWorkMemKb(max_payload_bytes), seed); + if (!AnserBloomLooksLikePart(payload, payload_len)) + return false; + + bitset_bytes = payload_len - sizeof(AnserBloomPartHeader); + if (bitset_bytes == 0) + return false; + + bits = (const char *) payload + sizeof(AnserBloomPartHeader); + bits_set = pg_popcount(bits, (int) bitset_bytes); + fill = (double) bits_set / (double) (bitset_bytes * BITS_PER_BYTE); + + if (fill > ANSER_BLOOM_MERGED_MAX_FILL) + { + ANSER_DEBUG("anser: merged filter is %.1f%% full, above the %.0f%% limit; not worth delivering", + fill * 100.0, ANSER_BLOOM_MERGED_MAX_FILL * 100.0); + return false; + } + + return true; } Size diff --git a/gpcontrib/anser/src/anserplan.c b/gpcontrib/anser/src/anserplan.c index c713970a72e..6d91f6584df 100644 --- a/gpcontrib/anser/src/anserplan.c +++ b/gpcontrib/anser/src/anserplan.c @@ -35,6 +35,7 @@ #include "postgres.h" #include "anser.h" +#include "anserfilter.h" #include "anserplan.h" #include "cdb/cdbvars.h" #include "catalog/pg_type.h" @@ -60,8 +61,6 @@ typedef struct AnserInjectCtx } AnserInjectCtx; static int anser_max_plan_node_id(Plan *plan); -static bool anser_rf_size(double est_rows, int64 *total_elems, - int64 *max_payload, int64 *planned_bytes); static bool anser_hashjoin_keys(HashJoin *hj, AttrNumber *inner_attno, AttrNumber *outer_attno); static bool anser_resolve_build_scan(Plan *hash, AttrNumber inner_attno, @@ -153,9 +152,9 @@ anser_max_plan_node_id(Plan *plan) * equals the realized bitset: target ~2 bytes/element, floor at 1 MB, cap at the * server payload budget, round DOWN to a power of two. */ -static bool -anser_rf_size(double est_rows, int64 *total_elems, int64 *max_payload, - int64 *planned_bytes) +bool +AnserRuntimeFilterSize(double est_rows, int64 *total_elems, int64 *max_payload, + int64 *planned_bytes) { int64 cap_bytes; int64 elems; @@ -169,20 +168,45 @@ anser_rf_size(double est_rows, int64 *total_elems, int64 *max_payload, return false; /* cap too small to hold even a floor-sized filter */ /* - * Clamp the element estimate so 2*elems never exceeds the cap; this also keeps - * total_elems within int range for custom_private (cap/2 <= 32M elements). + * The element count stays the honest estimate; only the *size* is clamped. + * + * It is tempting to clamp elems instead -- it is the number that drives the + * size, so bounding it bounds the bitset in one step. That is a trap. + * total_elems is also what optimal_k() sizes the hash count from, so + * understating it understates k: a 128M-row build side reported as 33.5M + * gets k=10, when the optimum for 128M keys in a 512 Mbit filter is k=3. + * Since the producer inserts all 128M keys regardless of what we wrote + * down, the result is a filter with a 38% false positive rate where 15% was + * available. Lowering the declared count never makes a filter fit; it only + * relabels it, and then misconfigures it. + * + * INT32 is the real bound on elems: custom_private carries it as an Integer + * node. The density check below refuses anything remotely near that. */ elems = (est_rows > 0.0) ? (int64) est_rows : 1; - if (elems > cap_bytes / 2) - elems = cap_bytes / 2; if (elems < 1) elems = 1; + if (elems > PG_INT32_MAX) + elems = PG_INT32_MAX; - target_bytes = Max((int64) ANSER_RF_MIN_BYTES, elems * 2); + target_bytes = Min(cap_bytes, Max((int64) ANSER_RF_MIN_BYTES, elems * 2)); realized = ANSER_RF_MIN_BYTES; while ((realized << 1) <= target_bytes) realized <<= 1; + /* + * Refuse a join whose build side cannot be usefully summarized within the + * payload cap. + * + * Refusing here is the cheapest possible outcome: no producer, no consumer, + * no channel, and nothing for a consumer to wait on and time out against. + * The runtime checks in anserfilter.c exist for when this estimate turns + * out to be wrong, not instead of this one. + */ + if (realized * (double) BITS_PER_BYTE / (double) elems + < ANSER_BLOOM_MIN_BITS_PER_KEY) + return false; + *total_elems = elems; *max_payload = cap_bytes + ANSER_RF_HEADER_ROOM; *planned_bytes = realized; @@ -339,18 +363,19 @@ anser_try_inject(HashJoin *hj, AnserInjectCtx *ctx) if (!anser_resolve_build_scan(hash, inner_attno, &build_parent, &build_scan, &build_attno)) return; - if (!anser_rf_size(hash->plan_rows, &total_elems, &max_payload, &planned_bytes)) + if (!AnserRuntimeFilterSize(hash->plan_rows, &total_elems, &max_payload, &planned_bytes)) return; condition_id = ctx->next_condition_id++; snprintf(condition_key, sizeof(condition_key), "anser_rf_%u", condition_id); /* - * One consumer per channel. The consumer wait table budgets exactly - * anser.max_consumers_per_channel slots per channel, sized for one - * consumer instance per segment (nseg). A second consumer plan node on the - * same channel would need 2*nseg slots and could exhaust that budget, so we - * never inject one -- skip the whole join and fail open instead. + * One consumer per channel. Two consumer nodes sharing a channel would + * both be served -- the coordinator pushes to every subscriber -- but they + * would also both count toward nothing: the channel's part count comes from + * the producers, so a second consumer only multiplies deliveries. More to + * the point, the two would be indistinguishable in the debug trace and in + * any future per-channel accounting, so keep the invariant. * * Minting a unique condition_id per injection makes this hold by * construction, so the check never fires. It stays as a guard against diff --git a/gpcontrib/anser/src/anserplanexec.c b/gpcontrib/anser/src/anserplanexec.c index 96089ff2fac..bcfc4575c66 100644 --- a/gpcontrib/anser/src/anserplanexec.c +++ b/gpcontrib/anser/src/anserplanexec.c @@ -363,7 +363,28 @@ anser_produce_next(CustomScanState *node) * Every process that deserializes the plan builds this node, but only the * ones running its slice ever execute it; the rest must stay silent. */ - st->started = true; + if (!st->started) + { + st->started = true; + + /* + * No filter means the estimate that justified this node was wrong by + * enough that one cannot usefully be built. Say so now, on the first + * tuple, rather than after scanning the whole build side: the consumers + * are sitting on their timeout, and every millisecond of it is wasted. + * + * This is also the earliest point at which cancelling is safe. Doing + * it at ExecInit time would cancel channels from the coordinator, which + * builds this node for every slice but executes none of them. + */ + if (st->produce != NULL && !st->published && + !ExecAnserBloomFilterProduceHasFilter(st->produce)) + { + ANSER_DEBUG("anser: producer has no usable filter; cancelling before the scan"); + (void) ExecAnserBloomFilterProduceCancel(st->produce); + st->published = true; + } + } slot = ExecProcNode(child); diff --git a/gpcontrib/anser/src/ansersideband.c b/gpcontrib/anser/src/ansersideband.c index 8a1ac201cee..556fe0d0a26 100644 --- a/gpcontrib/anser/src/ansersideband.c +++ b/gpcontrib/anser/src/ansersideband.c @@ -48,6 +48,8 @@ #include "libpq/pqformat.h" #include "miscadmin.h" #include "nodes/pg_list.h" +#include "port/pg_bswap.h" +#include "port/pg_crc32c.h" #include "storage/latch.h" #include "tcop/dest.h" #include "tcop/tcopprot.h" @@ -70,6 +72,7 @@ typedef struct AnserInboxEntry { AnserChannelKey key; + char payload_type; char *payload; /* NULL when cancelled */ Size payload_len; bool cancelled; @@ -78,12 +81,8 @@ typedef struct AnserInboxEntry static List *AnserInbox = NIL; static bool anser_sideband_send(const char *payload); -static char *anser_sideband_format(const AnserChannelKey *channel_key, char kind, - uint32 part_index, uint32 total_parts, - int flags, const void *payload, - Size payload_len); -static bool anser_inbox_take(const AnserChannelKey *key, void **payload, - Size *payload_len, bool *cancelled); +static bool anser_inbox_take(const AnserChannelKey *key, char payload_type, + void **payload, Size *payload_len, bool *cancelled); static bool anser_sideband_read_one(long timeout_ms); /* @@ -91,7 +90,7 @@ static bool anser_sideband_read_one(long timeout_ms); * because nothing on this side needs to wait for it. */ bool -AnserSidebandPublish(const AnserChannelKey *channel_key, +AnserSidebandPublish(const AnserChannelKey *channel_key, char payload_type, uint32 part_index, uint32 total_parts, const void *payload, Size payload_len, bool cancelled) { @@ -110,14 +109,14 @@ AnserSidebandPublish(const AnserChannelKey *channel_key, payload_len = 0; } - msg = anser_sideband_format(channel_key, ANSER_WIRE_KIND_PART, - part_index, total_parts, flags, - (flags & ANSER_WIRE_F_CANCELLED) ? NULL : payload, - (flags & ANSER_WIRE_F_CANCELLED) ? 0 : payload_len); + msg = AnserWireFormat(channel_key, ANSER_WIRE_KIND_PART, payload_type, + part_index, total_parts, flags, + (flags & ANSER_WIRE_F_CANCELLED) ? NULL : payload, + (flags & ANSER_WIRE_F_CANCELLED) ? 0 : payload_len); ok = anser_sideband_send(msg); - ANSER_DEBUG("anser: seg%d published cond=%u part=%u/%u bytes=%zu cancelled=%d sent=%d", - GpIdentity.segindex, channel_key->condition_id, part_index, - total_parts, payload_len, + ANSER_DEBUG("anser: seg%d published cond=%u type=%c part=%u/%u bytes=%zu cancelled=%d sent=%d", + GpIdentity.segindex, channel_key->condition_id, payload_type, + part_index, total_parts, payload_len, (flags & ANSER_WIRE_F_CANCELLED) ? 1 : 0, ok ? 1 : 0); pfree(msg); @@ -134,7 +133,7 @@ AnserSidebandPublish(const AnserChannelKey *channel_key, * wait could outlive the reason for it. */ bool -AnserSidebandConsumeWait(const AnserChannelKey *channel_key, +AnserSidebandConsumeWait(const AnserChannelKey *channel_key, char payload_type, void **payload, Size *payload_len, bool *cancelled, long timeout_ms) { @@ -154,11 +153,15 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, return false; /* It may already be here: the coordinator pushes as soon as it can. */ - if (anser_inbox_take(channel_key, payload, payload_len, cancelled)) + if (anser_inbox_take(channel_key, payload_type, payload, payload_len, cancelled)) return payload != NULL && *payload != NULL; - msg = anser_sideband_format(channel_key, ANSER_WIRE_KIND_SUBSCRIBE, - 0, 0, 0, NULL, 0); + /* + * A subscription carries nothing, so its payload type is NONE -- what this + * consumer expects to receive is checked on delivery, not announced here. + */ + msg = AnserWireFormat(channel_key, ANSER_WIRE_KIND_SUBSCRIBE, + ANSER_PAYLOAD_NONE, 0, 0, 0, NULL, 0); if (!anser_sideband_send(msg)) { pfree(msg); @@ -173,7 +176,8 @@ AnserSidebandConsumeWait(const AnserChannelKey *channel_key, { CHECK_FOR_INTERRUPTS(); - if (anser_inbox_take(channel_key, payload, payload_len, cancelled)) + if (anser_inbox_take(channel_key, payload_type, payload, payload_len, + cancelled)) return payload != NULL && *payload != NULL; if (timeout_ms >= 0 && @@ -216,6 +220,8 @@ anser_sideband_read_one(long timeout_ms) StringInfoData buf; AnserInboxEntry *entry; MemoryContext oldcxt; + char paytype; + uint32 crc; int condid; int flags; int keylen; @@ -273,6 +279,8 @@ anser_sideband_read_one(long timeout_ms) return false; } + paytype = (char) pq_getmsgint(&buf, 4); + crc = (uint32) pq_getmsgint(&buf, 4); condid = pq_getmsgint(&buf, 4); flags = pq_getmsgint(&buf, 4); keylen = pq_getmsgint(&buf, 4); @@ -292,6 +300,21 @@ anser_sideband_read_one(long timeout_ms) } payptr = paylen > 0 ? pq_getmsgbytes(&buf, paylen) : NULL; + if (crc != (uint32) AnserWirePushCrc(paytype, (uint32) condid, + (uint32) flags, keyptr, keylen, + payptr, paylen)) + { + /* + * Drop it and keep waiting: the deadline will expire and this consumer + * will run unfiltered. Using the filter anyway is the one thing we must + * not do -- a bit corrupted 1 -> 0 silently drops joinable rows. + */ + pfree(buf.data); + elog(LOG, "anser: checksum mismatch on filter for condition %u; discarding", + (uint32) condid); + return true; + } + /* * The inbox outlives this call and the memory context it was reached in, * so anchor it somewhere stable; AnserSidebandResetAll drops it. @@ -303,6 +326,7 @@ anser_sideband_read_one(long timeout_ms) entry->key.condition_id = (uint32) condid; memcpy(entry->key.condition_key, keyptr, keylen); entry->key.condition_key[keylen] = '\0'; + entry->payload_type = paytype; entry->cancelled = (flags & ANSER_WIRE_F_CANCELLED) != 0; if (!entry->cancelled && paylen > 0) { @@ -312,9 +336,9 @@ anser_sideband_read_one(long timeout_ms) } AnserInbox = lappend(AnserInbox, entry); MemoryContextSwitchTo(oldcxt); - ANSER_DEBUG("anser: seg%d received cond=%u bytes=%zu cancelled=%d", - GpIdentity.segindex, (uint32) condid, entry->payload_len, - entry->cancelled ? 1 : 0); + ANSER_DEBUG("anser: seg%d received cond=%u type=%c bytes=%zu cancelled=%d", + GpIdentity.segindex, (uint32) condid, paytype, + entry->payload_len, entry->cancelled ? 1 : 0); pfree(buf.data); return true; @@ -322,7 +346,7 @@ anser_sideband_read_one(long timeout_ms) /* Claim a delivery for this channel, if one has arrived. */ static bool -anser_inbox_take(const AnserChannelKey *key, void **payload, +anser_inbox_take(const AnserChannelKey *key, char payload_type, void **payload, Size *payload_len, bool *cancelled) { ListCell *lc; @@ -336,6 +360,22 @@ anser_inbox_take(const AnserChannelKey *key, void **payload, ANSER_CONDITION_KEY_SIZE) != 0) continue; + /* + * Right channel, wrong kind of information. The coordinator stamps + * the type from the parts it folded, so this means a producer and a + * consumer disagree about what the channel carries -- claim the entry + * to stop waiting on it, and treat it as a cancellation so this + * consumer runs unfiltered. An empty delivery carries no type to + * check. + */ + if (!entry->cancelled && entry->payload != NULL && + entry->payload_type != payload_type) + { + elog(WARNING, "anser: condition %u delivered payload type '%c', expected '%c'", + key->condition_id, entry->payload_type, payload_type); + entry->cancelled = true; + } + if (cancelled != NULL) *cancelled = entry->cancelled; if (!entry->cancelled && entry->payload != NULL) @@ -384,38 +424,108 @@ AnserSidebandResetAll(void) AnserDispatchReset(); } -/* Build a QE -> QD payload; see anser_disp_parse() for the layout. */ -static char * -anser_sideband_format(const AnserChannelKey *channel_key, char kind, - uint32 part_index, uint32 total_parts, int flags, - const void *payload, Size payload_len) +/* + * Build a QE -> QD payload: fixed-width header, then the key, then the base64 + * body. See ansersideband.h for the layout and AnserWireParse() for the + * reader. + */ +char * +AnserWireFormat(const AnserChannelKey *channel_key, char kind, + char payload_type, uint32 part_index, uint32 total_parts, + int flags, const void *payload, Size payload_len) { StringInfoData buf; + char hdr[ANSER_WIRE_HDR_LEN + 1]; + int hdrlen; int keylen = (int) strlen(channel_key->condition_key); int bodylen = 0; char *body = NULL; + pg_crc32c crc; - if (payload != NULL && payload_len > 0) + /* + * What the checksum covers has to be exactly what goes on the wire, so the + * bytes it will cover are recorded here, as the body is encoded, rather + * than re-derived from the arguments afterwards. Deriving them again would + * mean covering a payload that was suppressed -- a cancelled message + * carries no body however it was called -- and the reader, seeing no body, + * would compute a different checksum and discard a perfectly good message. + */ + const void *crc_body = NULL; + Size crc_body_len = 0; + + if (!(flags & ANSER_WIRE_F_CANCELLED) && payload != NULL && payload_len > 0) { int maxlen = pg_b64_enc_len((int) payload_len); - body = palloc(maxlen + 1); - bodylen = pg_b64_encode((const char *) payload, (int) payload_len, - body, maxlen); - if (bodylen < 0) + if (maxlen > ANSER_WIRE_MAX_BODYLEN) { - pfree(body); - body = NULL; + /* + * No header can describe a body this large. gp_anser_max_info_size + * keeps us far away from this, so it is a belt-and-braces check on + * the field width rather than a reachable path. + */ bodylen = 0; flags |= ANSER_WIRE_F_CANCELLED; } + else + { + body = palloc(maxlen + 1); + bodylen = pg_b64_encode((const char *) payload, (int) payload_len, + body, maxlen); + if (bodylen < 0) + { + pfree(body); + body = NULL; + bodylen = 0; + flags |= ANSER_WIRE_F_CANCELLED; + } + else + { + crc_body = payload; + crc_body_len = payload_len; + } + } + } + + Assert(keylen <= ANSER_WIRE_MAX_KEYLEN); + + /* + * Format the header with a zero CRC, checksum it together with the key and + * the raw body, then overwrite the CRC field in place -- it sits at a fixed + * offset, so one pass suffices and the checksum still covers every header + * field. Checksumming the body before base64 rather than after means a + * mangled encoding is caught too, and whether the body is covered at all is + * the payload type's call (anserpayload.h). + */ + hdrlen = snprintf(hdr, sizeof(hdr), ANSER_WIRE_HDR_FORMAT, + kind, payload_type, + (uint32) channel_key->gp_session_id, + (uint32) channel_key->gp_command_count, + channel_key->condition_id, + part_index, total_parts, + (uint32) flags, (uint32) keylen, (uint32) bodylen, 0U); + if (hdrlen != ANSER_WIRE_HDR_LEN) + { + /* + * Cannot happen -- every field is width-limited -- but complain rather + * than throw: this is reached from producer teardown, and the + * coordinator's length cross-check will reject the message anyway, + * which costs the filter and not the query. + */ + elog(WARNING, "anser: formatted a %d byte header, expected %d", + hdrlen, ANSER_WIRE_HDR_LEN); } + INIT_CRC32C(crc); + COMP_CRC32C(crc, hdr, ANSER_WIRE_CRC_OFFSET); + COMP_CRC32C(crc, channel_key->condition_key, keylen); + if (crc_body != NULL && AnserPayloadChecksumsBody(payload_type)) + COMP_CRC32C(crc, crc_body, crc_body_len); + FIN_CRC32C(crc); + snprintf(hdr + ANSER_WIRE_CRC_OFFSET, 9, "%08x", (uint32) crc); + initStringInfo(&buf); - appendStringInfo(&buf, ANSER_WIRE_TAG " %c %d %d %u %u %u %d %d %d\n", - kind, channel_key->gp_session_id, - channel_key->gp_command_count, channel_key->condition_id, - part_index, total_parts, flags, keylen, bodylen); + appendBinaryStringInfo(&buf, hdr, ANSER_WIRE_HDR_LEN); appendBinaryStringInfo(&buf, channel_key->condition_key, keylen); if (bodylen > 0) appendBinaryStringInfo(&buf, body, bodylen); @@ -426,6 +536,39 @@ anser_sideband_format(const AnserChannelKey *channel_key, char kind, return buf.data; } +/* + * Checksum of a QD -> QE push. Lives here, on the reading side, but is called + * from anserdispatch.c too: one definition means the writer and the reader + * cannot disagree about what is covered -- including whether the body is, which + * follows from the payload type. Integers go in network byte order so the + * value does not depend on the host. + */ +pg_crc32c +AnserWirePushCrc(char payload_type, uint32 condition_id, uint32 flags, + const char *key, int keylen, const void *body, int bodylen) +{ + pg_crc32c crc; + uint32 fields[4]; + uint32 belen; + + fields[0] = pg_hton32((uint32) (unsigned char) payload_type); + fields[1] = pg_hton32(condition_id); + fields[2] = pg_hton32(flags); + fields[3] = pg_hton32((uint32) keylen); + + INIT_CRC32C(crc); + COMP_CRC32C(crc, fields, sizeof(fields)); + if (keylen > 0) + COMP_CRC32C(crc, key, keylen); + belen = pg_hton32((uint32) bodylen); + COMP_CRC32C(crc, &belen, sizeof(belen)); + if (bodylen > 0 && AnserPayloadChecksumsBody(payload_type)) + COMP_CRC32C(crc, body, bodylen); + FIN_CRC32C(crc); + + return crc; +} + /* * Hand a payload to the coordinator. * From 3f743ccc3861b78092c5d9d3fc74192723bad146 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Fri, 11 Sep 2026 19:12:15 +0300 Subject: [PATCH 14/15] Add forgotten files --- gpcontrib/anser/include/anserpayload.h | 150 +++++++++++++++++++++++++ gpcontrib/anser/src/anserpayload.c | 108 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 gpcontrib/anser/include/anserpayload.h create mode 100644 gpcontrib/anser/src/anserpayload.c diff --git a/gpcontrib/anser/include/anserpayload.h b/gpcontrib/anser/include/anserpayload.h new file mode 100644 index 00000000000..cb5f30dffcf --- /dev/null +++ b/gpcontrib/anser/include/anserpayload.h @@ -0,0 +1,150 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserpayload.h + * What a channel carries, and who knows what to do with it. + * + * The transport (ansersideband.c on segments, anserdispatch.c on the + * coordinator) moves opaque bytes: it cannot tell a bloom filter from a row + * count, and should not have to. Everything that depends on what the bytes + * mean lives in one descriptor per payload type, so a new kind of runtime + * information needs no change to the framing, the checksumming or the channel + * bookkeeping. + * + * Exactly two things genuinely differ per type. + * + * How parts merge. The coordinator receives one part per producer and must + * reduce them to a single payload. For a bloom filter that is a bitwise OR of + * equal-sized bitsets; for a row count it would be a sum; for a min/max + * summary it would be two comparisons. There is no generic answer, so each + * type supplies fold(). + * + * Whether corruption can give a wrong answer. This is what 'checksum_body' + * decides, and it is about consequence, not size. A bloom filter fails + * asymmetrically: a bit flipped 0 -> 1 merely costs selectivity, but a bit + * flipped 1 -> 0 removes a key from the filter, so a joinable row is rejected + * and the query returns fewer rows than it should -- silently. A row count, by + * contrast, only feeds a planning decision: a corrupted one costs a worse plan, + * never an incorrect result. It does not need to be checksummed, and paying + * 0.05 ms/MB to checksum it anyway would be waste. + * + * Note what 'checksum_body' does *not* cover: the header and the condition key + * are checksummed for every message regardless. That is ~100 ns for a 95-byte + * header and it is what validates the routing fields, so there is nothing to + * gain by skipping it; the flag controls only the payload, which is where the + * cost lives. + * + * To add a payload type: + * + * 1. #define a code below. It travels as one byte and appears in the + * anser.debug trace, so keep it printable -- and it may not be '\0', + * which AnserPayloadTable uses to recognize a slot nobody registered. + * 2. Write a fold() and add a row to AnserPayloadTable in anserpayload.c, + * keyed by the code: the table is indexed by the wire byte, so the + * designator and the row's own 'code' field must agree (an assertion in + * AnserPayloadLookup catches it if they do not). + * 3. Pass the code to AnserSidebandPublish/AnserDispatchLocalPublish in the + * producer node, and to AnserSidebandConsumeWait/AnserDispatchLocalConsume + * in the consumer node. + * + * That is the whole extension point. In particular, do not add a branch on the + * payload type to the transport: if something there needs to know the type, + * that knowledge belongs in this descriptor instead. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserpayload.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERPAYLOAD_H +#define ANSERPAYLOAD_H + +#include "anser.h" + +/* + * Payload type codes. + * + * Distinct from the message kind (P/S), which says what a message *does* + * rather than what it *carries*: a subscription carries nothing, and a + * cancelled part still declares the type it would have carried. + */ +#define ANSER_PAYLOAD_NONE '-' /* no payload (a subscription) */ +#define ANSER_PAYLOAD_BLOOM 'B' /* serialized bloom filter part */ + +/* '\0' is reserved: it is how an unregistered code is recognized. */ + +typedef struct AnserPayloadOps +{ + char code; /* the wire byte */ + const char *name; /* for logs; no other use */ + bool checksum_body; /* see the file header */ + + /* + * Merge 'part' into the accumulator in place. + * + * The accumulator is the first part the channel received, kept verbatim, so + * the two always have the same type -- but not necessarily the same length + * or internal parameters, which is what this has to check. Return false if + * they cannot be merged; the caller then cancels the channel, which costs + * the consumers their filter but is never silently wrong. + * + * Must not modify 'accum' unless it returns true: the caller reports a + * false return and carries on, so a half-folded accumulator would be + * indistinguishable from a good one. + * + * NULL for a type that is never folded (one that has no payload). + */ + bool (*fold) (void *accum, Size accum_len, + const void *part, Size part_len); + + /* + * Is the finished accumulator worth delivering? Asked once, after the last + * part has been folded and before anything is sent to a consumer; a false + * return cancels the channel, so consumers run unfiltered. + * + * This is the only place the question can be answered honestly, because + * folding changes the answer: OR-ing bloom parts together makes the result + * denser than any part, so three parts that each looked worth sending can + * merge into one that is not. Producers cannot see that, and consumers + * should not be made to pay for it. + * + * NULL for a type that is always worth delivering -- a row count does not + * become less useful for having been summed. + */ + bool (*worth_delivering) (const void *accum, Size accum_len); +} AnserPayloadOps; + +/* + * Resolve a code to its descriptor, or NULL if nothing has registered it. + * + * Callers must handle the NULL: the code arrives from the wire, so an + * unregistered one is a real case, and defaulting it silently would turn a + * forgotten table row into a mystery rather than a log line. + */ +extern const AnserPayloadOps *AnserPayloadLookup(char code); + +/* + * Whether a type's body is covered by the message checksum. Takes a code + * rather than a descriptor because the transport reaches this while parsing, + * before it has resolved anything; an unregistered code answers false, which + * is why this one needs no error handling. + */ +extern bool AnserPayloadChecksumsBody(char code); + +#endif /* ANSERPAYLOAD_H */ diff --git a/gpcontrib/anser/src/anserpayload.c b/gpcontrib/anser/src/anserpayload.c new file mode 100644 index 00000000000..1e2852982e4 --- /dev/null +++ b/gpcontrib/anser/src/anserpayload.c @@ -0,0 +1,108 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserpayload.c + * The registry of payload types. + * + * One row per kind of runtime information a channel can carry. See + * anserpayload.h for what a row means and how to add one. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserpayload.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anserfilter.h" +#include "anserpayload.h" + +/* + * Indexed by the wire byte itself, so a lookup is one load and registering a + * type is one line (the pattern syscache.c:154 uses for its cache + * descriptors). Unregistered slots are all-zero, which is the answer we want + * for every question asked of them: no fold, no checksum, no name. + * + * Only two of the 256 slots are used, so this costs 8 KB of read-only data to + * hold mostly zeroes. That is the whole point: the alternative ways to reach a + * descriptor in constant time -- a second array mapping code to row, or a + * switch -- are extra code that has to be kept in step with the table, and the + * 8 KB buys them all away. A linear search over a two-row table would be just + * as fast in practice; what it would not be is impossible to get wrong. + */ +static const AnserPayloadOps AnserPayloadTable[UCHAR_MAX + 1] = +{ + /* + * A subscription: header and key only. It has no body to fold or to + * checksum, but its header still is checksummed -- that is what protects + * the condition key it is asking to be served on. + */ + [(unsigned char) ANSER_PAYLOAD_NONE] = { + ANSER_PAYLOAD_NONE, "none", false, NULL, NULL + }, + + /* + * A bloom filter part. AnserBloomFoldPartInPlace already has exactly the + * fold contract -- it validates both sides before touching the accumulator + * and ORs the bitsets -- so it is used directly rather than wrapped, as is + * the fill check for worth_delivering. + */ + [(unsigned char) ANSER_PAYLOAD_BLOOM] = { + ANSER_PAYLOAD_BLOOM, "bloom", true, AnserBloomFoldPartInPlace, + AnserBloomPartWorthSending + } +}; + +/* + * Resolve a code, or NULL if nothing has registered it. + * + * The NULL matters: 'code' reaches us straight off the wire, so "no such type" + * is a case that has to be reportable rather than silently defaulted. It is + * also the diagnostic that catches a payload type given a code in the header + * but no row in the table. + */ +const AnserPayloadOps * +AnserPayloadLookup(char code) +{ + /* + * Cast before indexing. 'char' is signed on x86, and the code is an + * untrusted byte: without this, a 0x80-0xff code from a corrupted or + * hostile message would index the table at a negative offset. + */ + const AnserPayloadOps *ops = &AnserPayloadTable[(unsigned char) code]; + + if (ops->code == '\0') + return NULL; + + /* The index is authoritative; a row filed under the wrong code is a typo. */ + Assert(ops->code == code); + + return ops; +} + +bool +AnserPayloadChecksumsBody(char code) +{ + /* + * No presence check: an unregistered code lands on a zeroed slot, and + * "false" is exactly right for it -- there is nothing to checksum that we + * would know how to use anyway. + */ + return AnserPayloadTable[(unsigned char) code].checksum_body; +} From 8c9a01b863eb8a4ff1315c72db35fb294e5742b0 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Fri, 11 Sep 2026 19:19:01 +0300 Subject: [PATCH 15/15] Use PG_UINT8_MAX --- gpcontrib/anser/src/anser_test.c | 15 ++++++++------- gpcontrib/anser/src/anserpayload.c | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gpcontrib/anser/src/anser_test.c b/gpcontrib/anser/src/anser_test.c index c1eccc586de..456d1a4a004 100644 --- a/gpcontrib/anser/src/anser_test.c +++ b/gpcontrib/anser/src/anser_test.c @@ -372,7 +372,7 @@ anser_test_rf_size(PG_FUNCTION_ARGS) int64 max_payload = 0; int64 planned_bytes = 0; bool injected; - Datum values[4]; + Datum values[4] = {0, 0, 0, 0}; bool nulls[4] = {false, false, false, false}; TupleDesc tupdesc; @@ -407,7 +407,7 @@ anser_test_bloom_shape(PG_FUNCTION_ARGS) int64 total_elems = PG_GETARG_INT64(0); int64 cap = PG_GETARG_INT64(1); bloom_filter *filter; - Datum values[4]; + Datum values[4] = {0, 0, 0, 0}; bool nulls[4] = {false, false, false, false}; TupleDesc tupdesc; @@ -647,10 +647,11 @@ anser_test_wire_roundtrip(PG_FUNCTION_ARGS) raw_len > 0 ? raw_body : NULL, (Size) raw_len); msg_len = strlen(msg); - /* One byte, or one length, altered -- see the case table in the test. */ - if (strcmp(tamper, "") == 0) - /* no damage */ ; - else if (strcmp(tamper, "truncate") == 0) + /* + * One byte, or one length, altered -- see the case table in the test. An + * empty 'tamper' falls through every branch and leaves the message intact. + */ + if (strcmp(tamper, "truncate") == 0) msg[msg_len - 1] = '\0'; else if (strcmp(tamper, "append") == 0) { @@ -693,7 +694,7 @@ anser_test_wire_roundtrip(PG_FUNCTION_ARGS) *first = (*first == 'A') ? 'B' : 'A'; } - else + else if (strcmp(tamper, "") != 0) elog(ERROR, "anser_test_wire_roundtrip: unknown tamper \"%s\"", tamper); if (!AnserWireParse(msg, &parsed)) diff --git a/gpcontrib/anser/src/anserpayload.c b/gpcontrib/anser/src/anserpayload.c index 1e2852982e4..2926a15a3e4 100644 --- a/gpcontrib/anser/src/anserpayload.c +++ b/gpcontrib/anser/src/anserpayload.c @@ -46,7 +46,7 @@ * 8 KB buys them all away. A linear search over a two-row table would be just * as fast in practice; what it would not be is impossible to get wrong. */ -static const AnserPayloadOps AnserPayloadTable[UCHAR_MAX + 1] = +static const AnserPayloadOps AnserPayloadTable[PG_UINT8_MAX + 1] = { /* * A subscription: header and key only. It has no body to fold or to