From 4e41ad2a97d39bf3d2cf0deb14c2c118925ead5f Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 19:19:54 +0300 Subject: [PATCH 1/8] Feature: Add Anser channel-map core Anser is a runtime pub/sub facility for MPP execution: producers publish a small piece of query information (today: a bloom filter over a join build key), the coordinator unions the per-segment parts into one global payload (bitwise OR), and consumers receive it to prune work. This commit adds the subsystem core: the coordinator-resident shared-memory channel map with a five-state channel lifecycle, the producer submission queue and consumer wait table, the gather/send background services that own the data path, the bloom part wire format and fold helpers, and the bloom filter library accessors they need. Includes postmaster/shmem/ LWLock/GUC wiring and the subsystem README. Everything is fail-open: any failure degrades to unfiltered execution, never wrong results. Disabled by default via gp_anser_enable. --- src/backend/cdb/Makefile | 2 +- src/backend/cdb/anser/Makefile | 33 + src/backend/cdb/anser/README.md | 399 +++++ src/backend/cdb/anser/anser.c | 2018 ++++++++++++++++++++++ src/backend/cdb/anser/anserfilter.c | 285 +++ src/backend/cdb/anser/anserservice.c | 203 +++ src/backend/lib/bloomfilter.c | 41 + src/backend/postmaster/bgworker.c | 7 + src/backend/postmaster/postmaster.c | 15 + src/backend/storage/ipc/ipci.c | 8 +- src/backend/storage/lmgr/lwlocknames.txt | 2 + src/backend/utils/misc/guc_gp.c | 64 + src/include/cdb/anser.h | 214 +++ src/include/cdb/anserfilter.h | 77 + src/include/lib/bloomfilter.h | 6 + src/include/postmaster/postmaster.h | 2 +- src/include/utils/unsync_guc_name.h | 5 + 17 files changed, 3378 insertions(+), 3 deletions(-) create mode 100644 src/backend/cdb/anser/Makefile create mode 100644 src/backend/cdb/anser/README.md create mode 100644 src/backend/cdb/anser/anser.c create mode 100644 src/backend/cdb/anser/anserfilter.c create mode 100644 src/backend/cdb/anser/anserservice.c create mode 100644 src/include/cdb/anser.h create mode 100644 src/include/cdb/anserfilter.h diff --git a/src/backend/cdb/Makefile b/src/backend/cdb/Makefile index dd33ce4e812..e4a42e2faf0 100644 --- a/src/backend/cdb/Makefile +++ b/src/backend/cdb/Makefile @@ -11,7 +11,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) -SUBDIRS := motion dispatcher endpoint +SUBDIRS := motion dispatcher endpoint anser OBJS = cdbappendonlystorageformat.o \ diff --git a/src/backend/cdb/anser/Makefile b/src/backend/cdb/anser/Makefile new file mode 100644 index 00000000000..90ac2e37c9a --- /dev/null +++ b/src/backend/cdb/anser/Makefile @@ -0,0 +1,33 @@ +#------------------------------------------------------------------------- +# +# 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-- +# Makefile for Anser subsystem +# +#------------------------------------------------------------------------- + +subdir = src/backend/cdb/anser +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) + +OBJS = anser.o anserservice.o anserfilter.o anserfuncs.o anserclient.o anserplan.o anserplanexec.o + +include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/cdb/anser/README.md b/src/backend/cdb/anser/README.md new file mode 100644 index 00000000000..7654ab85bdb --- /dev/null +++ b/src/backend/cdb/anser/README.md @@ -0,0 +1,399 @@ +# 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 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`. + +## 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 (`gp_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. + +### 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 +`gp_anser_producer_begin` / `gp_anser_publish` / `gp_anser_consume_wait` built-in +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 `gp_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 runs `anser_conn_authentication`: 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 gp_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 gp_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 `gp_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 | +| --- | --- | --- | --- | +| `gp_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). | +| `gp_anser_runtime_filter` | `off` | USERSET | Enables the post-planning pass that injects bloom-filter producer/consumer nodes into a matching plan. Requires `gp_anser_enable`; without it the pass is a no-op even when the subsystem is up. | +| `gp_anser_max_channels` | `0` (auto) | POSTMASTER | Number of channels the map can hold; sizes the channel hash, the producer submission queue, and (× `gp_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. | +| `gp_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. | +| `gp_anser_max_consumers_per_channel` | `64` | POSTMASTER | Wait-table slots reserved per channel; the consumer wait table is sized `gp_anser_max_channels * this`. Bounds how many consumers can block on one channel at once. | +| `gp_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 `gp_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/src/backend/cdb/anser/anser.c b/src/backend/cdb/anser/anser.c new file mode 100644 index 00000000000..845a3b940a0 --- /dev/null +++ b/src/backend/cdb/anser/anser.c @@ -0,0 +1,2018 @@ +/*------------------------------------------------------------------------- + * + * 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 + * src/backend/cdb/anser/anser.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/anser.h" +#include "cdb/anserfilter.h" +#include "cdb/cdbutil.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/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; +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 gp_anser_max_channels overrides it entirely. + */ +#define ANSER_AUTO_SLICES_PER_CONN 8 + +/* + * Inbound submission queue. + * + * A remote producer backend (gp_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 (gp_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; + +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()); + + return size; +} + +void +AnserShmemInit(void) +{ + bool found; + + if (!gp_anser_enable) + return; + + 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); +} + +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 + * gp_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 gp_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 gp_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 && AnserCtl != NULL && AnserChannelHash != NULL; +} diff --git a/src/backend/cdb/anser/anserfilter.c b/src/backend/cdb/anser/anserfilter.c new file mode 100644 index 00000000000..ff42c17ae77 --- /dev/null +++ b/src/backend/cdb/anser/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 + * src/backend/cdb/anser/anserfilter.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/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/src/backend/cdb/anser/anserservice.c b/src/backend/cdb/anser/anserservice.c new file mode 100644 index 00000000000..d2d48fd75ed --- /dev/null +++ b/src/backend/cdb/anser/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 + * src/backend/cdb/anser/anserservice.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/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/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/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index b2ab72ed950..eecd71c11b7 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -14,6 +14,7 @@ #include +#include "cdb/anser.h" #include "cdb/ic_proxy_bgworker.h" #include "access/parallel.h" @@ -166,6 +167,12 @@ static const struct { "CronBackgroundWorker", CronBackgroundWorker }, + { + "AnserGatherServiceMain", AnserGatherServiceMain + }, + { + "AnserSendServiceMain", AnserSendServiceMain + }, #ifdef ENABLE_IC_PROXY { "ICProxyMain", ICProxyMain diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 12c8649b159..7ca9a6cf647 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -153,6 +153,7 @@ #include "cdb/cdbgang.h" /* cdbgang_parse_gpqeid_params */ #include "cdb/cdbtm.h" #include "cdb/cdbvars.h" +#include "cdb/anser.h" #include "cdb/cdbendpoint.h" #include "cdb/ic_proxy_bgworker.h" #include "cdb/ml_ipc.h" @@ -446,6 +447,20 @@ static BackgroundWorker PMAuxProcList[MaxPMAuxProc] = "postgres", "PgCronLauncherMain", 0, {0}, 0, PgCronStartRule}, + {"anser gather service", "anser gather service", + BGWORKER_SHMEM_ACCESS, + BgWorkerStart_RecoveryFinished, + 1, + "postgres", "AnserGatherServiceMain", 0, {0}, 0, + AnserStartRule}, + + {"anser send service", "anser send service", + BGWORKER_SHMEM_ACCESS, + BgWorkerStart_RecoveryFinished, + 1, + "postgres", "AnserSendServiceMain", 0, {0}, 0, + AnserStartRule}, + #ifdef ENABLE_IC_PROXY {"ic proxy process", "ic proxy process", 0, diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index e0ced5a583e..7110f354570 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -26,6 +26,7 @@ #include "access/syncscan.h" #include "access/twophase.h" #include "access/distributedlog.h" +#include "cdb/anser.h" #include "cdb/cdblocaldistribxact.h" #include "cdb/cdbvars.h" #include "access/xlogprefetcher.h" @@ -222,8 +223,9 @@ CalculateShmemSize(int *num_semaphores) /* size of expand version */ size = add_size(size, GpExpandVersionShmemSize()); - /* size of token and endpoint shared memory */ + /* size of token, endpoint and Anser shared memory */ size = add_size(size, EndpointShmemSize()); + size = add_size(size, AnserShmemSize()); #ifndef USE_INTERNAL_FTS /* size of cdb etcd result cache */ if (Gp_role != GP_ROLE_EXECUTE) @@ -444,6 +446,10 @@ CreateSharedMemoryAndSemaphores(void) /* Initialize shared memory for parallel retrieve cursor */ if (!IsUnderPostmaster) EndpointShmemInit(); + + /* Initialize shared memory for Anser adaptive information sharing. */ + if (!IsUnderPostmaster) + AnserShmemInit(); #ifndef USE_INTERNAL_FTS /* Initialize shared memory for cdb etcd cache */ if (Gp_role != GP_ROLE_EXECUTE) diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt index c3583b146d7..96dcbb0597f 100644 --- a/src/backend/storage/lmgr/lwlocknames.txt +++ b/src/backend/storage/lmgr/lwlocknames.txt @@ -76,3 +76,5 @@ LoginFailedSharedMemoryLock 66 GPIVMResLock 67 DirectoryTableLock 68 CommittedGxidArrayLock 69 +AnserChannelLock 70 +AnserRingLock 71 diff --git a/src/backend/utils/misc/guc_gp.c b/src/backend/utils/misc/guc_gp.c index 4312c86ad15..e45b5d98373 100644 --- a/src/backend/utils/misc/guc_gp.c +++ b/src/backend/utils/misc/guc_gp.c @@ -24,6 +24,7 @@ #include "access/transam.h" #include "access/url.h" #include "access/xlog_internal.h" +#include "cdb/anser.h" #include "cdb/cdbappendonlyam.h" #include "cdb/cdbendpoint.h" #include "cdb/cdbdisp.h" @@ -621,6 +622,17 @@ struct config_bool ConfigureNamesBool_gp[] = NULL, NULL, NULL }, + { + {"gp_anser_enable", PGC_POSTMASTER, CUSTOM_OPTIONS, + gettext_noop("Enables the Anser adaptive information sharing subsystem."), + gettext_noop("When disabled, Anser does not allocate shared memory and its background services are not started."), + GUC_NOT_IN_SAMPLE + }, + &gp_anser_enable, + false, + NULL, NULL, NULL + }, + { {"gp_maintenance_conn", PGC_BACKEND, CUSTOM_OPTIONS, gettext_noop("Maintenance Connection"), @@ -3429,6 +3441,58 @@ struct config_int ConfigureNamesInt_gp[] = NULL, NULL, NULL }, + { + {"gp_anser_max_channels", PGC_POSTMASTER, CUSTOM_OPTIONS, + gettext_noop("Sets the maximum number of Anser channels."), + gettext_noop("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."), + GUC_NOT_IN_SAMPLE + }, + &gp_anser_max_channels, + 0, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"gp_anser_max_info_size", PGC_POSTMASTER, CUSTOM_OPTIONS, + gettext_noop("Sets the maximum byte size of one Anser information record."), + gettext_noop("Per-record DSM payload cap for Anser information. The default holds a full 64 MB bloom-filter bitset plus its serialized-part header."), + GUC_NOT_IN_SAMPLE + }, + &gp_anser_max_info_size, + 64 * 1024 * 1024 + 1024 * 1024, 1, INT_MAX, + NULL, NULL, NULL + }, + + { + {"gp_anser_timeout_ms", PGC_USERSET, CUSTOM_OPTIONS, + gettext_noop("Sets how long Anser consumers wait for producer registration."), + gettext_noop("After producer registration, consumers wait for data without this timeout and rely on query cancellation or channel cancellation."), + GUC_UNIT_MS | GUC_NOT_IN_SAMPLE + }, + &gp_anser_timeout_ms, + 1000, 0, INT_MAX, + NULL, NULL, NULL + }, + + { + {"gp_anser_max_consumers_per_channel", PGC_POSTMASTER, CUSTOM_OPTIONS, + gettext_noop("Sets the maximum number of waiting Anser consumers per channel."), + gettext_noop("This value sizes the fixed Anser consumer wait table at postmaster start " + "(gp_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."), + GUC_NOT_IN_SAMPLE + }, + &gp_anser_max_consumers_per_channel, + 64, 1, INT_MAX, + NULL, NULL, NULL + }, + { {"gp_predicate_pushdown_sample_rows", PGC_USERSET, DEVELOPER_OPTIONS, gettext_noop("Max sample rows during predicate pushdown"), diff --git a/src/include/cdb/anser.h b/src/include/cdb/anser.h new file mode 100644 index 00000000000..3cf1b55a241 --- /dev/null +++ b/src/include/cdb/anser.h @@ -0,0 +1,214 @@ +/*------------------------------------------------------------------------- + * + * 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 + * src/include/cdb/anser.h + * + *------------------------------------------------------------------------- + */ +#ifndef CDB_ANSER_H +#define CDB_ANSER_H + +#include "postgres.h" + +#include "datatype/timestamp.h" +#include "storage/dsm.h" +#include "storage/latch.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 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; + +/* Shared-memory setup. */ +extern Size AnserShmemSize(void); +extern void AnserShmemInit(void); + +/* + * Effective channel-map size (gp_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 gp_anser_* built-in 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. */ +extern void AnserGatherServiceMain(Datum main_arg); +extern void AnserSendServiceMain(Datum main_arg); +extern bool AnserStartRule(Datum main_arg); + +#endif /* CDB_ANSER_H */ diff --git a/src/include/cdb/anserfilter.h b/src/include/cdb/anserfilter.h new file mode 100644 index 00000000000..c66eabb61b7 --- /dev/null +++ b/src/include/cdb/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 + * src/include/cdb/anserfilter.h + * + *------------------------------------------------------------------------- + */ +#ifndef CDB_ANSERFILTER_H +#define CDB_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 /* CDB_ANSERFILTER_H */ 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/postmaster/postmaster.h b/src/include/postmaster/postmaster.h index 0683e56cd3b..64093a9e045 100644 --- a/src/include/postmaster/postmaster.h +++ b/src/include/postmaster/postmaster.h @@ -107,6 +107,6 @@ extern bool amAuxiliaryBgWorker(void); */ #define MAX_BACKENDS 0x3FFFF -#define MaxPMAuxProc (4 + IC_PROXY_NUM_BGWORKER + FTS_NUM_BGWORKER) +#define MaxPMAuxProc (6 + IC_PROXY_NUM_BGWORKER + FTS_NUM_BGWORKER) #endif /* _POSTMASTER_H */ diff --git a/src/include/utils/unsync_guc_name.h b/src/include/utils/unsync_guc_name.h index 02b04fdc5c4..7f146491450 100644 --- a/src/include/utils/unsync_guc_name.h +++ b/src/include/utils/unsync_guc_name.h @@ -161,6 +161,11 @@ "geqo_threshold", "gp_adjust_selectivity_for_outerjoins", "gp_allow_non_uniform_partitioning_ddl", + "gp_anser_enable", + "gp_anser_max_channels", + "gp_anser_max_consumers_per_channel", + "gp_anser_max_info_size", + "gp_anser_timeout_ms", "gp_auth_time_override", "gp_autostats_allow_nonowner", "gp_autostats_lock_wait", From acf6782d95bcd2a6b640047fde1b49e730faf347 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 19:25:22 +0300 Subject: [PATCH 2/8] Feature: Anser network transport with token auth Segment producers/consumers cannot touch the coordinator-resident channel map directly, so they open a libpq connection back to the QD (address from gp_qd_hostname/gp_qd_port) and drive the new gp_anser_producer_begin / gp_anser_publish / gp_anser_consume_wait builtins. Since stock clusters never grant segment hosts in the coordinator's pg_hba.conf, these connections authenticate with a per-session random token instead, following the parallel-retrieve cursor model: the QD registers a 128-bit token in a shared-memory session token hash, a gp_anser_conn startup marker selects token authentication in ClientAuthentication before pg_hba is consulted, and the token is presented as the connection password. Channels stay bound to their creator role. All paths fail open to unfiltered execution. --- src/backend/cdb/anser/anser.c | 191 +++++++++++- src/backend/cdb/anser/anserclient.c | 452 ++++++++++++++++++++++++++++ src/backend/cdb/anser/anserfuncs.c | 196 ++++++++++++ src/backend/libpq/auth.c | 165 ++++++++++ src/backend/utils/misc/guc_gp.c | 11 + src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_proc.dat | 17 ++ src/include/cdb/anser.h | 10 + src/include/cdb/anserclient.h | 60 ++++ src/include/utils/unsync_guc_name.h | 1 + 10 files changed, 1103 insertions(+), 2 deletions(-) create mode 100644 src/backend/cdb/anser/anserclient.c create mode 100644 src/backend/cdb/anser/anserfuncs.c create mode 100644 src/include/cdb/anserclient.h diff --git a/src/backend/cdb/anser/anser.c b/src/backend/cdb/anser/anser.c index 845a3b940a0..b8ecb30cb74 100644 --- a/src/backend/cdb/anser/anser.c +++ b/src/backend/cdb/anser/anser.c @@ -31,6 +31,8 @@ #include "cdb/anser.h" #include "cdb/anserfilter.h" #include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "common/hashfn.h" #include "miscadmin.h" #include "storage/dsm_impl.h" #include "storage/ipc.h" @@ -38,6 +40,7 @@ #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" @@ -47,9 +50,41 @@ #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" +#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 gp_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; 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; @@ -125,6 +160,10 @@ static AnserControl *AnserCtl = NULL; static HTAB *AnserChannelHash = NULL; static AnserSubmissionEntry *AnserSubmissionQueue = NULL; static AnserWaitSlot *AnserWaitTable = NULL; +static HTAB *AnserTokenHash = NULL; + +/* Set once this backend has registered its session-token cleanup hook. */ +static bool anser_token_exit_registered = false; /* * Data-path operations -- the internal machinery the public API and the gather/ @@ -181,6 +220,8 @@ static void AnserInitializeControl(bool found); static void AnserInitializeChannelHash(void); static void AnserInitializeSubmissionQueue(bool found); static void AnserInitializeWaitTable(bool found); +static void AnserInitializeTokenHash(void); +static void AnserTokenSessionCleanup(int code, Datum arg); Size AnserShmemSize(void) @@ -194,6 +235,8 @@ AnserShmemSize(void) size = add_size(size, AnserChannelHashSize()); size = add_size(size, AnserSubmissionQueueSize()); size = add_size(size, AnserWaitTableSize()); + size = add_size(size, hash_estimate_size(MaxConnections, + sizeof(AnserTokenEntry))); return size; } @@ -221,6 +264,8 @@ AnserShmemInit(void) ShmemInitStruct(ANSER_WAIT_TABLE_NAME, AnserWaitTableSize(), &found); AnserInitializeWaitTable(found); + + AnserInitializeTokenHash(); } bool @@ -1748,6 +1793,149 @@ AnserInitializeChannelHash(void) HASH_ELEM | HASH_BLOBS); } +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 (!AnserInitialized() || !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 for the gp_anser_conn authentication branch in + * libpq/auth.c: 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 (!AnserInitialized() || !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; +} + /* * Recycle terminal or orphaned channels before declaring registration failure. */ @@ -2014,5 +2202,6 @@ AnserWaitForState(const AnserChannelKey *channel_key, long timeout_ms, static bool AnserInitialized(void) { - return gp_anser_enable && AnserCtl != NULL && AnserChannelHash != NULL; + return gp_anser_enable && AnserCtl != NULL && AnserChannelHash != NULL && + AnserTokenHash != NULL; } diff --git a/src/backend/cdb/anser/anserclient.c b/src/backend/cdb/anser/anserclient.c new file mode 100644 index 00000000000..102254f466d --- /dev/null +++ b/src/backend/cdb/anser/anserclient.c @@ -0,0 +1,452 @@ +/*------------------------------------------------------------------------- + * + * 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 + * gp_anser_* built-in functions. When the QD supplied a session token (carried + * in the plan), the connection authenticates with it via the gp_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 + * src/backend/cdb/anser/anserclient.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq-fe.h" + +#include "cdb/anser.h" +#include "cdb/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 gp_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 + * anser_conn_authentication in libpq/auth.c) -- 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 gp_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 gp_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 gp_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 gp_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 + * gp_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 gp_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/src/backend/cdb/anser/anserfuncs.c b/src/backend/cdb/anser/anserfuncs.c new file mode 100644 index 00000000000..47ed3f10efe --- /dev/null +++ b/src/backend/cdb/anser/anserfuncs.c @@ -0,0 +1,196 @@ +/*------------------------------------------------------------------------- + * + * 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 + * Built-in 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 builtins; + * all real work happens in the gather and send background services. A producer + * announces itself with gp_anser_producer_begin(), streams parts with + * gp_anser_publish(), and a consumer blocks in gp_anser_consume_wait() until the + * send service delivers its payload (or cancels it). + * + * Access control: these functions are intentionally granted to PUBLIC (their + * pg_proc.dat entries carry no proacl, so they inherit the default EXECUTE grant + * to PUBLIC). The network transport connects to the QD as the *query's own* + * role, so restricting the functions 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 builtin 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 + * src/backend/cdb/anser/anserfuncs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/anser.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "varatt.h" + +static bool anser_builtin_build_key(int32 gp_session_id, int32 gp_command_count, + int32 condition_id, text *condition_key_text, + AnserChannelKey *key); + +/* + * gp_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 gp_anser_timeout_ms the gather service cancels the whole dataset. + */ +Datum +gp_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_builtin_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())); +} + +/* + * gp_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: gp_anser_producer_begin already stamped it on the + * channel, so we pass 0 to leave it unchanged. + */ +Datum +gp_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_builtin_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())); +} + +/* + * gp_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 +gp_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_builtin_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_builtin_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_builtin_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/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index b6021e169dd..6b48e8a3468 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -49,6 +49,7 @@ #include "catalog/pg_auth_time_constraint.h" #include "catalog/pg_profile.h" #include "cdb/cdbendpoint.h" +#include "cdb/anser.h" #include "cdb/cdbvars.h" #include "pgtime.h" #include "postmaster/loginmonitor.h" @@ -499,6 +500,93 @@ cmd_options_include_retrieve_conn(char* cmd_options) return ret; } +/* + * Return true if command line contains gp_anser_conn=true. Mirrors + * cmd_options_include_retrieve_conn() above. + */ +static bool +cmd_options_include_anser_conn(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, "gp_anser_conn") == 0) && + !parse_bool(value, &ret)) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid value for guc gp_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; +} + static bool guc_options_include_retrieve_conn(List *guc_options) { @@ -532,6 +620,43 @@ guc_options_include_retrieve_conn(List *guc_options) return ret; } +/* + * Return true if startup GUC options contain gp_anser_conn=true. Mirrors + * guc_options_include_retrieve_conn() above. + */ +static bool +guc_options_include_anser_conn(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, "gp_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 gp_anser_conn: \"%s\"", + value))); + + } + } + + return ret; +} + /* * Retrieve role directly uses the token of PARALLEL RETRIEVE CURSOR as password to authenticate. */ @@ -559,6 +684,35 @@ retrieve_conn_authentication(Port *port) FakeClientAuthentication(port); } +/* + * Anser backward (segment -> QD) connections authenticate with the per-session + * Anser token as password, bypassing pg_hba -- the same model as + * retrieve_conn_authentication() above. The QD registers the token in shared + * memory when a plan gets a runtime filter; segment executors present it when + * publishing/consuming bloom filter parts. On success the connection becomes + * an ordinary backend for the session user, which then runs the gp_anser_* + * builtins (anserfuncs.c). + */ +static void +anser_conn_authentication(Port *port) +{ + char *passwd; + Oid owner_uid; + const char *msg1 = "Failed to retrieve the authentication password"; + const char *msg2 = "Authentication failure (invalid Anser session token)"; + + sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); + passwd = recv_password_packet(port); + if (passwd == NULL) + ereport(FATAL, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("%s", msg1))); + + owner_uid = get_role_oid(port->user_name, false); + if (!AnserSessionTokenIsValid(owner_uid, 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 +872,17 @@ ClientAuthentication(Port *port) return; } + /* + * For Anser segment -> QD runtime-filter connections, per-session token + * authentication is performed, likewise before pg_hba is consulted. + */ + if (cmd_options_include_anser_conn(port->cmdline_options) || + guc_options_include_anser_conn(port->guc_options)) + { + anser_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/utils/misc/guc_gp.c b/src/backend/utils/misc/guc_gp.c index e45b5d98373..07ad43783fb 100644 --- a/src/backend/utils/misc/guc_gp.c +++ b/src/backend/utils/misc/guc_gp.c @@ -1025,6 +1025,17 @@ struct config_bool ConfigureNamesBool_gp[] = NULL, NULL, NULL }, + { + {"gp_anser_conn", PGC_BACKEND, GP_WORKER_IDENTITY, + gettext_noop("Specify this is a connection for the Anser runtime filter transport"), + NULL, + GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NO_RESET_ALL + }, + &gp_anser_conn, + false, + NULL, NULL, NULL + }, + { {"gp_cost_hashjoin_chainwalk", PGC_USERSET, QUERY_TUNING_COST, diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 851e58debc3..eba07adaed6 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -60,6 +60,6 @@ */ /* 3yyymmddN */ -#define CATALOG_VERSION_NO 302606111 +#define CATALOG_VERSION_NO 302608181 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 0e0dcb0dc5d..8dc20f0b7a6 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -13019,3 +13019,20 @@ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{queueid,queueid,queries_submitted,queries_admitted,queries_rejected,queries_completed,elapsed_wait_secs,max_wait_secs,elapsed_exec_secs,max_exec_secs,total_cost,total_memory_kb,stat_reset_timestamp,have_stats}', prosrc => 'pg_stat_get_resqueue_stats' }, + +# Anser adaptive-information-sharing network transport. +{ oid => '8195', descr => 'Anser: register a producer condition and arm the produce deadline', + proname => 'gp_anser_producer_begin', provolatile => 'v', proparallel => 'u', + proisstrict => 't', prorettype => 'bool', + proargtypes => 'int4 int4 int4 text int4', + proexeclocation => 'c', prosrc => 'gp_anser_producer_begin' }, +{ oid => '8196', descr => 'Anser: publish one producer part to the gather service', + proname => 'gp_anser_publish', provolatile => 'v', proparallel => 'u', + proisstrict => 't', prorettype => 'bool', + proargtypes => 'int4 int4 int4 text bytea bool', + proexeclocation => 'c', prosrc => 'gp_anser_publish' }, +{ oid => '8197', descr => 'Anser: wait for delivery of a channel payload to a consumer', + proname => 'gp_anser_consume_wait', provolatile => 'v', proparallel => 'u', + proisstrict => 't', prorettype => 'bytea', + proargtypes => 'int4 int4 int4 text', + proexeclocation => 'c', prosrc => 'gp_anser_consume_wait' }, diff --git a/src/include/cdb/anser.h b/src/include/cdb/anser.h index 3cf1b55a241..bbf21b0b7b6 100644 --- a/src/include/cdb/anser.h +++ b/src/include/cdb/anser.h @@ -135,6 +135,7 @@ typedef struct 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; @@ -211,4 +212,13 @@ extern void AnserGatherServiceMain(Datum main_arg); extern 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; libpq/auth.c calls + * AnserSessionTokenIsValid from the gp_anser_conn authentication branch. + */ +extern char *AnserGetOrCreateSessionToken(Oid user_id); +extern bool AnserSessionTokenIsValid(Oid user_id, const char *token_hex); + #endif /* CDB_ANSER_H */ diff --git a/src/include/cdb/anserclient.h b/src/include/cdb/anserclient.h new file mode 100644 index 00000000000..85b9f49ad04 --- /dev/null +++ b/src/include/cdb/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 + * src/include/cdb/anserclient.h + * + *------------------------------------------------------------------------- + */ +#ifndef CDB_ANSERCLIENT_H +#define CDB_ANSERCLIENT_H + +#include "cdb/anser.h" + +/* + * Publish one producer part to the coordinator. Opens a short-lived libpq + * connection to the QD, runs gp_anser_producer_begin + gp_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: gp_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 gp_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 /* CDB_ANSERCLIENT_H */ diff --git a/src/include/utils/unsync_guc_name.h b/src/include/utils/unsync_guc_name.h index 7f146491450..c7b9529cd95 100644 --- a/src/include/utils/unsync_guc_name.h +++ b/src/include/utils/unsync_guc_name.h @@ -282,6 +282,7 @@ "gp_resqueue_priority_local_interval", "gp_resqueue_priority_sweeper_interval", "gp_retrieve_conn", + "gp_anser_conn", "gp_role", "gp_internal_is_singlenode", "gp_segment_connect_timeout", From c4e795f3adb43c35f186ab3af896a070ecd480ac Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 19:28:25 +0300 Subject: [PATCH 3/8] Feature: Inject Anser runtime bloom filters Add the planner integration that puts Anser to work: a post-plan hook in planner() (covers both the Postgres planner and ORCA) recognizes a single-column equijoin over a hash build and injects two pass-through CustomScan providers. The producer above the build scan feeds join keys into a bloom filter and publishes it to the channel; the consumer above the probe scan receives the unioned filter and prunes rows that cannot join. Executor helper library selects the transport by role (coordinator: direct shmem; segment: libpq to the QD with the session token carried in the plan). EXPLAIN reports planned filter size and, under ANALYZE, winner-segment lookup/prune counters via instrumentation. Adds the src/test/modules/anser regression module (channel map, services, bloom protocol, client loopback, session token, plan injection) and wires it into CI as the ic-anser test with gp_anser_enable=on. --- .github/workflows/build-cloudberry.yml | 13 + src/backend/cdb/anser/anserplan.c | 411 +++++ src/backend/cdb/anser/anserplanexec.c | 611 +++++++ src/backend/executor/Makefile | 2 + .../executor/nodeAnserBloomFilterConsume.c | 240 +++ .../executor/nodeAnserBloomFilterProduce.c | 168 ++ src/backend/optimizer/plan/planner.c | 9 + src/backend/utils/init/postinit.c | 8 + src/backend/utils/misc/guc_gp.c | 11 + src/include/cdb/anserplan.h | 74 + src/include/executor/nodeAnserBloomFilter.h | 81 + src/include/utils/unsync_guc_name.h | 1 + src/test/modules/Makefile | 1 + src/test/modules/anser/Makefile | 50 + src/test/modules/anser/anser_test.c | 1413 +++++++++++++++++ .../anser/expected/anser_runtime_filter.out | 121 ++ .../modules/anser/expected/test_anser.out | 334 ++++ src/test/modules/anser/meson.build | 51 + .../anser/sql/anser_runtime_filter.sql | 69 + src/test/modules/anser/sql/test_anser.sql | 116 ++ src/test/modules/anser/test_anser--1.0.sql | 149 ++ src/test/modules/anser/test_anser.control | 5 + src/test/modules/meson.build | 1 + 23 files changed, 3939 insertions(+) create mode 100644 src/backend/cdb/anser/anserplan.c create mode 100644 src/backend/cdb/anser/anserplanexec.c create mode 100644 src/backend/executor/nodeAnserBloomFilterConsume.c create mode 100644 src/backend/executor/nodeAnserBloomFilterProduce.c create mode 100644 src/include/cdb/anserplan.h create mode 100644 src/include/executor/nodeAnserBloomFilter.h create mode 100644 src/test/modules/anser/Makefile create mode 100644 src/test/modules/anser/anser_test.c create mode 100644 src/test/modules/anser/expected/anser_runtime_filter.out create mode 100644 src/test/modules/anser/expected/test_anser.out create mode 100644 src/test/modules/anser/meson.build create mode 100644 src/test/modules/anser/sql/anser_runtime_filter.sql create mode 100644 src/test/modules/anser/sql/test_anser.sql create mode 100644 src/test/modules/anser/test_anser--1.0.sql create mode 100644 src/test/modules/anser/test_anser.control diff --git a/.github/workflows/build-cloudberry.yml b/.github/workflows/build-cloudberry.yml index 7abb658632d..03efd3ca0d6 100644 --- a/.github/workflows/build-cloudberry.yml +++ b/.github/workflows/build-cloudberry.yml @@ -243,6 +243,10 @@ jobs: "make_configs":["src/test/regress:installcheck-good"], "pg_settings":{"optimizer":"on"} }, + {"test":"ic-anser", + "make_configs":["src/test/modules/anser:installcheck"], + "postgres_conf_addons":"gp_anser_enable=on" + }, {"test":"pax-ic-good-opt-off", "make_configs":[ "contrib/pax_storage/:pax-test", @@ -1447,6 +1451,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/src/backend/cdb/anser/anserplan.c b/src/backend/cdb/anser/anserplan.c new file mode 100644 index 00000000000..c618cdd48c0 --- /dev/null +++ b/src/backend/cdb/anser/anserplan.c @@ -0,0 +1,411 @@ +/*------------------------------------------------------------------------- + * + * 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 src/include/cdb/anserplan.h for the + * rationale. + * + * IDENTIFICATION + * src/backend/cdb/anser/anserplan.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/anser.h" +#include "cdb/anserplan.h" +#include "cdb/cdbvars.h" +#include "nodes/nodeFuncs.h" +#include "nodes/pg_list.h" +#include "utils/acl.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 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; + + { + 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); + } +} + +/* + * 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 meaningful + * for a pass-by-value key type (the value lives in the Datum, not behind a + * pointer). Restrict to those. + */ + if (!get_typbyval(inner_var->vartype)) + 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 + * gp_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/src/backend/cdb/anser/anserplanexec.c b/src/backend/cdb/anser/anserplanexec.c new file mode 100644 index 00000000000..130441e4bcc --- /dev/null +++ b/src/backend/cdb/anser/anserplanexec.c @@ -0,0 +1,611 @@ +/*------------------------------------------------------------------------- + * + * 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 + * executor/nodeAnserBloomFilter.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 + * src/backend/cdb/anser/anserplanexec.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/anser.h" +#include "cdb/anserfilter.h" +#include "cdb/anserplan.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "commands/explain.h" +#include "executor/executor.h" +#include "executor/nodeAnserBloomFilter.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" + +/* + * 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? */ +} 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; + + 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)); +} + +static TupleTableSlot * +anser_consume_next(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + PlanState *child = (PlanState *) linitial(node->custom_ps); + + anser_consume_receive(st); + + for (;;) + { + TupleTableSlot *slot = ExecProcNode(child); + bool isnull; + Datum value; + + if (TupIsNull(slot)) + return NULL; + + /* Fail open: no usable filter -> pass everything. */ + if (st->filter == NULL) + 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; + + if (st->consume != NULL) + ExecEndAnserBloomFilterConsume(st->consume); + st->consume = NULL; + st->filter = NULL; + if (node->custom_ps != NIL) + ExecEndNode((PlanState *) linitial(node->custom_ps)); +} + +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). + */ + 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 (nloops > 0) + ExplainPropertyFloat("Rows Removed by Bloom Filter", NULL, + nfiltered / nloops, 0, es); + } +} diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 1c0ff9088ba..f440ca7b6eb 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -35,6 +35,8 @@ OBJS = \ functions.o \ instrument.o \ nodeAgg.o \ + nodeAnserBloomFilterConsume.o \ + nodeAnserBloomFilterProduce.o \ nodeAppend.o \ nodeBitmapAnd.o \ nodeBitmapHeapscan.o \ diff --git a/src/backend/executor/nodeAnserBloomFilterConsume.c b/src/backend/executor/nodeAnserBloomFilterConsume.c new file mode 100644 index 00000000000..28fa213f998 --- /dev/null +++ b/src/backend/executor/nodeAnserBloomFilterConsume.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. + * + * nodeAnserBloomFilterConsume.c + * Standalone Anser Bloom filter consumer executor helper. + * + * IDENTIFICATION + * src/backend/executor/nodeAnserBloomFilterConsume.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/anser.h" +#include "cdb/anserclient.h" +#include "cdb/anserfilter.h" +#include "cdb/cdbvars.h" +#include "executor/nodeAnserBloomFilter.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/src/backend/executor/nodeAnserBloomFilterProduce.c b/src/backend/executor/nodeAnserBloomFilterProduce.c new file mode 100644 index 00000000000..624b8b36f9e --- /dev/null +++ b/src/backend/executor/nodeAnserBloomFilterProduce.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. + * + * nodeAnserBloomFilterProduce.c + * Standalone Anser Bloom filter producer executor helper. + * + * IDENTIFICATION + * src/backend/executor/nodeAnserBloomFilterProduce.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "cdb/anser.h" +#include "cdb/anserclient.h" +#include "cdb/anserfilter.h" +#include "cdb/cdbvars.h" +#include "executor/nodeAnserBloomFilter.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/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 94e8f4ff8a0..92ade925a42 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -73,6 +73,7 @@ #include "utils/syscache.h" #include "catalog/pg_proc.h" +#include "cdb/anserplan.h" #include "cdb/cdbhash.h" #include "cdb/cdbllize.h" #include "cdb/cdbmutate.h" /* apply_shareinput */ @@ -361,6 +362,14 @@ planner(Query *parse, const char *query_string, int cursorOptions, } else result = standard_planner(parse, query_string, cursorOptions, boundParams, optimizer_options); + + /* + * Single post-plan hook covering both ORCA and the Postgres planner: inject + * Anser runtime bloom-filter nodes into the finished plan tree. A no-op + * unless the Anser runtime-filter GUCs are on (see anserplan.c). + */ + AnserApplyRuntimeFilters(result); + pfree(optimizer_options); return result; } diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b769c3b249f..0a352ed5fbb 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -46,6 +46,7 @@ #include "libpq/auth.h" #include "libpq/hba.h" #include "libpq/libpq-be.h" +#include "cdb/anserplan.h" #include "cdb/cdbendpoint.h" #include "cdb/cdbtm.h" #include "cdb/cdbvars.h" @@ -1398,6 +1399,13 @@ InitPostgres(const char *in_dbname, Oid dboid, /* Initialize this backend's session state. */ InitializeSession(); + /* + * Register the Anser runtime-filter CustomScan providers so their methods + * resolve by name when a dispatched plan carrying them is deserialized. + * Harmless (a hash insert) when the feature is unused. + */ + AnserRegisterRuntimeFilterMethods(); + /* * report this backend in the PgBackendStatus array, meanwhile, we do not * want users to see auxiliary background worker like fts in pg_stat_* views. diff --git a/src/backend/utils/misc/guc_gp.c b/src/backend/utils/misc/guc_gp.c index 07ad43783fb..da36eb3f495 100644 --- a/src/backend/utils/misc/guc_gp.c +++ b/src/backend/utils/misc/guc_gp.c @@ -633,6 +633,17 @@ struct config_bool ConfigureNamesBool_gp[] = NULL, NULL, NULL }, + { + {"gp_anser_runtime_filter", PGC_USERSET, QUERY_TUNING_METHOD, + gettext_noop("Enables Anser-backed runtime bloom filter helpers."), + gettext_noop("This GUC is a no-op until Anser bloom filter plan nodes are inserted into the plan tree."), + GUC_NOT_IN_SAMPLE + }, + &gp_anser_runtime_filter, + false, + NULL, NULL, NULL + }, + { {"gp_maintenance_conn", PGC_BACKEND, CUSTOM_OPTIONS, gettext_noop("Maintenance Connection"), diff --git a/src/include/cdb/anserplan.h b/src/include/cdb/anserplan.h new file mode 100644 index 00000000000..0c472241862 --- /dev/null +++ b/src/include/cdb/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 + * src/include/cdb/anserplan.h + * + *------------------------------------------------------------------------- + */ +#ifndef CDB_ANSERPLAN_H +#define CDB_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 /* CDB_ANSERPLAN_H */ diff --git a/src/include/executor/nodeAnserBloomFilter.h b/src/include/executor/nodeAnserBloomFilter.h new file mode 100644 index 00000000000..bb784d5596c --- /dev/null +++ b/src/include/executor/nodeAnserBloomFilter.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. + * + * nodeAnserBloomFilter.h + * Standalone Anser Bloom filter producer/consumer executor helpers. + * + * IDENTIFICATION + * src/include/executor/nodeAnserBloomFilter.h + * + *------------------------------------------------------------------------- + */ +#ifndef NODE_ANSER_BLOOM_FILTER_H +#define NODE_ANSER_BLOOM_FILTER_H + +#include "postgres.h" + +#include "cdb/anser.h" +#include "lib/bloomfilter.h" + +/* + * Opaque executor state handles for the Bloom filter producer and + * consumer; the struct definitions are private to + * nodeAnserBloomFilterProduce.c and nodeAnserBloomFilterConsume.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 /* NODE_ANSER_BLOOM_FILTER_H */ diff --git a/src/include/utils/unsync_guc_name.h b/src/include/utils/unsync_guc_name.h index c7b9529cd95..9b42335a351 100644 --- a/src/include/utils/unsync_guc_name.h +++ b/src/include/utils/unsync_guc_name.h @@ -165,6 +165,7 @@ "gp_anser_max_channels", "gp_anser_max_consumers_per_channel", "gp_anser_max_info_size", + "gp_anser_runtime_filter", "gp_anser_timeout_ms", "gp_auth_time_override", "gp_autostats_allow_nonowner", diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 77af107b55e..b38365cf6b7 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -5,6 +5,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global SUBDIRS = \ + anser \ brin \ commit_ts \ delay_execution \ diff --git a/src/test/modules/anser/Makefile b/src/test/modules/anser/Makefile new file mode 100644 index 00000000000..25731f87e16 --- /dev/null +++ b/src/test/modules/anser/Makefile @@ -0,0 +1,50 @@ +#------------------------------------------------------------------------- +# +# 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. +# +# src/test/modules/anser/Makefile +# +#------------------------------------------------------------------------- + +MODULE_big = test_anser +OBJS = \ + $(WIN32RES) \ + anser_test.o +PGFILEDESC = "test_anser - test helpers for Anser" + +# anser_test.c drives loopback libpq connections for the client/multi-consumer +# round-trip tests. +PG_CPPFLAGS = -I$(libpq_srcdir) +SHLIB_LINK_INTERNAL = $(libpq) +SHLIB_PREREQS = submake-libpq + +EXTENSION = test_anser +DATA = test_anser--1.0.sql + +REGRESS = test_anser anser_runtime_filter + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/anser +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/anser/anser_test.c b/src/test/modules/anser/anser_test.c new file mode 100644 index 00000000000..3c834b86e90 --- /dev/null +++ b/src/test/modules/anser/anser_test.c @@ -0,0 +1,1413 @@ +/*------------------------------------------------------------------------- + * + * 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 + * src/test/modules/anser/anser_test.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq-fe.h" + +#include "cdb/anser.h" +#include "cdb/anserclient.h" +#include "cdb/anserfilter.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "commands/dbcommands.h" +#include "executor/nodeAnserBloomFilter.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" + +PG_MODULE_MAGIC; + +/* + * 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 gp_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(); + { + 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("gp_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 > gp_anser_timeout_ms (100 ms) */ + AnserGatherServiceCycle(); + + freed_after_timeout = AnserChannelPayloadBytes(&key) <= 0; + } + } + PG_FINALLY(); + { + SetConfigOption("gp_anser_timeout_ms", saved_timeout, + PGC_USERSET, PGC_S_SESSION); + AnserSetSweepEnabled(true); + AnserServiceMaintenance(); /* reclaim the cancelled entry */ + AnserSetSweepEnabled(false); + } + 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(); + { + 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 gp_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 gp_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/src/test/modules/anser/expected/anser_runtime_filter.out b/src/test/modules/anser/expected/anser_runtime_filter.out new file mode 100644 index 00000000000..998acf20b9e --- /dev/null +++ b/src/test/modules/anser/expected/anser_runtime_filter.out @@ -0,0 +1,121 @@ +-- Anser runtime bloom filter: plan-tree integration (PR4). +-- +-- Requires the cluster to run with gp_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. +-- 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 gp_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 gp_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 gp_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 gp_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 gp_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) + +DROP TABLE anser_rf_r_on, anser_rf_r_off, anser_rf_r_orca; +DROP TABLE anser_rf_build, anser_rf_probe; +RESET gp_anser_runtime_filter; +RESET optimizer; +RESET enable_nestloop; +RESET enable_mergejoin; diff --git a/src/test/modules/anser/expected/test_anser.out b/src/test/modules/anser/expected/test_anser.out new file mode 100644 index 00000000000..95596067d49 --- /dev/null +++ b/src/test/modules/anser/expected/test_anser.out @@ -0,0 +1,334 @@ +CREATE EXTENSION test_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) + +-- Built-in 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 gp_anser_producer_begin(10, 1, 1, 'svc_roundtrip', 1) AS begin_ok; + begin_ok +---------- + t +(1 row) + +SELECT gp_anser_publish(10, 1, 1, 'svc_roundtrip', '\x6162'::bytea, false) AS publish_ok; + publish_ok +------------ + t +(1 row) + +SELECT encode(gp_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 gp_anser_timeout_ms, so the waiting consumer is delivered a cancel and +-- consume_wait returns NULL. +SELECT gp_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 gp_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 test_anser; diff --git a/src/test/modules/anser/meson.build b/src/test/modules/anser/meson.build new file mode 100644 index 00000000000..36f5a297db9 --- /dev/null +++ b/src/test/modules/anser/meson.build @@ -0,0 +1,51 @@ +# 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. + +test_anser_sources = files( + 'anser_test.c', +) + +if host_system == 'windows' + test_anser_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_anser', + '--FILEDESC', 'test_anser - test helpers for Anser',]) +endif + +test_anser = shared_module('test_anser', + test_anser_sources, + kwargs: pg_test_mod_args + { + 'dependencies': pg_test_mod_args['dependencies'] + [libpq], + }, +) +test_install_libs += test_anser + +test_install_data += files( + 'test_anser.control', + 'test_anser--1.0.sql', +) + +tests += { + 'name': 'test_anser', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_anser', + 'anser_runtime_filter', + ], + }, +} diff --git a/src/test/modules/anser/sql/anser_runtime_filter.sql b/src/test/modules/anser/sql/anser_runtime_filter.sql new file mode 100644 index 00000000000..16a71397dcf --- /dev/null +++ b/src/test/modules/anser/sql/anser_runtime_filter.sql @@ -0,0 +1,69 @@ +-- Anser runtime bloom filter: plan-tree integration (PR4). +-- +-- Requires the cluster to run with gp_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. + +-- 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 gp_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 gp_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 gp_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 gp_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 gp_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; + +DROP TABLE anser_rf_r_on, anser_rf_r_off, anser_rf_r_orca; +DROP TABLE anser_rf_build, anser_rf_probe; +RESET gp_anser_runtime_filter; +RESET optimizer; +RESET enable_nestloop; +RESET enable_mergejoin; diff --git a/src/test/modules/anser/sql/test_anser.sql b/src/test/modules/anser/sql/test_anser.sql new file mode 100644 index 00000000000..516f11d3167 --- /dev/null +++ b/src/test/modules/anser/sql/test_anser.sql @@ -0,0 +1,116 @@ +CREATE EXTENSION test_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); + +-- 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; + +-- Built-in 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 gp_anser_producer_begin(10, 1, 1, 'svc_roundtrip', 1) AS begin_ok; +SELECT gp_anser_publish(10, 1, 1, 'svc_roundtrip', '\x6162'::bytea, false) AS publish_ok; +SELECT encode(gp_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 gp_anser_timeout_ms, so the waiting consumer is delivered a cancel and +-- consume_wait returns NULL. +SELECT gp_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 gp_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 test_anser; diff --git a/src/test/modules/anser/test_anser--1.0.sql b/src/test/modules/anser/test_anser--1.0.sql new file mode 100644 index 00000000000..af2c54ea781 --- /dev/null +++ b/src/test/modules/anser/test_anser--1.0.sql @@ -0,0 +1,149 @@ +/* src/test/modules/anser/test_anser--1.0.sql */ + +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/src/test/modules/anser/test_anser.control b/src/test/modules/anser/test_anser.control new file mode 100644 index 00000000000..650f59f930e --- /dev/null +++ b/src/test/modules/anser/test_anser.control @@ -0,0 +1,5 @@ +# test_anser extension +comment = 'test helpers for Anser' +default_version = '1.0' +module_pathname = '$libdir/test_anser' +relocatable = true diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 9556bac4ad4..cd8777ea6d4 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -1,5 +1,6 @@ # Copyright (c) 2022-2023, PostgreSQL Global Development Group +subdir('anser') subdir('brin') subdir('commit_ts') subdir('delay_execution') From 379353af183dc0654de6343e4feafaa8947f58ae Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 23:08:06 +0300 Subject: [PATCH 4/8] Fix guc.out for gp_anser_runtime_filter The query-tuning GUC list test now also returns gp_anser_runtime_filter (registered under QUERY_TUNING_METHOD by the plan-injection commit); add it to the expected output. This commit collects CI/review fixes for the Anser PR series; squash later fixes into it. --- src/test/regress/expected/guc.out | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out index f7079d372bc..543ebd59459 100644 --- a/src/test/regress/expected/guc.out +++ b/src/test/regress/expected/guc.out @@ -920,6 +920,7 @@ SELECT name FROM tab_settings_flags ------------------------------------------------------ enable_groupagg gp_adjust_selectivity_for_outerjoins + gp_anser_runtime_filter gp_cte_sharing gp_dynamic_partition_pruning gp_enable_agg_distinct @@ -962,7 +963,7 @@ SELECT name FROM tab_settings_flags optimizer_penalize_broadcast_threshold optimizer_push_group_by_below_setop_threshold optimizer_sort_factor -(44 rows) +(45 rows) -- Runtime-computed GUCs should be part of the preset category. SELECT name FROM tab_settings_flags From 7458dfff6e2e96db46a2dc9fe54b2debb07a7b21 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 23:08:06 +0300 Subject: [PATCH 5/8] Fix guc.out for gp_anser_runtime_filter The query-tuning GUC list test now also returns gp_anser_runtime_filter (registered under QUERY_TUNING_METHOD by the plan-injection commit); add it to the expected output. This commit collects CI/review fixes for the Anser PR series; squash later fixes into it. --- .github/workflows/build-cloudberry.yml | 3 ++- contrib/pax_storage/src/test/regress/expected/guc.out | 3 ++- src/test/modules/anser/anser_test.c | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-cloudberry.yml b/.github/workflows/build-cloudberry.yml index 03efd3ca0d6..504cc2f694d 100644 --- a/.github/workflows/build-cloudberry.yml +++ b/.github/workflows/build-cloudberry.yml @@ -244,7 +244,8 @@ jobs: "pg_settings":{"optimizer":"on"} }, {"test":"ic-anser", - "make_configs":["src/test/modules/anser:installcheck"], + "make_configs":["src/test/modules/anser:install", + "src/test/modules/anser:installcheck"], "postgres_conf_addons":"gp_anser_enable=on" }, {"test":"pax-ic-good-opt-off", diff --git a/contrib/pax_storage/src/test/regress/expected/guc.out b/contrib/pax_storage/src/test/regress/expected/guc.out index f7079d372bc..543ebd59459 100644 --- a/contrib/pax_storage/src/test/regress/expected/guc.out +++ b/contrib/pax_storage/src/test/regress/expected/guc.out @@ -920,6 +920,7 @@ SELECT name FROM tab_settings_flags ------------------------------------------------------ enable_groupagg gp_adjust_selectivity_for_outerjoins + gp_anser_runtime_filter gp_cte_sharing gp_dynamic_partition_pruning gp_enable_agg_distinct @@ -962,7 +963,7 @@ SELECT name FROM tab_settings_flags optimizer_penalize_broadcast_threshold optimizer_push_group_by_below_setop_threshold optimizer_sort_factor -(44 rows) +(45 rows) -- Runtime-computed GUCs should be part of the preset category. SELECT name FROM tab_settings_flags diff --git a/src/test/modules/anser/anser_test.c b/src/test/modules/anser/anser_test.c index 3c834b86e90..679d2c8d91e 100644 --- a/src/test/modules/anser/anser_test.c +++ b/src/test/modules/anser/anser_test.c @@ -908,6 +908,8 @@ anser_test_dsm_free_on_success(PG_FUNCTION_ARGS) } 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]); @@ -973,9 +975,9 @@ anser_test_dsm_free_on_timeout(PG_FUNCTION_ARGS) { SetConfigOption("gp_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 */ - AnserSetSweepEnabled(false); } PG_END_TRY(); @@ -1068,6 +1070,8 @@ anser_test_dsm_free_on_cancel(PG_FUNCTION_ARGS) } 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]); From 985b045e5fc404945e6aa7127adc7a8324ba8abe Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 23:08:06 +0300 Subject: [PATCH 6/8] Fix guc.out for gp_anser_runtime_filter The query-tuning GUC list test now also returns gp_anser_runtime_filter (registered under QUERY_TUNING_METHOD by the plan-injection commit); add it to the expected output. This commit collects CI/review fixes for the Anser PR series; squash later fixes into it. --- .github/workflows/build-cloudberry.yml | 3 +-- src/test/modules/anser/Makefile | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cloudberry.yml b/.github/workflows/build-cloudberry.yml index 8c25c7e3fde..695ea308009 100644 --- a/.github/workflows/build-cloudberry.yml +++ b/.github/workflows/build-cloudberry.yml @@ -244,8 +244,7 @@ jobs: "pg_settings":{"optimizer":"on"} }, {"test":"ic-anser", - "make_configs":["src/test/modules/anser:install", - "src/test/modules/anser:installcheck"], + "make_configs":["src/test/modules/anser:installcheck-with-install"], "postgres_conf_addons":"gp_anser_enable=on" }, {"test":"pax-ic-good-opt-off", diff --git a/src/test/modules/anser/Makefile b/src/test/modules/anser/Makefile index 25731f87e16..626f69eb44b 100644 --- a/src/test/modules/anser/Makefile +++ b/src/test/modules/anser/Makefile @@ -38,6 +38,15 @@ DATA = test_anser--1.0.sql REGRESS = test_anser anser_runtime_filter +# CI runs a single make target per test config, and pg_regress can only CREATE +# EXTENSION test_anser once the module is installed (src/test/modules is not +# covered by the top-level install). One-shot target so the CI matrix needs +# only one entry whose log still contains the regression results. +.PHONY: installcheck-with-install +installcheck-with-install: + $(MAKE) install + $(MAKE) installcheck + ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) From dc8a4f2cc0cdaee05a7072794399722c41d410c3 Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 23:08:06 +0300 Subject: [PATCH 7/8] Fix guc.out for gp_anser_runtime_filter The query-tuning GUC list test now also returns gp_anser_runtime_filter (registered under QUERY_TUNING_METHOD by the plan-injection commit); add it to the expected output. This commit collects CI/review fixes for the Anser PR series; squash later fixes into it. --- src/backend/cdb/anser/anserplan.c | 15 ++- src/backend/cdb/anser/anserplanexec.c | 79 ++++++++++++-- .../anser/expected/anser_runtime_filter.out | 102 ++++++++++++++++++ .../anser/sql/anser_runtime_filter.sql | 72 +++++++++++++ 4 files changed, 257 insertions(+), 11 deletions(-) diff --git a/src/backend/cdb/anser/anserplan.c b/src/backend/cdb/anser/anserplan.c index c618cdd48c0..f2f54962b12 100644 --- a/src/backend/cdb/anser/anserplan.c +++ b/src/backend/cdb/anser/anserplan.c @@ -40,6 +40,7 @@ #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). */ @@ -244,12 +245,20 @@ anser_hashjoin_keys(HashJoin *hj, AttrNumber *inner_attno, AttrNumber *outer_att return false; /* - * Producer and consumer hash the raw Datum bytes, which is only meaningful - * for a pass-by-value key type (the value lives in the Datum, not behind a - * pointer). Restrict to those. + * 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; diff --git a/src/backend/cdb/anser/anserplanexec.c b/src/backend/cdb/anser/anserplanexec.c index 130441e4bcc..c6241baa093 100644 --- a/src/backend/cdb/anser/anserplanexec.c +++ b/src/backend/cdb/anser/anserplanexec.c @@ -37,6 +37,7 @@ */ #include "postgres.h" +#include "access/skey.h" #include "cdb/anser.h" #include "cdb/anserfilter.h" #include "cdb/anserplan.h" @@ -51,6 +52,7 @@ #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. @@ -99,6 +101,9 @@ typedef struct AnserBloomConsumeScanState 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. */ @@ -461,6 +466,7 @@ anser_consume_begin(CustomScanState *node, EState *estate, int eflags) 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); @@ -503,6 +509,41 @@ anser_consume_receive(AnserBloomConsumeScanState *st) 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) { @@ -511,6 +552,13 @@ anser_consume_next(CustomScanState *node) 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); @@ -520,8 +568,11 @@ anser_consume_next(CustomScanState *node) if (TupIsNull(slot)) return NULL; - /* Fail open: no usable filter -> pass everything. */ - if (st->filter == 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); @@ -562,12 +613,16 @@ 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; - if (node->custom_ps != NIL) - ExecEndNode((PlanState *) linitial(node->custom_ps)); } static void @@ -598,13 +653,21 @@ anser_consume_explain(CustomScanState *node, List *ancestors, ExplainState *es) * 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. */ - snprintf(buf, sizeof(buf), - "memory=" INT64_FORMAT "kB checked=%.0f rejected=%.0f", - st->planned_bytes / 1024, nchecked, nfiltered); + 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 (nloops > 0) + if (!st->pushed_down && nloops > 0) ExplainPropertyFloat("Rows Removed by Bloom Filter", NULL, nfiltered / nloops, 0, es); } diff --git a/src/test/modules/anser/expected/anser_runtime_filter.out b/src/test/modules/anser/expected/anser_runtime_filter.out index 998acf20b9e..2d8504fa63f 100644 --- a/src/test/modules/anser/expected/anser_runtime_filter.out +++ b/src/test/modules/anser/expected/anser_runtime_filter.out @@ -113,8 +113,110 @@ SELECT count(*) AS 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 gp_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 gp_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 gp_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 gp_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 gp_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 gp_anser_runtime_filter; RESET optimizer; RESET enable_nestloop; diff --git a/src/test/modules/anser/sql/anser_runtime_filter.sql b/src/test/modules/anser/sql/anser_runtime_filter.sql index 16a71397dcf..6c833b73cc5 100644 --- a/src/test/modules/anser/sql/anser_runtime_filter.sql +++ b/src/test/modules/anser/sql/anser_runtime_filter.sql @@ -61,8 +61,80 @@ SELECT count(*) AS only_orca 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 gp_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 gp_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 gp_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 gp_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 gp_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 gp_anser_runtime_filter; RESET optimizer; RESET enable_nestloop; From e0eaba752997d8e202c88eb4377bda0a1641bcff Mon Sep 17 00:00:00 2001 From: Leonid Borchuk Date: Mon, 31 Aug 2026 23:08:06 +0300 Subject: [PATCH 8/8] Fix guc.out for gp_anser_runtime_filter The query-tuning GUC list test now also returns gp_anser_runtime_filter (registered under QUERY_TUNING_METHOD by the plan-injection commit); add it to the expected output. This commit collects CI/review fixes for the Anser PR series; squash later fixes into it. --- src/backend/cdb/anser/anserplan.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/cdb/anser/anserplan.c b/src/backend/cdb/anser/anserplan.c index f2f54962b12..bf4d76d3a75 100644 --- a/src/backend/cdb/anser/anserplan.c +++ b/src/backend/cdb/anser/anserplan.c @@ -37,6 +37,7 @@ #include "cdb/anser.h" #include "cdb/anserplan.h" #include "cdb/cdbvars.h" +#include "catalog/pg_type.h" #include "nodes/nodeFuncs.h" #include "nodes/pg_list.h" #include "utils/acl.h"