diff --git a/.github/workflows/build-cloudberry.yml b/.github/workflows/build-cloudberry.yml index b790d3411ec..6ec2f52ecba 100644 --- a/.github/workflows/build-cloudberry.yml +++ b/.github/workflows/build-cloudberry.yml @@ -243,6 +243,11 @@ jobs: "make_configs":["src/test/regress:installcheck-good"], "pg_settings":{"optimizer":"on"} }, + {"test":"ic-anser", + "make_configs":["gpcontrib/anser:installcheck-with-install"], + "shared_preload_libraries":"anser", + "postgres_conf_addons":"anser.enable=on" + }, {"test":"pax-ic-good-opt-off", "make_configs":[ "contrib/pax_storage/:pax-test", @@ -1500,6 +1505,15 @@ jobs: echo "Adding shared_preload_libraries: ${{ matrix.shared_preload_libraries }}" fi + # Append extra postgresql.conf settings requested by the matrix + # entry (e.g. postmaster-context GUCs a test module requires). + # BLDWRAP_POSTGRES_CONF_ADDONS is a pipe-separated list of + # key=value pairs (see gpAux/gpdemo/demo_cluster.sh). + if [[ -n "${{ matrix.postgres_conf_addons }}" ]]; then + EXTRA_CONF="${EXTRA_CONF}${EXTRA_CONF:+|}${{ matrix.postgres_conf_addons }}" + echo "Adding postgres_conf_addons: ${{ matrix.postgres_conf_addons }}" + fi + if ! time su - gpadmin -c "cd ${SRC_DIR} && NUM_PRIMARY_MIRROR_PAIRS='${{ matrix.num_primary_mirror_pairs }}' BLDWRAP_POSTGRES_CONF_ADDONS=\"${EXTRA_CONF}\" SRC_DIR=${SRC_DIR} ${SRC_DIR}/devops/build/automation/cloudberry/scripts/create-cloudberry-demo-cluster.sh"; then echo "::error::Demo cluster creation failed" exit 1 diff --git a/gpcontrib/Makefile b/gpcontrib/Makefile index 32c134c95e6..89e1adb0cb7 100644 --- a/gpcontrib/Makefile +++ b/gpcontrib/Makefile @@ -15,7 +15,8 @@ recurse_targets = "" recurse_targets = gp_exttable_fdw ifeq "$(enable_debug_extensions)" "yes" - recurse_targets = gp_sparse_vector \ + recurse_targets = anser \ + gp_sparse_vector \ gp_distribution_policy \ gp_internal_tools \ gp_debug_numsegments \ @@ -28,7 +29,8 @@ ifeq "$(enable_debug_extensions)" "yes" pg_hint_plan \ reject_partition_fullscan else - recurse_targets = gp_sparse_vector \ + recurse_targets = anser \ + gp_sparse_vector \ gp_distribution_policy \ gp_internal_tools \ gp_legacy_string_agg \ diff --git a/gpcontrib/anser/Makefile b/gpcontrib/anser/Makefile new file mode 100644 index 00000000000..a675e3d58e0 --- /dev/null +++ b/gpcontrib/anser/Makefile @@ -0,0 +1,103 @@ +#------------------------------------------------------------------------- +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# Makefile for the anser extension. +# +# IDENTIFICATION +# gpcontrib/anser/Makefile +# +#------------------------------------------------------------------------- + +MODULE_big = anser +OBJS = \ + $(WIN32RES) \ + src/anserbloomconsume.o \ + src/anserbloomproduce.o \ + src/anserdispatch.o \ + src/anserfilter.o \ + src/anserinit.o \ + src/anserpayload.o \ + src/anserplan.o \ + src/anserplanexec.o \ + src/ansersideband.o \ + src/anser_test.o + +PGFILEDESC = "anser - adaptive information sharing runtime filters" + +# The subsystem itself needs no catalog objects -- it is configured entirely by +# GUCs and travels over the dispatch connection. The only extension here is +# anser_test, which exposes the internal C API to the regression tests from +# inside the same library. +EXTENSION = anser_test +DATA = anser_test--1.0.sql + +REGRESS = anser_test anser_runtime_filter + +# src/anserdispatch.c writes sideband messages onto the dispatcher's own libpq +# connections, so it needs libpq's internal headers (as the dispatcher itself +# does). Deliberately NOT linked against libpq: the pq* writers we use stay +# global in the postgres binary, while the public PQ* API is made local there +# on purpose (src/backend/Makefile), so linking libpq.so would put a second +# copy of libpq on connections the backend's copy created. +PG_CPPFLAGS = -I$(srcdir)/include -I$(libpq_srcdir) +SHLIB_PREREQS = submake-libpq + +ifdef USE_PGXS +PG_CONFIG ?= pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = gpcontrib/anser +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +# The tests need the live gather/send services, which only exist when the +# library is preloaded and anser.enable is on -- both postmaster-context +# settings. Ensure them (and restart) before installcheck, as +# gp_relsizes_stats does for its worker. +installcheck: preload-anser + +.PHONY: preload-anser +preload-anser: + @if [ -z "$$COORDINATOR_DATA_DIRECTORY$$MASTER_DATA_DIRECTORY" ]; then \ + echo "ERROR: COORDINATOR_DATA_DIRECTORY (or MASTER_DATA_DIRECTORY) is not set;" >&2; \ + echo " source cloudberry-env.sh and gpdemo-env.sh before running installcheck." >&2; \ + exit 1; \ + fi + @current=`psql -d postgres -At -c "SHOW shared_preload_libraries;" | tr -d ' '`; \ + case ",$$current," in \ + *,anser,*) \ + echo "==> shared_preload_libraries already contains anser" ;; \ + *) \ + if [ -z "$$current" ]; then new=anser; else new="$$current,anser"; fi; \ + echo "==> Adding anser to shared_preload_libraries ($$new)"; \ + gpconfig -c shared_preload_libraries -v "'$$new'" --skipvalidation ;; \ + esac + gpconfig -c anser.enable -v on --skipvalidation + gpstop -ra + psql -d postgres -c "SHOW shared_preload_libraries;" + +# CI runs a single make target per test config, so offer one target that +# installs the extension, arms the cluster, and runs the tests. +.PHONY: installcheck-with-install +installcheck-with-install: + $(MAKE) install + $(MAKE) installcheck diff --git a/gpcontrib/anser/README.md b/gpcontrib/anser/README.md new file mode 100644 index 00000000000..862b994a84e --- /dev/null +++ b/gpcontrib/anser/README.md @@ -0,0 +1,318 @@ +# 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 running query (today: a +bloom filter over a join-build key), the coordinator combines the per-segment +parts into one, and consumers on the segments receive it and use it to prune +work (today: skip probe rows that cannot join). + +Everything travels over the **dispatch connection the coordinator already holds +open to every segment**. There is no shared memory, no background worker, no +second connection and nothing extra to authenticate: a channel exists only in +the coordinator backend running the query, for exactly as long as that query +runs. + +This document covers installation, the architecture, the wire protocol, the +configuration surface, and what a *channel* is. For the plan-tree integration +see `anserplan.c`; for the payload/bloom protocol see `anserfilter.c` and +`lib/bloomfilter.c`. + +## Installation + +Anser is configured entirely by GUCs — it creates no catalog objects, so there +is nothing to `CREATE EXTENSION` in each database. + +``` +gpconfig -c shared_preload_libraries -v "'anser'" --skipvalidation +gpconfig -c anser.enable -v on +gpstop -ra +``` + +The library **must** be preloaded: a segment backend deserializing a dispatched +plan has no opportunity to load it on demand, and its CustomScan providers have +to be registered before that happens. + +Then turn the filter on where you want it — `anser.runtime_filter` is +`PGC_USERSET`, so per session, per role, or cluster-wide. + +`anser_test` is a separate control file over the same library, exposing the +internal C API to the regression tests. It is test-only; do not create it in +production databases. + +## Architecture + +A **channel** is one rendezvous point between the producers and consumers of a +single piece of runtime information, for a single query: + +``` +AnserChannelKey = { gp_session_id, gp_command_count, condition_id, condition_key[64] } +``` + +- `gp_session_id` + `gp_command_count` scope the channel to one query execution, + so keys never collide across sessions or across statements in a session. +- `condition_id` distinguishes multiple filters within the same query. +- `condition_key` is an opaque string describing the filtered condition — today + just `anser_rf_`, since the planner stamps the same key into + both plan nodes at injection time. It exists for the case where the key has to + be derived from the build's semantic identity instead (so that producer and + consumer can find each other without having been planned together), which is + why the wire format treats it as arbitrary bytes rather than as an identifier. + +Channels live in a hash in the coordinator backend, created on first use and +dropped at `ExecutorEnd` (or on transaction abort). Since the merge and the +delivery both happen in that one process, the accumulator is an ordinary +`palloc`'d buffer. + +### Giving up early + +A bloom filter that is too small for its key count matches almost everything: it +costs a hash and `k` probes per probe row and eliminates nothing. Anser checks +for that at the three points where new information becomes available, and always +fails open — giving up means the query runs unfiltered, never that it runs wrong. + +| Where | Knows | Check | Cost of giving up | +| --- | --- | --- | --- | +| Planner (`anserplan.c`) | the row estimate | bitset bits / estimated keys ≥ 4 | nothing — the nodes are never injected | +| Producer init (`anserfilter.c`) | the plan parameters | the filter fits the payload cap, and still ≥ 4 bits/key | one `palloc0`, freed immediately; cancel published on the first tuple, before the build side is scanned | +| Producer publish (`anserbloomproduce.c`) | the filter, fully built | estimated FPR (`fill^k`) ≤ 50% | the build scan, already spent; saves the network and every consumer's probing | +| Coordinator merge (`anserdispatch.c`) | the merged payload | fill ≤ 95% | the merge, already spent; saves delivery and probing | + +The planner gate is the one that matters, because it is the only one that costs +nothing. The rest exist because a row estimate can be wrong, and the last one +exists because **folding changes the answer**: OR-ing three parts that are each +60% full gives a merged filter that is 94% full, so no producer can tell whether +the result will be useful. + +One subtlety worth knowing if you touch the sizing: do **not** clamp the element +estimate to make a filter fit. `total_elems` is also what `optimal_k()` derives +the hash count from, so understating it misconfigures the filter — a 128M-row +build side declared as 33.5M gets `k=10` where the optimum is `k=3`, turning a +13% false positive rate into 38%. The keys all go in regardless of what was +written down. Clamp the *size*; keep the count honest. + +### Payload types + +A channel carries one **payload type**, declared on the wire and registered in +`anserpayload.c`. The transport itself moves opaque bytes and never branches on +what they mean; everything type-specific lives in one descriptor +(`AnserPayloadOps`): + +| | `fold()` | `checksum_body` | +| --- | --- | --- | +| `B` — bloom filter | bitwise OR of equal-sized bitsets | yes | +| `-` — none (a subscription) | n/a | n/a | + +`fold()` is how the coordinator reduces one part per producer to a single +payload; there is no generic answer, which is why each type supplies it. A row +count, for instance, would fold by summing. + +`checksum_body` is about consequence, not size. A bloom filter fails +asymmetrically — a bit flipped 1 → 0 removes a key, so a joinable row is +rejected and the query silently returns too few rows — whereas a row count only +feeds a planning decision, where corruption costs a worse plan and never a wrong +answer. Types of the second kind opt out and pay nothing. (The header and +condition key are checksummed either way; that is ~100 ns and it is what +protects the routing fields.) + +Adding a type is three steps, none of which touch the framing: define a code, +add a row to `AnserPayloadTable`, and pass the code from the producer and +consumer nodes. `anserpayload.h` has the details. + +### How it attaches to the server + +Everything is reached through an existing extensibility point, so the server +carries no Anser-specific code (`anserinit.c`): + +| Hook | Used for | +| --- | --- | +| `planner_hook` | the injection pass, on the finished plan; wrapping the hook covers ORCA too, since it is dispatched from inside `standard_planner()` | +| `RegisterCustomScanMethods` | the producer/consumer nodes, so their methods resolve by name in every backend that deserializes a dispatched plan | +| `cdbdisp_notify_hook` | parts and subscriptions arriving from segments | +| `ExecutorEnd_hook` | dropping a query's channels | +| `DefineCustom*Variable` | the `anser.*` GUCs below | + +The first two already existed; `cdbdisp_notify_hook` and the +`GP_SIDEBAND_MESSAGE` tolerance in the QE command loop are the only additions +Anser needed in the server, and neither mentions Anser. + +## Wire protocol + +The two directions are deliberately asymmetric, because the constraints differ. + +**Segment → coordinator** is a `NOTIFY` on channel `anser_rf`, the model +`nextval()` uses (`cdb_sequence_nextval_qe` in `commands/sequence.c`). The QD is +a libpq *client*, and libpq rejects message types it does not know, so this +direction has to be a message type libpq already understands. `NotifyMyFrontEnd` +imposes no length limit of its own — the ~8 KB `NOTIFY_PAYLOAD_MAX_LENGTH` +applies to the SQL-level `NOTIFY`, which must fit a queue page — but it delivers +through `pq_sendstring`, so the payload must be a NUL-free string: + +``` +anser3 K T SSSSSSSSSS CCCCCCCCCC DDDDDDDDDD PPPPPPPPPP TTTTTTTTTT FFFF KKKK BBBBBBBBBB XXXXXXXX +^tag ^ ^ session command condition part total flags keylen bodylen crc + | \ payload type + \ kind + +``` + +`kind` is `P` (a producer's part) or `S` (a consumer subscribing) — what the +message *does*, as opposed to the payload type, which is what it *carries*. The +header is **95 bytes of fixed-width ASCII**, so the key and the body begin at +offsets that nothing in their own contents can shift, and the two lengths must +account for the message exactly — a short, long or misaligned message is rejected +before any of it is used. There is deliberately no delimiter anywhere in the +format: the earlier version ended its header with a newline and found it with +`strchr()`, which was correct only while the key could not itself contain a +newline, an invariant nothing enforced. + +**Coordinator → segment** is a `GP_SIDEBAND_MESSAGE`, written with `pqPutnchar`, +which performs no conversion — so the merged filter travels as **raw binary**, +with no base64 tax. That is the direction that matters most, since the merged +payload is sent once *per consumer* while each part is sent once. + +Both directions carry a **CRC32C**, and both discard a message that fails it. +The transport is TCP, not the UDP interconnect, so this is not about a lossy +link — TCP's 16-bit checksum is simply thin cover for a megabyte, and hardware +CRC32C costs about 0.05 ms/MB. The header and key are always covered; whether +the body is covered too is the payload type's decision, for the reasons in +[Payload types](#payload-types) above. Where it applies, the segment → +coordinator CRC covers the pre-base64 bytes, so it validates the decode as well. + +The coordinator services these while it is blocked receiving tuples: the +interconnect adds every dispatch socket to its wait set +(`ic_udpifc.c`, `ic_tcp.c`), and a readable one leads to +`checkForCancelFromQD` → `cdbdisp_checkForCancel` → `processResults`, which is +where the notify handler runs. Under `gp_interconnect_type=proxy` that check is +driven by a 2 s timer instead (`ic_proxy_backend.c`), so filter delivery can lag +by up to that long. + +## GUCs + +| GUC | Default | Context | Meaning | +| --- | --- | --- | --- | +| `anser.enable` | `off` | SIGHUP | Master switch. With it off the plan pass never injects anything and no filters are exchanged. | +| `anser.runtime_filter` | `off` | USERSET | Enables the post-planning pass that injects bloom-filter producer/consumer nodes into a matching plan. Requires `anser.enable`. | +| `anser.max_info_size` | `65 MB` | POSTMASTER | Maximum serialized payload (merged bloom filter + part header) a channel may hold; caps the effective bloom-filter size. The default is `64 MB + 1 MB` so a full 64 MB power-of-two bitset fits with its header; `bloom_create` also floors every bitset at 1 MB. | +| `anser.timeout_ms` | `1000` | USERSET | How long a consumer waits for its filter before running unfiltered. The deadline matters because a producer that gets squelched never publishes at all: `ExecSquelchNode` only marks a `CustomScanState`, it does not call the node back. | +| `anser.debug` | `off` | USERSET | Traces the exchange — publish, merge, delivery, receive — in the log of the process each step happens in. See below. | + +### Tracing an exchange + +Until a filter is either used or timed out, none of the handoff is visible in +`EXPLAIN`: a consumer that waited and got nothing looks exactly like one whose +producers never published. `anser.debug` makes each step log where it happened, +which is normally the fastest way to find where an exchange broke: + +``` +seg0 producer init cond=0 part=0/3 elems=3334 payload=67108928 state=ok +seg0 producer child exhausted, publishing (state=ok) +seg0 published cond=0 part=0/3 bytes=1048592 cancelled=0 sent=1 +QD part cond=0 from seg0 (says part 0 of 3) 1/3 bytes=1048592 -> collecting +QD part cond=0 from seg1 (says part 1 of 3) 2/3 bytes=1048592 -> collecting +QD part cond=0 from seg2 (says part 2 of 3) 3/3 bytes=1048592 -> complete +QD delivering cond=0 to 3 subscriber(s) +QD pushed cond=0 bytes=1048592 cancelled=0 +seg0 received cond=0 bytes=1048592 cancelled=0 +``` + +Set it in `postgresql.conf` (`gpconfig -c anser.debug -v on`) rather than with +`SET` if you need to see the producer gang: a session-level `SET` does not +reliably reach every gang, and the producers are the half you usually want. + +A consumer that gives up reports what it saw — `read 0 message(s), 0 +unclaimed` means nothing arrived at all, whereas unclaimed messages mean +something arrived for a channel it was not waiting on. + +## Data flow: producer → merge (bitwise union) → consumer + +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 ├─ NOTIFY ─► QD backend (notify hook) +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 + │ + sideband push ───────────┼───────────────┐ + ▼ ▼ ▼ + 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. Publishing is fire-and-forget: the producer sends + its part and carries on without waiting for an acknowledgement. + +2. **Merge (coordinator, once per part).** The coordinator never reconstructs a + filter — it works on raw bytes. The **first** part is stored verbatim; every + later part is folded into the channel's payload with an in-place **bitwise OR** + of the bitset (`AnserBloomFoldPartInPlace` in `anserfilter.c`, called from + `anserdispatch.c`). The payload is therefore always a **single merged bitset**, + the size of one filter — it does **not** grow with the segment count. Folding + happens as parts arrive, so only the last fold is on the critical path. The OR + requires the incoming part to be the same size as the accumulator (guaranteed + by the shared parameters); a mismatch cancels the channel and consumers fail + open. + +3. **Deliver (coordinator → every consumer).** Once every expected part is + folded, the merged payload is judged once (see [Giving up + early](#giving-up-early)) and then pushed to each subscriber. Delivery is per + consumer: a failed write costs that one segment its filter and leaves the + others alone. A consumer that subscribes *after* the channel completed — which + happens routinely, since producers on other segments may finish first — is + served immediately. + +4. **Consume (per segment).** Each consumer rebuilds an empty filter from its own + plan parameters (the same `total_elems` / `max_payload` / seed the producers + used) and loads the received bitset into it (`AnserBloomDeserializePart`), + requiring the received length to match exactly (else it fails open). It does + **not** re-union anything. + +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). + +## Failure handling + +Every failure mode ends in unfiltered execution, never in a wrong answer and +never in an error raised into the query: + +- a producer that is squelched, errors, or produces an oversized part → the + channel never completes (or is cancelled) → consumers hit `anser.timeout_ms` + and run unfiltered; +- a malformed or undecodable part → the channel is cancelled and every consumer + is told so; +- a broken connection → the query is failing anyway, and the interconnect + reports it far more usefully than the filter path could; +- query cancellation → the consumer's wait is a `CHECK_FOR_INTERRUPTS` loop, and + a delivery that arrives after nobody is waiting is discarded by the QE command + loop (`GP_SIDEBAND_MESSAGE` is accepted and ignored there). + +## Tests + +`make installcheck` runs two suites: + +- `anser_test` — the payload protocol (serialize, fold, reject a mismatched + part) and the four give-up decisions, each walked along its boundary: the + estimate either side of 4 bits/key, the payload cap either side of "1 MB plus + a header", fill either side of the 95% limit, and a producer whose filter + saturates. The case tables live in the `.sql` file, so the expected output + records real sizes rather than a bare `t`. +- `anser_runtime_filter` — plan-tree integration end to end: the nodes are + injected, and query results are identical with the feature on and off. diff --git a/gpcontrib/anser/anser_test--1.0.sql b/gpcontrib/anser/anser_test--1.0.sql new file mode 100644 index 00000000000..3c2f8d14cfd --- /dev/null +++ b/gpcontrib/anser/anser_test--1.0.sql @@ -0,0 +1,99 @@ +/* gpcontrib/anser/anser_test--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION anser_test" to load this file. \quit + +CREATE FUNCTION anser_test_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 STRICT; + +CREATE FUNCTION anser_test_bloom_rejects_mismatch() +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +CREATE FUNCTION anser_test_node_roundtrip(value int4) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The planner's give-up decision: NULL means no filter is injected. +CREATE FUNCTION anser_test_rf_size( + est_rows float8, + OUT injected bool, + OUT total_elems bigint, + OUT max_payload bigint, + OUT planned_bytes bigint) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- What AnserBloomCreate builds, or NULL when it declines to build anything. +CREATE FUNCTION anser_test_bloom_shape( + total_elems bigint, + cap bigint, + OUT built bool, + OUT bits bigint, + OUT serialized bigint, + OUT bits_per_key float8) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The coordinator's decision about a merged payload, on a synthetic part. +CREATE FUNCTION anser_test_worth_delivering( + bitset_bytes int4, + bytes_set int4, + damage text) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- Producer end to end: ":". +CREATE FUNCTION anser_test_producer_decision( + total_elems bigint, + cap bigint, + n_keys int4) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The formatted QE -> QD message, verbatim. +CREATE FUNCTION anser_test_wire_format( + kind "char", + payload_type "char", + flags int4, + condition_key text, + body bytea) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- Format, alter one byte, parse: reports what the reader made of it. +CREATE FUNCTION anser_test_wire_roundtrip( + condition_key text, + body bytea, + tamper text, + payload_type "char") +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- The QD -> QE checksum, so a test can compare two of them. +CREATE FUNCTION anser_test_push_crc( + payload_type "char", + condition_id int4, + flags int4, + condition_key text, + body bytea) +RETURNS int8 +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; diff --git a/gpcontrib/anser/anser_test.control b/gpcontrib/anser/anser_test.control new file mode 100644 index 00000000000..8a4f941a49b --- /dev/null +++ b/gpcontrib/anser/anser_test.control @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# anser_test extension +comment = 'test helpers for the anser extension' +default_version = '1.0' +module_pathname = '$libdir/anser' +relocatable = true diff --git a/gpcontrib/anser/expected/anser_runtime_filter.out b/gpcontrib/anser/expected/anser_runtime_filter.out new file mode 100644 index 00000000000..eb4765ac25a --- /dev/null +++ b/gpcontrib/anser/expected/anser_runtime_filter.out @@ -0,0 +1,223 @@ +-- Anser runtime bloom filter: plan-tree integration (PR4). +-- +-- Requires the cluster to run with anser.enable=on so the gather/send +-- services are live. On a multi-segment cluster the build side redistributes, +-- exercising the Motion-directly-under-CustomScan case; on a single segment it +-- degrades to the leaf case. Either way the feature must (a) inject the +-- producer/consumer nodes and (b) never change query results. +-- Deterministic plan shape: force a hash join. +SET enable_nestloop = off; +SET enable_mergejoin = off; +-- build is the smaller (hashed / preserved) side, distributed so the join key +-- must be redistributed; probe is the larger side distributed by the join key. +CREATE TABLE anser_rf_build (id int, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe (id int, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build; +ANALYZE anser_rf_probe; +-- Use the Postgres planner for a deterministic plan shape. +SET optimizer = off; +-- With the filter on the plan carries an "Anser Bloom Producer" under the Hash +-- and an "Anser Bloom Consumer" above the probe scan (with the planned bloom +-- size); with it off neither node appears. (COSTS OFF keeps the output stable.) +SET anser.runtime_filter = on; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + QUERY PLAN +------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Right Join + Hash Cond: (p.id = b.id) + -> Custom Scan (Anser Bloom Consumer) + Bloom Filter Size: 1048576 bytes + -> Seq Scan on anser_rf_probe p + -> Hash + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: b.id + -> Custom Scan (Anser Bloom Producer) + Bloom Filter Size: 1048576 bytes + Bloom Filter Stats: memory=1024kB + -> Seq Scan on anser_rf_build b + Optimizer: Postgres query optimizer +(14 rows) + +SET anser.runtime_filter = off; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + QUERY PLAN +------------------------------------------------------------------ + Gather Motion 3:1 (slice1; segments: 3) + -> Hash Right Join + Hash Cond: (p.id = b.id) + -> Seq Scan on anser_rf_probe p + -> Hash + -> Redistribute Motion 3:3 (slice2; segments: 3) + Hash Key: b.id + -> Seq Scan on anser_rf_build b + Optimizer: Postgres query optimizer +(9 rows) + +-- Correctness: identical results with the filter on vs off (Postgres planner). +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_on AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_r_off AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS rows_on FROM anser_rf_r_on; + rows_on +--------- + 200 +(1 row) + +SELECT count(*) AS rows_off FROM anser_rf_r_off; + rows_off +---------- + 200 +(1 row) + +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_r_on EXCEPT ALL SELECT * FROM anser_rf_r_off) d; + only_on +--------- + 0 +(1 row) + +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_on) d; + only_off +---------- + 0 +(1 row) + +-- Correctness must also hold under ORCA (whether or not it injects the nodes). +SET optimizer = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_orca AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_orca + FROM (SELECT * FROM anser_rf_r_orca EXCEPT ALL SELECT * FROM anser_rf_r_off) d; + only_orca +----------- + 0 +(1 row) + +SELECT count(*) AS only_off_orca + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_orca) d; + only_off_orca +--------------- + 0 +(1 row) + +-- Pushdown mix: with gp_enable_runtime_filter_pushdown on, the consumer hands +-- the unioned filter to the probe SeqScan as an SK_BLOOM_FILTER scan key and +-- the scan (or the table AM) does the pruning. Results must still match the +-- filter-off run. +SET optimizer = off; +SET gp_enable_runtime_filter_pushdown = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_push AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_push + FROM (SELECT * FROM anser_rf_r_push EXCEPT ALL SELECT * FROM anser_rf_r_off) d; + only_push +----------- + 0 +(1 row) + +SELECT count(*) AS only_off_push + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_push) d; + only_off_push +--------------- + 0 +(1 row) + +RESET gp_enable_runtime_filter_pushdown; +DROP TABLE anser_rf_r_push; +DROP TABLE anser_rf_r_on, anser_rf_r_off, anser_rf_r_orca; +DROP TABLE anser_rf_build, anser_rf_probe; +-- Datatype guard: producer and consumer hash the raw Datum bytes, so injection +-- is restricted to keys where SQL equality is bitwise Datum equality. A +-- cross-type join (float4 vs float8 Datums for equal values differ) must not +-- be injected; results must match the filter-off run either way. +CREATE TABLE anser_rf_build_f (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_f (id float4, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_f SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe_f SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build_f; +ANALYZE anser_rf_probe_f; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_f_on AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_f_off AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS rows_on FROM anser_rf_f_on; + rows_on +--------- + 200 +(1 row) + +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_f_on EXCEPT ALL SELECT * FROM anser_rf_f_off) d; + only_on +--------- + 0 +(1 row) + +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_f_off EXCEPT ALL SELECT * FROM anser_rf_f_on) d; + only_off +---------- + 0 +(1 row) + +DROP TABLE anser_rf_f_on, anser_rf_f_off, anser_rf_build_f, anser_rf_probe_f; +-- Same-typed float keys are also excluded: -0.0 and 0.0 compare equal in SQL +-- but are not bitwise equal, so hashing raw Datums could prune a joinable row. +CREATE TABLE anser_rf_build_z (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_z (id float8, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_z VALUES (1, 'b1'), (-0.0, 'bz'); +INSERT INTO anser_rf_probe_z VALUES (1, 'p1'), (0.0, 'pz'), (2, 'p2'); +ANALYZE anser_rf_build_z; +ANALYZE anser_rf_probe_z; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_z_on AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_z_off AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS rows_on FROM anser_rf_z_on; + rows_on +--------- + 2 +(1 row) + +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_z_on EXCEPT ALL SELECT * FROM anser_rf_z_off) d; + only_on +--------- + 0 +(1 row) + +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_z_off EXCEPT ALL SELECT * FROM anser_rf_z_on) d; + only_off +---------- + 0 +(1 row) + +DROP TABLE anser_rf_z_on, anser_rf_z_off, anser_rf_build_z, anser_rf_probe_z; +RESET anser.runtime_filter; +RESET optimizer; +RESET enable_nestloop; +RESET enable_mergejoin; diff --git a/gpcontrib/anser/expected/anser_test.out b/gpcontrib/anser/expected/anser_test.out new file mode 100644 index 00000000000..9e663672a02 --- /dev/null +++ b/gpcontrib/anser/expected/anser_test.out @@ -0,0 +1,286 @@ +CREATE EXTENSION anser_test; +-- Bloom payload protocol: serialize one part and read it back. +SELECT anser_test_bloom_roundtrip('bf_roundtrip', 42); + anser_test_bloom_roundtrip +---------------------------- + t +(1 row) + +-- In-place fold: same-size union mutates the accumulator; a mismatched size is +-- rejected. This is the coordinator's only combine path (the first part is +-- kept verbatim, every later part folds into it). +SELECT anser_test_bloom_fold_inplace() AS fold_inplace_ok; + fold_inplace_ok +----------------- + t +(1 row) + +-- Safety regression: the consumer rebuilds the filter from its own parameters +-- and requires the received bitset to be exactly the expected size (and the +-- header magic to match); truncated/oversized/corrupt parts are rejected. +SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; + reject_mismatch +----------------- + t +(1 row) + +-- Producer and consumer driven end to end in one backend: publish a part, let +-- the coordinator side merge it, then receive and query the filter. +SELECT anser_test_node_roundtrip(168); + anser_test_node_roundtrip +--------------------------- + t +(1 row) + +-- --------------------------------------------------------------------------- +-- Sizing, and the decision not to bother. +-- +-- Anser can decline to build a filter at four points, and these cases walk the +-- boundary of each. Declining is always safe: it costs the query its filter, +-- never its correctness. The payload cap is pinned here because every size +-- below follows from it. +-- --------------------------------------------------------------------------- +SET anser.max_info_size = 68157440; +-- 1. The planner's gate -- the only one that costs nothing, since nothing is +-- injected and no consumer is left waiting. A 64 MB bitset is 536870912 bits, +-- so 134217728 estimated keys is exactly 4 bits/key and one more key is not. +-- A zero or negative estimate is treated as one key, not as "no filter". +SELECT t.est_rows, s.injected, s.total_elems, s.planned_bytes +FROM (VALUES (0), (-5), (1), (1000), (1000000), (10000000), + (134217728), (134217729), (150000000), (1000000000)) + AS t(est_rows), + LATERAL anser_test_rf_size(t.est_rows) AS s; + est_rows | injected | total_elems | planned_bytes +------------+----------+-------------+--------------- + 0 | t | 1 | 1048576 + -5 | t | 1 | 1048576 + 1 | t | 1 | 1048576 + 1000 | t | 1000 | 1048576 + 1000000 | t | 1000000 | 1048576 + 10000000 | t | 10000000 | 16777216 + 134217728 | t | 134217728 | 67108864 + 134217729 | f | | + 150000000 | f | | + 1000000000 | f | | +(10 rows) + +-- 2. Filter construction. bloom_create floors every bitset at 1 MB and that +-- floor overrides the work_mem cap, so a cap below "1 MB + header" can only +-- yield a filter too large to send -- which is why a flat 1 MB cap is refused +-- while 1048592 (the floor plus exactly one header) is accepted and serializes +-- to precisely the cap. fits_cap is the invariant that must never be false. +SELECT t.what, t.elems, t.cap, s.built, s.bits, s.serialized, + round(s.bits_per_key::numeric, 3) AS bits_per_key, + (NOT s.built OR s.serialized <= t.cap) AS fits_cap +FROM (VALUES + ('no keys', 0::bigint, 1048640::bigint), + ('negative keys', -1, 1048640), + ('cap equals the header', 32, 16), + ('cap one byte over the header', 32, 17), + ('cap is 1 MB, no header room', 32, 1048576), + ('cap is 1 MB plus a header', 32, 1048592), + ('cap one byte short of that', 32, 1048591), + ('a single key', 1, 1048640), + ('exactly 4 bits/key', 2097152, 1048640), + ('one key too many', 2097153, 1048640), + ('exactly 4 bits/key at 2 MB', 4194304, 2097216), + ('one key too many at 2 MB', 4194305, 2097216), + ('a billion keys', 1000000000, 68157440)) + AS t(what, elems, cap), + LATERAL anser_test_bloom_shape(t.elems, t.cap) AS s; + what | elems | cap | built | bits | serialized | bits_per_key | fits_cap +------------------------------+------------+----------+-------+----------+------------+--------------+---------- + no keys | 0 | 1048640 | f | | | | t + negative keys | -1 | 1048640 | f | | | | t + cap equals the header | 32 | 16 | f | | | | t + cap one byte over the header | 32 | 17 | f | | | | t + cap is 1 MB, no header room | 32 | 1048576 | f | | | | t + cap is 1 MB plus a header | 32 | 1048592 | t | 8388608 | 1048592 | 262144.000 | t + cap one byte short of that | 32 | 1048591 | f | | | | t + a single key | 1 | 1048640 | t | 8388608 | 1048592 | 8388608.000 | t + exactly 4 bits/key | 2097152 | 1048640 | t | 8388608 | 1048592 | 4.000 | t + one key too many | 2097153 | 1048640 | f | | | | t + exactly 4 bits/key at 2 MB | 4194304 | 2097216 | t | 16777216 | 2097168 | 4.000 | t + one key too many at 2 MB | 4194305 | 2097216 | f | | | | t + a billion keys | 1000000000 | 68157440 | f | | | | t +(13 rows) + +-- 3. The coordinator's gate on a merged payload. The fill is fixed exactly by +-- setting whole bytes rather than by inserting keys, which would be both slow +-- and only statistically precise. The rule is "reject above 95%", so a payload +-- sitting exactly on the limit is still delivered. Malformed framing is not +-- worth delivering either, since it cannot be a filter at all. +SELECT t.what, t.bitset_bytes, t.bytes_set, t.damage, + anser_test_worth_delivering(t.bitset_bytes, t.bytes_set, t.damage) + AS worth_delivering +FROM (VALUES + ('empty', 1000, 0, ''), + ('half full', 1000, 500, ''), + ('just under the limit', 1000, 949, ''), + ('exactly on the limit', 1000, 950, ''), + ('just over the limit', 1000, 951, ''), + ('completely full', 1000, 1000, ''), + ('no bitset at all', 0, 0, ''), + ('corrupt magic', 1000, 0, 'magic'), + ('corrupt version', 1000, 0, 'version'), + ('zero part count', 1000, 0, 'parts'), + ('null payload', 1000, 0, 'null')) + AS t(what, bitset_bytes, bytes_set, damage); + what | bitset_bytes | bytes_set | damage | worth_delivering +----------------------+--------------+-----------+---------+------------------ + empty | 1000 | 0 | | t + half full | 1000 | 500 | | t + just under the limit | 1000 | 949 | | t + exactly on the limit | 1000 | 950 | | t + just over the limit | 1000 | 951 | | f + completely full | 1000 | 1000 | | f + no bitset at all | 0 | 0 | | f + corrupt magic | 1000 | 0 | magic | f + corrupt version | 1000 | 0 | version | f + zero part count | 1000 | 0 | parts | f + null payload | 1000 | 0 | null | f +(11 rows) + +-- 4. The producer driven end to end on the coordinator-local path: build, +-- insert, publish, consume. "no-filter:cancelled" is the case that matters +-- most -- construction declined, so the cancel goes out before the build side +-- is scanned and no consumer waits for its timeout. The last row is the one +-- the planner cannot predict: a filter sized for 32 keys that receives three +-- million, which saturates and so publishes a cancel instead of a payload. +SELECT t.what, + anser_test_producer_decision(t.elems, t.cap, t.n_keys) AS decision +FROM (VALUES + ('sparse filter', 32::bigint, 1048640::bigint, 100), + ('cap with no header room', 32, 1048576, 100), + ('no keys declared', 0, 1048640, 0), + ('one key past the floor', 2097153, 1048640, 0), + ('exactly 4 bits/key', 2097152, 1048640, 100000), + ('estimate of 32, three million inserted', 32, 1048640, 3000000)) + AS t(what, elems, cap, n_keys); + what | decision +----------------------------------------+--------------------- + sparse filter | built:delivered + cap with no header room | no-filter:cancelled + no keys declared | no-filter:cancelled + one key past the floor | no-filter:cancelled + exactly 4 bits/key | built:delivered + estimate of 32, three million inserted | built:cancelled +(6 rows) + +-- --------------------------------------------------------------------------- +-- The wire protocol. +-- +-- Not exhaustive; a fuzzer would be the right tool for that. These cover the +-- properties that are cheap to check and expensive to lose: a fixed-width +-- header with the fields where they are documented, a payload that cannot be +-- mistaken for framing, a length cross-check that rejects a message which does +-- not add up, and a checksum that rejects one altered byte anywhere it covers. +-- --------------------------------------------------------------------------- +-- The bytes, exactly. Session/command/condition are 42/7/3 and part is 1 of 3, +-- so these are golden values: any change to the layout, the field widths or +-- what the checksum covers shows up here as a diff. Note the third row -- a +-- cancelled message carries no body even though one was passed, and its +-- checksum must be computed over what is actually sent. +-- +-- That the message comes back as text is itself the check that it is NUL-free, +-- which a NOTIFY payload has to be: the fourth row's body is 00 ff 00. +SELECT t.what, length(m.msg) AS len, m.msg +FROM (VALUES + ('part with a 3-byte body', 'P', 'B', 0, '\x616263'::bytea), + ('subscription, no body', 'S', '-', 0, '\x'::bytea), + ('cancelled: body suppressed', 'P', 'B', 1, '\x616263'::bytea), + ('body of NUL and 0xff bytes', 'P', 'B', 0, '\x00ff00'::bytea)) + AS t(what, kind, ptype, flags, body), + LATERAL (SELECT anser_test_wire_format(t.kind::"char", t.ptype::"char", + t.flags, 'anser_rf_3', t.body) + AS msg) m; + what | len | msg +----------------------------+-----+--------------------------------------------------------------------------------------------------------------- + part with a 3-byte body | 109 | anser3 P B 0000000042 0000000007 0000000003 0000000001 0000000003 0000 0010 0000000004 f7b3c0d5anser_rf_3YWJj + subscription, no body | 105 | anser3 S - 0000000042 0000000007 0000000003 0000000001 0000000003 0000 0010 0000000000 ecd98662anser_rf_3 + cancelled: body suppressed | 105 | anser3 P B 0000000042 0000000007 0000000003 0000000001 0000000003 0001 0010 0000000000 e88148e9anser_rf_3 + body of NUL and 0xff bytes | 109 | anser3 P B 0000000042 0000000007 0000000003 0000000001 0000000003 0000 0010 0000000004 027f789banser_rf_3AP8A +(4 rows) + +-- Alter exactly one byte (or one length) and see what the reader does. +-- +-- The split matters: "malformed" is rejected by framing, before a byte is +-- allocated or a channel created, while "checksum-mismatch" got past framing +-- and was caught by the CRC. Corrupting the kind byte is the interesting one +-- -- the message stays perfectly well-formed, and only the checksum notices, +-- which is why the checksum covers the header and not just the payload. +SELECT t.tamper, + anser_test_wire_roundtrip('anser_rf_3', '\x616263'::bytea, t.tamper, + 'B'::"char") AS outcome +FROM (VALUES (''), ('truncate'), ('append'), ('bodylen'), ('tag'), ('kind'), + ('type'), ('crc'), ('key'), ('body')) AS t(tamper); + tamper | outcome +----------+------------------- + | ok + truncate | malformed + append | malformed + bodylen | malformed + tag | malformed + kind | checksum-mismatch + type | unknown-type + crc | checksum-mismatch + key | checksum-mismatch + body | checksum-mismatch +(10 rows) + +-- Keys and bodies that must survive the trip unchanged. "ok" means every +-- field and the decoded body came back identical, so each of these is a case +-- where a naive framing would have gone wrong: the key travels as raw bytes +-- and is taken by length, which is what lets it contain a newline, a space, or +-- the protocol's own tag without any escaping. +SELECT t.what, + anser_test_wire_roundtrip(t.key, t.body, '', 'B'::"char") AS outcome +FROM (VALUES + ('plain key', 'anser_rf_3' , '\x616263'::bytea), + ('key containing a newline', E'rf:a\nb' , '\x616263'::bytea), + ('key that looks like a header', 'anser3 P B 0000000000', '\x616263'::bytea), + ('key with spaces and quotes', 'rf:"a b".c' , '\x616263'::bytea), + ('key at the 63-byte limit', repeat('k', 63) , '\x616263'::bytea), + ('empty key', '' , '\x616263'::bytea), + ('empty body', 'anser_rf_3' , '\x'::bytea), + ('body of a single NUL', 'anser_rf_3' , '\x00'::bytea), + ('body of framing-hostile bytes', 'anser_rf_3' , '\x00ff0a0d5c22'::bytea), + ('1 KB body', 'anser_rf_3' , decode(repeat('00ff', 512), 'hex'))) + AS t(what, key, body); + what | outcome +-------------------------------+--------- + plain key | ok + key containing a newline | ok + key that looks like a header | ok + key with spaces and quotes | ok + key at the 63-byte limit | ok + empty key | ok + empty body | ok + body of a single NUL | ok + body of framing-hostile bytes | ok + 1 KB body | ok +(10 rows) + +-- The QD -> QE checksum. The value itself does not matter; which inputs +-- change it does. Every routing field and the key are always covered, and the +-- body only for a payload type that asks for it -- the fifth column is that +-- deliberate exemption, not an oversight. +SELECT + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 2, 0, 'k', '\x01') AS condition_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 1, 'k', '\x01') AS flags_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'j', '\x01') AS key_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'k', '\x02') AS bloom_body_covered, + anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') + = anser_test_push_crc('-'::"char", 1, 0, 'k', '\x02') AS other_body_exempt, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') AS type_covered; + condition_covered | flags_covered | key_covered | bloom_body_covered | other_body_exempt | type_covered +-------------------+---------------+-------------+--------------------+-------------------+-------------- + t | t | t | t | t | t +(1 row) + +DROP EXTENSION anser_test; diff --git a/gpcontrib/anser/include/anser.h b/gpcontrib/anser/include/anser.h new file mode 100644 index 00000000000..5bfa5d772e8 --- /dev/null +++ b/gpcontrib/anser/include/anser.h @@ -0,0 +1,84 @@ +/*------------------------------------------------------------------------- + * + * 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 definitions for the Anser adaptive information sharing + * subsystem. + * + * Anser lets producers on the segments publish a small piece of information + * about a running query -- today a bloom filter over a join-build key -- have + * the coordinator combine the per-segment parts, and hand the result back to + * consumers on the segments, which use it to prune work. + * + * Everything travels over the dispatch connection the coordinator already + * holds open to each segment (see ansersideband.h), so there is no shared + * memory, no background worker and no second connection to authenticate: a + * channel exists only in the coordinator backend running the query, for as + * long as that query runs. + * + * IDENTIFICATION + * gpcontrib/anser/include/anser.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSER_H +#define ANSER_H + +#include "postgres.h" + +#define ANSER_CONDITION_KEY_SIZE 64 + +/* + * Identifies one channel: a single condition within one running command. + * + * condition_key is an opaque symbol naming the condition -- the + * optimizer-generated equivalence-class symbol described in the Anser paper. + * Channels are only ever compared for equality, never ordered. + */ +typedef struct AnserChannelKey +{ + int gp_session_id; + int gp_command_count; + uint32 condition_id; + char condition_key[ANSER_CONDITION_KEY_SIZE]; +} AnserChannelKey; + +/* GUCs (defined in anserinit.c). */ +extern bool gp_anser_enable; +extern bool gp_anser_runtime_filter; +extern bool gp_anser_debug; +extern int gp_anser_max_info_size; +extern int gp_anser_timeout_ms; + +/* + * Trace the handoff between producers, the coordinator and consumers. + * + * Every step of the exchange is invisible in EXPLAIN until it is over -- a + * consumer that waits and gets nothing looks exactly like one whose producers + * never published -- so the path is traceable on demand rather than only + * reconstructable from a debugger. LOG level, so it lands in the log of + * whichever process it happened in. + */ +#define ANSER_DEBUG(...) \ + do { \ + if (gp_anser_debug) \ + elog(LOG, __VA_ARGS__); \ + } while (0) + +#endif /* ANSER_H */ diff --git a/gpcontrib/anser/include/anserbloom.h b/gpcontrib/anser/include/anserbloom.h new file mode 100644 index 00000000000..24c7eaba7f6 --- /dev/null +++ b/gpcontrib/anser/include/anserbloom.h @@ -0,0 +1,79 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserbloom.h + * Standalone Anser Bloom filter producer/consumer executor helpers. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserbloom.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERBLOOM_H +#define ANSERBLOOM_H + +#include "postgres.h" + +#include "anser.h" +#include "lib/bloomfilter.h" + +/* + * Opaque executor state handles for the Bloom filter producer and + * consumer; the struct definitions are private to + * anserbloomproduce.c and anserbloomconsume.c. + */ +typedef struct AnserBloomFilterProduceState AnserBloomFilterProduceState; +typedef struct AnserBloomFilterConsumeState AnserBloomFilterConsumeState; + +/* Producer side: build one Bloom filter part and publish it to the channel. */ + +/* + * 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); +extern void ExecAnserBloomFilterProduceAddDatum(AnserBloomFilterProduceState *state, + Datum value, bool isnull); +extern bool ExecAnserBloomFilterProduceHasFilter(AnserBloomFilterProduceState *state); +extern bool ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state); +extern bool ExecAnserBloomFilterProduceCancel(AnserBloomFilterProduceState *state); +extern void ExecEndAnserBloomFilterProduce(AnserBloomFilterProduceState *state); + +/* 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); +extern bool ExecAnserBloomFilterConsume(AnserBloomFilterConsumeState *state, + long registration_timeout_ms); +extern bloom_filter *ExecAnserBloomFilterConsumerGetFilter( + AnserBloomFilterConsumeState *state); +extern uint32 ExecAnserBloomFilterConsumerReceivedParts( + AnserBloomFilterConsumeState *state); +extern bool ExecAnserBloomFilterConsumerWasCancelled( + AnserBloomFilterConsumeState *state); +extern void ExecEndAnserBloomFilterConsume(AnserBloomFilterConsumeState *state); + +#endif /* ANSERBLOOM_H */ diff --git a/gpcontrib/anser/include/anserfilter.h b/gpcontrib/anser/include/anserfilter.h new file mode 100644 index 00000000000..7e50e25c042 --- /dev/null +++ b/gpcontrib/anser/include/anserfilter.h @@ -0,0 +1,127 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserfilter.h + * Bloom-filter payload helpers for Anser channels. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserfilter.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERFILTER_H +#define ANSERFILTER_H + +#include "postgres.h" + +#include "lib/bloomfilter.h" + +#define ANSER_BLOOM_PART_MAGIC 0x41424631U /* ABF1 */ +#define ANSER_BLOOM_PART_VERSION 1U + +/* + * On-wire framing for a serialized bloom part. It deliberately does NOT carry + * the bitset parameters (size / seed / hash count): both the producer and the + * consumer build the filter with bloom_create from the same plan parameters, so + * the shape is agreed by construction and never reconstructed from the wire. + * magic/version guard the framing; part_index/total_parts track the coordinator + * fold count (surfaced as diagnostics). Kept as a struct for forward + * extensibility. + */ +typedef struct AnserBloomPartHeader +{ + uint32 magic; + uint32 version; + uint32 part_index; + uint32 total_parts; +} AnserBloomPartHeader; + +/* + * When a bloom filter stops being worth building, or sending once built. + * Three checks, at the three points where new information arrives. + * + * ANSER_BLOOM_MIN_BITS_PER_KEY -- a floor on the *planned* density: bitset bits + * divided by the number of distinct keys we expect. Below it no amount of care + * in building the filter helps, so the filter is not built and, at plan time, + * the nodes are not even injected. At 4 bits/key the optimal hash count is 3 + * and the false positive rate is already ~15%; below that it collapses (2 + * bits/key is ~40%). + * + * ANSER_BLOOM_MAX_FPR -- the check on the *realized* filter, at publish time. + * It catches what a row estimate cannot: the estimate was too low, so the + * filter saturated anyway. This one is expressed as a false positive rate + * rather than a fill fraction on purpose. FPR is fill^k, so a single fill + * limit is not a single quality bar: 80% full is a 51% FPR at k=3 but only + * 10.7% at k=10, and a rule that cancelled the latter would be throwing away a + * filter that eliminates nine probe rows in ten. The producer has the filter + * and therefore its k, so it can just ask. + * + * ANSER_BLOOM_MERGED_MAX_FILL -- the same question asked by the coordinator of + * the merged payload, which is the only place it can be asked about what + * consumers will actually receive. It has to fall back on fill, because k is + * derived from plan parameters and is not carried in the serialized part. It + * is therefore set where even k=10 is past saving (0.95^10 = 60% FPR), so that + * it only ever rejects the hopeless. Putting k in AnserBloomPartHeader would + * let this use the FPR too. + * + * All three numbers are first cuts. Calibrating them is the "stop building + * filters that add nothing" part of the bloom-performance work; the debug trace + * logs fill and FPR at every decision point so that study has data. + */ +#define ANSER_BLOOM_MIN_BITS_PER_KEY 4.0 +#define ANSER_BLOOM_MAX_FPR 0.50 +#define ANSER_BLOOM_MERGED_MAX_FILL 0.95 + +/* + * Is a serialized part (or a merged accumulator of them) still worth + * delivering? False when too many of its bits are set for it to reject + * anything useful. Works on the wire form, so the coordinator can ask this of + * a merged payload without rebuilding a filter. + */ +extern bool AnserBloomPartWorthSending(const void *payload, Size payload_len); + +extern uint64 AnserBloomSeed(const char *condition_key); + +/* + * Build an empty filter, or return NULL when AnserBloomShapeFor says it is not + * worth building. A NULL return is not an error: the producer turns it into an + * immediate cancel, so consumers stop waiting instead of timing out. + */ +extern bloom_filter *AnserBloomCreate(int64 total_elems, + Size max_payload_bytes, + uint64 seed); +extern Size AnserBloomSerializedSize(const bloom_filter *filter); +extern bool AnserBloomSerializePart(const bloom_filter *filter, + uint32 part_index, + uint32 total_parts, + void *buffer, + Size buffer_size, + Size *payload_len); +extern bloom_filter *AnserBloomDeserializePart(const void *payload, + Size payload_len, + int64 total_elems, + Size max_payload_bytes, + uint64 seed, + uint32 *part_index, + uint32 *total_parts); +extern bool AnserBloomLooksLikePart(const void *payload, Size payload_len); +extern bool AnserBloomFoldPartInPlace(void *acc, Size acc_len, + const void *part, Size part_len); + +#endif /* ANSERFILTER_H */ diff --git a/gpcontrib/anser/include/anserpayload.h b/gpcontrib/anser/include/anserpayload.h new file mode 100644 index 00000000000..cb5f30dffcf --- /dev/null +++ b/gpcontrib/anser/include/anserpayload.h @@ -0,0 +1,150 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserpayload.h + * What a channel carries, and who knows what to do with it. + * + * The transport (ansersideband.c on segments, anserdispatch.c on the + * coordinator) moves opaque bytes: it cannot tell a bloom filter from a row + * count, and should not have to. Everything that depends on what the bytes + * mean lives in one descriptor per payload type, so a new kind of runtime + * information needs no change to the framing, the checksumming or the channel + * bookkeeping. + * + * Exactly two things genuinely differ per type. + * + * How parts merge. The coordinator receives one part per producer and must + * reduce them to a single payload. For a bloom filter that is a bitwise OR of + * equal-sized bitsets; for a row count it would be a sum; for a min/max + * summary it would be two comparisons. There is no generic answer, so each + * type supplies fold(). + * + * Whether corruption can give a wrong answer. This is what 'checksum_body' + * decides, and it is about consequence, not size. A bloom filter fails + * asymmetrically: a bit flipped 0 -> 1 merely costs selectivity, but a bit + * flipped 1 -> 0 removes a key from the filter, so a joinable row is rejected + * and the query returns fewer rows than it should -- silently. A row count, by + * contrast, only feeds a planning decision: a corrupted one costs a worse plan, + * never an incorrect result. It does not need to be checksummed, and paying + * 0.05 ms/MB to checksum it anyway would be waste. + * + * Note what 'checksum_body' does *not* cover: the header and the condition key + * are checksummed for every message regardless. That is ~100 ns for a 95-byte + * header and it is what validates the routing fields, so there is nothing to + * gain by skipping it; the flag controls only the payload, which is where the + * cost lives. + * + * To add a payload type: + * + * 1. #define a code below. It travels as one byte and appears in the + * anser.debug trace, so keep it printable -- and it may not be '\0', + * which AnserPayloadTable uses to recognize a slot nobody registered. + * 2. Write a fold() and add a row to AnserPayloadTable in anserpayload.c, + * keyed by the code: the table is indexed by the wire byte, so the + * designator and the row's own 'code' field must agree (an assertion in + * AnserPayloadLookup catches it if they do not). + * 3. Pass the code to AnserSidebandPublish/AnserDispatchLocalPublish in the + * producer node, and to AnserSidebandConsumeWait/AnserDispatchLocalConsume + * in the consumer node. + * + * That is the whole extension point. In particular, do not add a branch on the + * payload type to the transport: if something there needs to know the type, + * that knowledge belongs in this descriptor instead. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserpayload.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERPAYLOAD_H +#define ANSERPAYLOAD_H + +#include "anser.h" + +/* + * Payload type codes. + * + * Distinct from the message kind (P/S), which says what a message *does* + * rather than what it *carries*: a subscription carries nothing, and a + * cancelled part still declares the type it would have carried. + */ +#define ANSER_PAYLOAD_NONE '-' /* no payload (a subscription) */ +#define ANSER_PAYLOAD_BLOOM 'B' /* serialized bloom filter part */ + +/* '\0' is reserved: it is how an unregistered code is recognized. */ + +typedef struct AnserPayloadOps +{ + char code; /* the wire byte */ + const char *name; /* for logs; no other use */ + bool checksum_body; /* see the file header */ + + /* + * Merge 'part' into the accumulator in place. + * + * The accumulator is the first part the channel received, kept verbatim, so + * the two always have the same type -- but not necessarily the same length + * or internal parameters, which is what this has to check. Return false if + * they cannot be merged; the caller then cancels the channel, which costs + * the consumers their filter but is never silently wrong. + * + * Must not modify 'accum' unless it returns true: the caller reports a + * false return and carries on, so a half-folded accumulator would be + * indistinguishable from a good one. + * + * NULL for a type that is never folded (one that has no payload). + */ + bool (*fold) (void *accum, Size accum_len, + const void *part, Size part_len); + + /* + * Is the finished accumulator worth delivering? Asked once, after the last + * part has been folded and before anything is sent to a consumer; a false + * return cancels the channel, so consumers run unfiltered. + * + * This is the only place the question can be answered honestly, because + * folding changes the answer: OR-ing bloom parts together makes the result + * denser than any part, so three parts that each looked worth sending can + * merge into one that is not. Producers cannot see that, and consumers + * should not be made to pay for it. + * + * NULL for a type that is always worth delivering -- a row count does not + * become less useful for having been summed. + */ + bool (*worth_delivering) (const void *accum, Size accum_len); +} AnserPayloadOps; + +/* + * Resolve a code to its descriptor, or NULL if nothing has registered it. + * + * Callers must handle the NULL: the code arrives from the wire, so an + * unregistered one is a real case, and defaulting it silently would turn a + * forgotten table row into a mystery rather than a log line. + */ +extern const AnserPayloadOps *AnserPayloadLookup(char code); + +/* + * Whether a type's body is covered by the message checksum. Takes a code + * rather than a descriptor because the transport reaches this while parsing, + * before it has resolved anything; an unregistered code answers false, which + * is why this one needs no error handling. + */ +extern bool AnserPayloadChecksumsBody(char code); + +#endif /* ANSERPAYLOAD_H */ diff --git a/gpcontrib/anser/include/anserplan.h b/gpcontrib/anser/include/anserplan.h new file mode 100644 index 00000000000..bbfab22f066 --- /dev/null +++ b/gpcontrib/anser/include/anserplan.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. + * + * anserplan.h + * Post-planning transformation that injects Anser runtime bloom-filter + * producer/consumer nodes into a finished plan tree. + * + * IDENTIFICATION + * gpcontrib/anser/include/anserplan.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERPLAN_H +#define ANSERPLAN_H + +#include "nodes/plannodes.h" + +/* + * Post-plan pass: recognize the supported join shape in a finished PlannedStmt + * and inject an Anser bloom-filter producer (on the hash build side) and a + * consumer (above the probe scan). Called once from planner(), so it covers + * both the Postgres planner and ORCA. A no-op unless the Anser runtime-filter + * GUCs are on and this is a coordinator SELECT. + */ +extern void AnserApplyRuntimeFilters(PlannedStmt *stmt); + +/* + * Register the two CustomScan providers (producer, consumer) so their methods + * resolve by name when a dispatched plan is deserialized. Must run once per + * backend (QD and every QE) before any plan execution. + */ +extern void AnserRegisterRuntimeFilterMethods(void); + +/* + * Node builders (implemented in anserplanexec.c, where the CustomScan method + * tables live). Each wraps `child` in a pass-through CustomScan carrying the + * runtime-filter parameters in custom_private; the caller assigns plan_node_id. + * `key_attno` is the build (producer) / probe (consumer) join-key attno in the + * child's output tuple. + */ +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); +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); + +/* + * Bloom sizing for one join, from its estimated build cardinality. False means + * no filter is worth injecting -- see the density rule in anserfilter.h. + * + * Exposed rather than static because it is the cheapest of Anser's give-up + * decisions and therefore the one most worth testing directly at its boundary + * (anser_test.c); nothing but the injection pass and the tests should call it. + */ +extern bool AnserRuntimeFilterSize(double est_rows, int64 *total_elems, + int64 *max_payload, int64 *planned_bytes); + +#endif /* ANSERPLAN_H */ diff --git a/gpcontrib/anser/include/ansersideband.h b/gpcontrib/anser/include/ansersideband.h new file mode 100644 index 00000000000..2cbea9336cd --- /dev/null +++ b/gpcontrib/anser/include/ansersideband.h @@ -0,0 +1,249 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * ansersideband.h + * Anser transport over the existing QD <-> QE dispatch connection. + * + * Instead of opening a second libpq connection back to the coordinator, a + * segment executor reuses the connection the dispatcher already holds open: + * QE -> QD travels as a NOTIFY (the model nextval() uses, see + * cdb_sequence_nextval_qe in commands/sequence.c), and QD -> QE as a + * GP_SIDEBAND_MESSAGE the waiting QE reads off its own socket. + * + * The two directions are not symmetric, and the wire formats differ for a + * reason. A NOTIFY payload is delivered by pq_sendstring(), so it must be a + * NUL-free C string -- hence the text header and base64 body. The QD -> QE + * push is written with pqPutnchar(), which performs no conversion, so the + * merged filter travels as raw binary. That matters: the merged payload is + * sent once per consumer, while each part is sent once. + * + * Both directions carry a CRC32C. Not because the transport is lossy -- it is + * TCP, or a Unix socket; the UDP interconnect is a different channel entirely + * -- but because some payloads fail asymmetrically and TCP's 16-bit checksum is + * thin cover for a megabyte. The header and condition key are always covered; + * whether the body is covered too is the payload type's decision + * (AnserPayloadOps.checksum_body, see anserpayload.h), because that is the part + * that costs 0.05 ms/MB and only some payloads can turn corruption into a wrong + * answer. The QE -> QD checksum covers the raw pre-base64 bytes, so where it + * applies it validates the decode as well. + * + * Neither direction branches on the payload type beyond consulting that one + * flag: everything else about what the bytes mean belongs in the registry. + * + * IDENTIFICATION + * gpcontrib/anser/include/ansersideband.h + * + *------------------------------------------------------------------------- + */ +#ifndef ANSERSIDEBAND_H +#define ANSERSIDEBAND_H + +#include "anser.h" +#include "anserpayload.h" +#include "port/pg_crc32c.h" + +struct CdbDispatchResult; /* #include "cdb/cdbdispatchresult.h" */ +struct pgNotify; /* #include "libpq-fe.h" */ + +/* + * NOTIFY channel for QE -> QD Anser traffic. The dispatcher hands notifies it + * does not recognize to cdbdisp_notify_hook, where we match on this name. + */ +#define ANSER_NOTIFY_CHANNEL "anser_rf" + +/* First token of every QE -> QD payload; bump when the format changes. */ +#define ANSER_WIRE_TAG "anser3" + +/* + * Message kinds (QE -> QD): what the message does. What it carries is a + * separate field, the payload type -- see anserpayload.h. + */ +#define ANSER_WIRE_KIND_PART 'P' /* a producer's serialized part */ +#define ANSER_WIRE_KIND_SUBSCRIBE 'S' /* a consumer registering interest */ + +/* Flag bits, shared by both directions. */ +#define ANSER_WIRE_F_CANCELLED 0x0001 + +/* + * QE -> QD header: fixed width, so the key and body start at known offsets and + * nothing about their contents can affect parsing. + * + * anser3 K T SSSSSSSSSS CCCCCCCCCC DDDDDDDDDD PPPPPPPPPP TTTTTTTTTT FFFF KKKK BBBBBBBBBB XXXXXXXX + * ^tag ^ ^ session command condition part total ^ ^ bodylen crc + * | \ payload type flags keylen + * \ kind + * + * followed immediately by key bytes and body bytes. Every + * field is zero-padded (flags and CRC are hex, the rest decimal), so the header + * is pure ASCII of a constant length and holds no delimiter that payload bytes + * could imitate. The two lengths are then a cross-check: the total payload + * length must be exactly the header plus both, or the message is rejected. + * + * The signed session and command ids travel as uint32 and are cast back on + * receipt, which round-trips them exactly while keeping the width fixed -- + * "%010d" of a negative value is eleven characters, which would have made the + * header variable-width again. + * + * The previous format ended its header with a newline and found it with + * strchr(). That was correct only because the header could not itself contain + * a newline -- an invariant nothing enforced, and one that a key derived from + * relation names (quoted identifiers may contain anything) would have broken + * silently. + */ +#define ANSER_WIRE_HDR_FORMAT \ + ANSER_WIRE_TAG " %c %c %010u %010u %010u %010u %010u %04x %04u %010u %08x" + +/* + * Read format for the same header. The widths are what make the parse safe: + * every conversion stops after exactly its field, so no value can consume the + * separator or run into the key bytes that follow the header. + */ +#define ANSER_WIRE_HDR_SCANF \ + ANSER_WIRE_TAG " %c %c %10u %10u %10u %10u %10u %4x %4u %10u %8x" + +/* 6 + the 11 fields above, each preceded by its separating space. */ +#define ANSER_WIRE_HDR_LEN (6 + (1 + 1) + (1 + 1) + (1 + 10) + (1 + 10) \ + + (1 + 10) + (1 + 10) + (1 + 10) + (1 + 4) \ + + (1 + 4) + (1 + 10) + (1 + 8)) + +/* Offset of the 8-hex-digit CRC; the checksum covers everything before it. */ +#define ANSER_WIRE_CRC_OFFSET (ANSER_WIRE_HDR_LEN - 8) + +/* + * Largest key and body a header can describe. Checked before formatting so a + * field can never overflow its width and shift every field after it. + */ +#define ANSER_WIRE_MAX_KEYLEN 9999 +#define ANSER_WIRE_MAX_BODYLEN 999999999 + +/* + * The codec. + * + * Four functions, exported rather than static because together they *are* the + * protocol: the two directions have to agree byte for byte about offsets and + * about what the checksum covers, and that agreement is only auditable -- and + * only testable -- if both halves are reachable by name. sql/anser_test.sql + * drives them directly. + * + * They are implemented on the side that writes: AnserWireFormat in + * ansersideband.c (only a QE formats), AnserWireParse and AnserWireCheckCrc in + * anserdispatch.c (only the QD parses). + * + * AnserWireFormat returns a palloc'd NUL-terminated string. AnserWireParse + * fills 'out' and returns false for anything it will not vouch for; its + * contract and the checks it makes are documented at the definition. + * AnserWireCheckCrc takes the *decoded* body, or NULL/0 when there is none. + */ +typedef struct AnserWireMsg +{ + const char *wire; /* the message itself; the CRC covers its head */ + char kind; + char payload_type; /* resolved against the registry by the caller */ + AnserChannelKey key; + int key_len; + int part_index; + int total_parts; + int flags; + const char *body; /* base64, not NUL-terminated */ + int body_len; + uint32 crc; /* of the header, key and decoded body */ +} AnserWireMsg; + +extern char *AnserWireFormat(const AnserChannelKey *channel_key, char kind, + char payload_type, uint32 part_index, + uint32 total_parts, int flags, + const void *payload, Size payload_len); +extern bool AnserWireParse(const char *msg, AnserWireMsg *out); +extern bool AnserWireCheckCrc(const AnserWireMsg *msg, const void *body, + Size body_len); + +/* + * QD -> QE push: raw binary, written with pqPutInt/pqPutnchar. + * + * payload_type | crc32c | condition_id | flags | keylen | key | bodylen | body + * + * with each field a 4-byte integer except the key and body. The CRC covers all + * of the other fields in that order, the integers in network byte order, so it + * validates the routing information as well as the payload; the body is + * included only when the payload type says so. Both ends compute it through + * this one function so the two cannot drift apart. It is implemented in + * ansersideband.c, beside the reader that verifies it. + */ +extern pg_crc32c AnserWirePushCrc(char payload_type, uint32 condition_id, + uint32 flags, const char *key, int keylen, + const void *body, int bodylen); + +/* + * QE side (ansersideband.c). + * + * AnserSidebandPublish is fire-and-forget: unlike the libpq transport it does + * not wait for the coordinator to acknowledge the part. 'payload_type' is + * stamped on the wire even when cancelled, since it is what tells the + * coordinator how to fold the channel's parts. + * + * AnserSidebandConsumeWait blocks on this backend's own dispatch socket until + * the merged payload arrives, the channel is cancelled, or timeout_ms elapses. + * On success *payload is palloc'd in the caller's context. A false return + * always means "run unfiltered", never an error -- including when what arrived + * is not of 'payload_type', which would mean two different kinds of information + * had collided on one channel. + */ +extern bool AnserSidebandPublish(const AnserChannelKey *channel_key, + char payload_type, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, + bool cancelled); +extern bool AnserSidebandConsumeWait(const AnserChannelKey *channel_key, + char payload_type, + void **payload, Size *payload_len, + bool *cancelled, long timeout_ms); + +/* + * QD side (anserdispatch.c). + * + * AnserDispatchNotifyHandler is installed as cdbdisp_notify_hook; it folds + * arriving parts and pushes the merged payload to subscribers. The Local + * variants serve producers and consumers running on the coordinator itself, + * which have no dispatch connection to themselves and so operate on the same + * per-query channel table directly. They take the same payload type for the + * same reasons, so that a coordinator-local producer and a segment producer are + * interchangeable on one channel. + */ +extern bool AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, + struct pgNotify *notify); +extern bool AnserDispatchLocalPublish(const AnserChannelKey *channel_key, + char payload_type, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, + bool cancelled); +extern bool AnserDispatchLocalConsume(const AnserChannelKey *channel_key, + char payload_type, + void **payload, Size *payload_len, + bool *cancelled); + +/* + * Drop per-query state. AnserSidebandResetAll does both sides and is what + * the executor/transaction-end callbacks call; the halves are exposed because + * each lives with the state it owns. + */ +extern void AnserDispatchReset(void); +extern void AnserSidebandResetInbox(void); +extern void AnserSidebandResetAll(void); + +#endif /* ANSERSIDEBAND_H */ diff --git a/gpcontrib/anser/sql/anser_runtime_filter.sql b/gpcontrib/anser/sql/anser_runtime_filter.sql new file mode 100644 index 00000000000..c8a100a2e7c --- /dev/null +++ b/gpcontrib/anser/sql/anser_runtime_filter.sql @@ -0,0 +1,143 @@ +-- Anser runtime bloom filter: plan-tree integration (PR4). +-- +-- Requires the cluster to run with anser.enable=on so the gather/send +-- services are live. On a multi-segment cluster the build side redistributes, +-- exercising the Motion-directly-under-CustomScan case; on a single segment it +-- degrades to the leaf case. Either way the feature must (a) inject the +-- producer/consumer nodes and (b) never change query results. + + +-- Deterministic plan shape: force a hash join. +SET enable_nestloop = off; +SET enable_mergejoin = off; + +-- build is the smaller (hashed / preserved) side, distributed so the join key +-- must be redistributed; probe is the larger side distributed by the join key. +CREATE TABLE anser_rf_build (id int, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe (id int, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build; +ANALYZE anser_rf_probe; + +-- Use the Postgres planner for a deterministic plan shape. +SET optimizer = off; + +-- With the filter on the plan carries an "Anser Bloom Producer" under the Hash +-- and an "Anser Bloom Consumer" above the probe scan (with the planned bloom +-- size); with it off neither node appears. (COSTS OFF keeps the output stable.) +SET anser.runtime_filter = on; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + +SET anser.runtime_filter = off; +EXPLAIN (COSTS OFF) +SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id; + +-- Correctness: identical results with the filter on vs off (Postgres planner). +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_on AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_r_off AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); + +SELECT count(*) AS rows_on FROM anser_rf_r_on; +SELECT count(*) AS rows_off FROM anser_rf_r_off; +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_r_on EXCEPT ALL SELECT * FROM anser_rf_r_off) d; +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_on) d; + +-- Correctness must also hold under ORCA (whether or not it injects the nodes). +SET optimizer = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_orca AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_orca + FROM (SELECT * FROM anser_rf_r_orca EXCEPT ALL SELECT * FROM anser_rf_r_off) d; +SELECT count(*) AS only_off_orca + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_orca) d; + +-- Pushdown mix: with gp_enable_runtime_filter_pushdown on, the consumer hands +-- the unioned filter to the probe SeqScan as an SK_BLOOM_FILTER scan key and +-- the scan (or the table AM) does the pruning. Results must still match the +-- filter-off run. +SET optimizer = off; +SET gp_enable_runtime_filter_pushdown = on; +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_r_push AS + SELECT b.name, p.payload FROM anser_rf_build b LEFT JOIN anser_rf_probe p ON b.id = p.id + DISTRIBUTED BY (name); +SELECT count(*) AS only_push + FROM (SELECT * FROM anser_rf_r_push EXCEPT ALL SELECT * FROM anser_rf_r_off) d; +SELECT count(*) AS only_off_push + FROM (SELECT * FROM anser_rf_r_off EXCEPT ALL SELECT * FROM anser_rf_r_push) d; +RESET gp_enable_runtime_filter_pushdown; + +DROP TABLE anser_rf_r_push; +DROP TABLE anser_rf_r_on, anser_rf_r_off, anser_rf_r_orca; +DROP TABLE anser_rf_build, anser_rf_probe; + +-- Datatype guard: producer and consumer hash the raw Datum bytes, so injection +-- is restricted to keys where SQL equality is bitwise Datum equality. A +-- cross-type join (float4 vs float8 Datums for equal values differ) must not +-- be injected; results must match the filter-off run either way. +CREATE TABLE anser_rf_build_f (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_f (id float4, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_f SELECT g, 'b' || g FROM generate_series(1, 200) g; +INSERT INTO anser_rf_probe_f SELECT g, 'p' || g FROM generate_series(1, 2000) g; +ANALYZE anser_rf_build_f; +ANALYZE anser_rf_probe_f; + +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_f_on AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_f_off AS + SELECT b.name, p.payload FROM anser_rf_build_f b JOIN anser_rf_probe_f p ON b.id = p.id + DISTRIBUTED BY (name); + +SELECT count(*) AS rows_on FROM anser_rf_f_on; +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_f_on EXCEPT ALL SELECT * FROM anser_rf_f_off) d; +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_f_off EXCEPT ALL SELECT * FROM anser_rf_f_on) d; + +DROP TABLE anser_rf_f_on, anser_rf_f_off, anser_rf_build_f, anser_rf_probe_f; + +-- Same-typed float keys are also excluded: -0.0 and 0.0 compare equal in SQL +-- but are not bitwise equal, so hashing raw Datums could prune a joinable row. +CREATE TABLE anser_rf_build_z (id float8, name text) DISTRIBUTED BY (name); +CREATE TABLE anser_rf_probe_z (id float8, payload text) DISTRIBUTED BY (id); +INSERT INTO anser_rf_build_z VALUES (1, 'b1'), (-0.0, 'bz'); +INSERT INTO anser_rf_probe_z VALUES (1, 'p1'), (0.0, 'pz'), (2, 'p2'); +ANALYZE anser_rf_build_z; +ANALYZE anser_rf_probe_z; + +SET anser.runtime_filter = on; +CREATE TEMP TABLE anser_rf_z_on AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); +SET anser.runtime_filter = off; +CREATE TEMP TABLE anser_rf_z_off AS + SELECT b.name, p.payload FROM anser_rf_build_z b JOIN anser_rf_probe_z p ON b.id = p.id + DISTRIBUTED BY (name); + +SELECT count(*) AS rows_on FROM anser_rf_z_on; +SELECT count(*) AS only_on + FROM (SELECT * FROM anser_rf_z_on EXCEPT ALL SELECT * FROM anser_rf_z_off) d; +SELECT count(*) AS only_off + FROM (SELECT * FROM anser_rf_z_off EXCEPT ALL SELECT * FROM anser_rf_z_on) d; + +DROP TABLE anser_rf_z_on, anser_rf_z_off, anser_rf_build_z, anser_rf_probe_z; + +RESET anser.runtime_filter; +RESET optimizer; +RESET enable_nestloop; +RESET enable_mergejoin; + diff --git a/gpcontrib/anser/sql/anser_test.sql b/gpcontrib/anser/sql/anser_test.sql new file mode 100644 index 00000000000..0c6e3585c89 --- /dev/null +++ b/gpcontrib/anser/sql/anser_test.sql @@ -0,0 +1,184 @@ +CREATE EXTENSION anser_test; + +-- Bloom payload protocol: serialize one part and read it back. +SELECT anser_test_bloom_roundtrip('bf_roundtrip', 42); + +-- In-place fold: same-size union mutates the accumulator; a mismatched size is +-- rejected. This is the coordinator's only combine path (the first part is +-- kept verbatim, every later part folds into it). +SELECT anser_test_bloom_fold_inplace() AS fold_inplace_ok; + +-- Safety regression: the consumer rebuilds the filter from its own parameters +-- and requires the received bitset to be exactly the expected size (and the +-- header magic to match); truncated/oversized/corrupt parts are rejected. +SELECT anser_test_bloom_rejects_mismatch() AS reject_mismatch; + +-- Producer and consumer driven end to end in one backend: publish a part, let +-- the coordinator side merge it, then receive and query the filter. +SELECT anser_test_node_roundtrip(168); + +-- --------------------------------------------------------------------------- +-- Sizing, and the decision not to bother. +-- +-- Anser can decline to build a filter at four points, and these cases walk the +-- boundary of each. Declining is always safe: it costs the query its filter, +-- never its correctness. The payload cap is pinned here because every size +-- below follows from it. +-- --------------------------------------------------------------------------- +SET anser.max_info_size = 68157440; + +-- 1. The planner's gate -- the only one that costs nothing, since nothing is +-- injected and no consumer is left waiting. A 64 MB bitset is 536870912 bits, +-- so 134217728 estimated keys is exactly 4 bits/key and one more key is not. +-- A zero or negative estimate is treated as one key, not as "no filter". +SELECT t.est_rows, s.injected, s.total_elems, s.planned_bytes +FROM (VALUES (0), (-5), (1), (1000), (1000000), (10000000), + (134217728), (134217729), (150000000), (1000000000)) + AS t(est_rows), + LATERAL anser_test_rf_size(t.est_rows) AS s; + +-- 2. Filter construction. bloom_create floors every bitset at 1 MB and that +-- floor overrides the work_mem cap, so a cap below "1 MB + header" can only +-- yield a filter too large to send -- which is why a flat 1 MB cap is refused +-- while 1048592 (the floor plus exactly one header) is accepted and serializes +-- to precisely the cap. fits_cap is the invariant that must never be false. +SELECT t.what, t.elems, t.cap, s.built, s.bits, s.serialized, + round(s.bits_per_key::numeric, 3) AS bits_per_key, + (NOT s.built OR s.serialized <= t.cap) AS fits_cap +FROM (VALUES + ('no keys', 0::bigint, 1048640::bigint), + ('negative keys', -1, 1048640), + ('cap equals the header', 32, 16), + ('cap one byte over the header', 32, 17), + ('cap is 1 MB, no header room', 32, 1048576), + ('cap is 1 MB plus a header', 32, 1048592), + ('cap one byte short of that', 32, 1048591), + ('a single key', 1, 1048640), + ('exactly 4 bits/key', 2097152, 1048640), + ('one key too many', 2097153, 1048640), + ('exactly 4 bits/key at 2 MB', 4194304, 2097216), + ('one key too many at 2 MB', 4194305, 2097216), + ('a billion keys', 1000000000, 68157440)) + AS t(what, elems, cap), + LATERAL anser_test_bloom_shape(t.elems, t.cap) AS s; + +-- 3. The coordinator's gate on a merged payload. The fill is fixed exactly by +-- setting whole bytes rather than by inserting keys, which would be both slow +-- and only statistically precise. The rule is "reject above 95%", so a payload +-- sitting exactly on the limit is still delivered. Malformed framing is not +-- worth delivering either, since it cannot be a filter at all. +SELECT t.what, t.bitset_bytes, t.bytes_set, t.damage, + anser_test_worth_delivering(t.bitset_bytes, t.bytes_set, t.damage) + AS worth_delivering +FROM (VALUES + ('empty', 1000, 0, ''), + ('half full', 1000, 500, ''), + ('just under the limit', 1000, 949, ''), + ('exactly on the limit', 1000, 950, ''), + ('just over the limit', 1000, 951, ''), + ('completely full', 1000, 1000, ''), + ('no bitset at all', 0, 0, ''), + ('corrupt magic', 1000, 0, 'magic'), + ('corrupt version', 1000, 0, 'version'), + ('zero part count', 1000, 0, 'parts'), + ('null payload', 1000, 0, 'null')) + AS t(what, bitset_bytes, bytes_set, damage); + +-- 4. The producer driven end to end on the coordinator-local path: build, +-- insert, publish, consume. "no-filter:cancelled" is the case that matters +-- most -- construction declined, so the cancel goes out before the build side +-- is scanned and no consumer waits for its timeout. The last row is the one +-- the planner cannot predict: a filter sized for 32 keys that receives three +-- million, which saturates and so publishes a cancel instead of a payload. +SELECT t.what, + anser_test_producer_decision(t.elems, t.cap, t.n_keys) AS decision +FROM (VALUES + ('sparse filter', 32::bigint, 1048640::bigint, 100), + ('cap with no header room', 32, 1048576, 100), + ('no keys declared', 0, 1048640, 0), + ('one key past the floor', 2097153, 1048640, 0), + ('exactly 4 bits/key', 2097152, 1048640, 100000), + ('estimate of 32, three million inserted', 32, 1048640, 3000000)) + AS t(what, elems, cap, n_keys); + +-- --------------------------------------------------------------------------- +-- The wire protocol. +-- +-- Not exhaustive; a fuzzer would be the right tool for that. These cover the +-- properties that are cheap to check and expensive to lose: a fixed-width +-- header with the fields where they are documented, a payload that cannot be +-- mistaken for framing, a length cross-check that rejects a message which does +-- not add up, and a checksum that rejects one altered byte anywhere it covers. +-- --------------------------------------------------------------------------- + +-- The bytes, exactly. Session/command/condition are 42/7/3 and part is 1 of 3, +-- so these are golden values: any change to the layout, the field widths or +-- what the checksum covers shows up here as a diff. Note the third row -- a +-- cancelled message carries no body even though one was passed, and its +-- checksum must be computed over what is actually sent. +-- +-- That the message comes back as text is itself the check that it is NUL-free, +-- which a NOTIFY payload has to be: the fourth row's body is 00 ff 00. +SELECT t.what, length(m.msg) AS len, m.msg +FROM (VALUES + ('part with a 3-byte body', 'P', 'B', 0, '\x616263'::bytea), + ('subscription, no body', 'S', '-', 0, '\x'::bytea), + ('cancelled: body suppressed', 'P', 'B', 1, '\x616263'::bytea), + ('body of NUL and 0xff bytes', 'P', 'B', 0, '\x00ff00'::bytea)) + AS t(what, kind, ptype, flags, body), + LATERAL (SELECT anser_test_wire_format(t.kind::"char", t.ptype::"char", + t.flags, 'anser_rf_3', t.body) + AS msg) m; + +-- Alter exactly one byte (or one length) and see what the reader does. +-- +-- The split matters: "malformed" is rejected by framing, before a byte is +-- allocated or a channel created, while "checksum-mismatch" got past framing +-- and was caught by the CRC. Corrupting the kind byte is the interesting one +-- -- the message stays perfectly well-formed, and only the checksum notices, +-- which is why the checksum covers the header and not just the payload. +SELECT t.tamper, + anser_test_wire_roundtrip('anser_rf_3', '\x616263'::bytea, t.tamper, + 'B'::"char") AS outcome +FROM (VALUES (''), ('truncate'), ('append'), ('bodylen'), ('tag'), ('kind'), + ('type'), ('crc'), ('key'), ('body')) AS t(tamper); + +-- Keys and bodies that must survive the trip unchanged. "ok" means every +-- field and the decoded body came back identical, so each of these is a case +-- where a naive framing would have gone wrong: the key travels as raw bytes +-- and is taken by length, which is what lets it contain a newline, a space, or +-- the protocol's own tag without any escaping. +SELECT t.what, + anser_test_wire_roundtrip(t.key, t.body, '', 'B'::"char") AS outcome +FROM (VALUES + ('plain key', 'anser_rf_3' , '\x616263'::bytea), + ('key containing a newline', E'rf:a\nb' , '\x616263'::bytea), + ('key that looks like a header', 'anser3 P B 0000000000', '\x616263'::bytea), + ('key with spaces and quotes', 'rf:"a b".c' , '\x616263'::bytea), + ('key at the 63-byte limit', repeat('k', 63) , '\x616263'::bytea), + ('empty key', '' , '\x616263'::bytea), + ('empty body', 'anser_rf_3' , '\x'::bytea), + ('body of a single NUL', 'anser_rf_3' , '\x00'::bytea), + ('body of framing-hostile bytes', 'anser_rf_3' , '\x00ff0a0d5c22'::bytea), + ('1 KB body', 'anser_rf_3' , decode(repeat('00ff', 512), 'hex'))) + AS t(what, key, body); + +-- The QD -> QE checksum. The value itself does not matter; which inputs +-- change it does. Every routing field and the key are always covered, and the +-- body only for a payload type that asks for it -- the fifth column is that +-- deliberate exemption, not an oversight. +SELECT + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 2, 0, 'k', '\x01') AS condition_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 1, 'k', '\x01') AS flags_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'j', '\x01') AS key_covered, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('B'::"char", 1, 0, 'k', '\x02') AS bloom_body_covered, + anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') + = anser_test_push_crc('-'::"char", 1, 0, 'k', '\x02') AS other_body_exempt, + anser_test_push_crc('B'::"char", 1, 0, 'k', '\x01') + <> anser_test_push_crc('-'::"char", 1, 0, 'k', '\x01') AS type_covered; + +DROP EXTENSION anser_test; diff --git a/gpcontrib/anser/src/anser_test.c b/gpcontrib/anser/src/anser_test.c new file mode 100644 index 00000000000..456d1a4a004 --- /dev/null +++ b/gpcontrib/anser/src/anser_test.c @@ -0,0 +1,765 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anser_test.c + * SQL-callable test helpers for the Anser subsystem. + * + * IDENTIFICATION + * gpcontrib/anser/src/anser_test.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "anser.h" +#include "anserbloom.h" +#include "anserfilter.h" +#include "anserpayload.h" +#include "anserplan.h" +#include "ansersideband.h" +#include "cdb/cdbvars.h" +#include "common/base64.h" +#include "fmgr.h" +#include "funcapi.h" +#include "lib/bloomfilter.h" +#include "miscadmin.h" +#include "utils/builtins.h" +#include "varatt.h" + +/* + * Bloom sizing used by the test helpers. bloom_create floors every filter at + * 1 MB, so these are the smallest filters we can build; producer and consumer + * sides must pass the identical pair (that is the whole point of carrying the + * parameters in the node rather than on the wire). + */ +#define ANSER_TEST_ELEMS 32 + +/* + * bloom_create floors every bitset at 1 MB, so a payload cap must be 1 MB + * *plus* room for the serialized-part header -- the same allowance the planner + * makes (ANSER_RF_HEADER_ROOM). Passing a flat 1 MB asks AnserBloomCreate for + * a filter that cannot fit the cap it was given, and it now declines. + */ +#define ANSER_TEST_MAX_PAYLOAD (1024 * 1024 + 64) + +PG_FUNCTION_INFO_V1(anser_test_bloom_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_bloom_fold_inplace); +PG_FUNCTION_INFO_V1(anser_test_bloom_rejects_mismatch); +PG_FUNCTION_INFO_V1(anser_test_node_roundtrip); + + +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(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, 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(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, seed); + right = AnserBloomCreate(ANSER_TEST_ELEMS, ANSER_TEST_MAX_PAYLOAD, seed); + if (left == NULL || right == NULL) + PG_RETURN_BOOL(false); + bloom_add_element(left, (unsigned char *) &left_value, sizeof(Datum)); + 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); +} + +/* Send a cancel request for conn's in-flight query (best effort). */ +/* Printable name for a channel state ("UNKNOWN" when out of range). */ +/* + * Drive a producer and a consumer through the whole path in this one backend. + * + * Coordinator-local, so it exercises the merge, the channel table and the + * lifetime rules without needing segments; the segment half (NOTIFY out, + * sideband message in) is covered by the runtime-filter test on a cluster. + */ +Datum +anser_test_node_roundtrip(PG_FUNCTION_ARGS) +{ + AnserChannelKey key; + AnserBloomFilterProduceState *producer; + AnserBloomFilterConsumeState *consumer; + int32 value_arg = PG_GETARG_INT32(0); + Datum value = Int32GetDatum(value_arg); + bool ok = false; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = gp_session_id; + key.gp_command_count = gp_command_count; + key.condition_id = 77; + strlcpy(key.condition_key, "node_roundtrip", ANSER_CONDITION_KEY_SIZE); + + PG_TRY(); + { + producer = ExecInitAnserBloomFilterProduce(&key, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, 0, 1); + if (producer == NULL) + ok = false; + else + { + ExecAnserBloomFilterProduceAddDatum(producer, value, false); + ok = ExecAnserBloomFilterProducePublish(producer); + ExecEndAnserBloomFilterProduce(producer); + } + + if (ok) + { + consumer = ExecInitAnserBloomFilterConsume(&key, ANSER_TEST_ELEMS, + ANSER_TEST_MAX_PAYLOAD, 1); + if (consumer == NULL) + ok = false; + else + { + ok = ExecAnserBloomFilterConsume(consumer, 1000) && + ExecAnserBloomFilterConsumerGetFilter(consumer) != NULL && + ExecAnserBloomFilterConsumerReceivedParts(consumer) == 1 && + !ExecAnserBloomFilterConsumerWasCancelled(consumer) && + !bloom_lacks_element(ExecAnserBloomFilterConsumerGetFilter(consumer), + (unsigned char *) &value, + sizeof(Datum)); + ExecEndAnserBloomFilterConsume(consumer); + } + } + } + PG_FINALLY(); + { + AnserSidebandResetAll(); + } + PG_END_TRY(); + + PG_RETURN_BOOL(ok); +} + +/* + * --------------------------------------------------------------------------- + * Sizing and give-up decisions. + * + * These four take their inputs as arguments and return what Anser decided, so + * the case tables live in sql/anser_test.sql where they are readable and the + * expected output records real sizes rather than a bare "ok". Between them + * they cover each of the four points where Anser can decide not to bother: + * the planner, filter construction, publication, and the merged result. + * --------------------------------------------------------------------------- + */ + +PG_FUNCTION_INFO_V1(anser_test_rf_size); +PG_FUNCTION_INFO_V1(anser_test_bloom_shape); +PG_FUNCTION_INFO_V1(anser_test_worth_delivering); +PG_FUNCTION_INFO_V1(anser_test_producer_decision); + +/* + * The planner gate: what AnserRuntimeFilterSize decides for an estimated build + * cardinality. NULL means no filter would be injected at all. + */ +Datum +anser_test_rf_size(PG_FUNCTION_ARGS) +{ + double est_rows = PG_GETARG_FLOAT8(0); + int64 total_elems = 0; + int64 max_payload = 0; + int64 planned_bytes = 0; + bool injected; + Datum values[4] = {0, 0, 0, 0}; + bool nulls[4] = {false, false, false, false}; + TupleDesc tupdesc; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "anser_test_rf_size: expected a composite return type"); + tupdesc = BlessTupleDesc(tupdesc); + + injected = AnserRuntimeFilterSize(est_rows, &total_elems, &max_payload, + &planned_bytes); + + values[0] = BoolGetDatum(injected); + values[1] = Int64GetDatum(total_elems); + values[2] = Int64GetDatum(max_payload); + values[3] = Int64GetDatum(planned_bytes); + nulls[1] = nulls[2] = nulls[3] = !injected; + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * Filter construction: what AnserBloomCreate makes of (total_elems, cap). + * NULL means it declined -- either the smallest possible filter does not fit + * the cap, or there are too many keys for it to be worth building. + * + * Returns the realized sizes so the expected output pins down bloom_create's + * actual behaviour (its 1 MB floor, its power-of-two rounding) and not merely + * our verdict on it. + */ +Datum +anser_test_bloom_shape(PG_FUNCTION_ARGS) +{ + int64 total_elems = PG_GETARG_INT64(0); + int64 cap = PG_GETARG_INT64(1); + bloom_filter *filter; + Datum values[4] = {0, 0, 0, 0}; + bool nulls[4] = {false, false, false, false}; + TupleDesc tupdesc; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "anser_test_bloom_shape: expected a composite return type"); + tupdesc = BlessTupleDesc(tupdesc); + + /* A negative cap would be a Size wraparound; the SQL side never sends one. */ + if (cap < 0) + elog(ERROR, "anser_test_bloom_shape: negative cap"); + + filter = AnserBloomCreate(total_elems, (Size) cap, AnserBloomSeed("shape")); + + values[0] = BoolGetDatum(filter != NULL); + nulls[1] = nulls[2] = nulls[3] = (filter == NULL); + if (filter != NULL) + { + values[1] = Int64GetDatum((int64) bloom_total_bits(filter)); + values[2] = Int64GetDatum((int64) AnserBloomSerializedSize(filter)); + values[3] = Float8GetDatum((double) bloom_total_bits(filter) / + (double) total_elems); + bloom_free(filter); + } + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* + * The coordinator's gate, on a synthetic part. + * + * Builds a part whose bitset has 'bytes_set' of its 'bitset_bytes' bytes fully + * set, which pins the fill fraction exactly -- reaching a given fill by + * inserting keys would be both slow and only statistically precise. 'damage' + * corrupts the framing instead: 'magic', 'version', 'parts' or 'null'. + */ +Datum +anser_test_worth_delivering(PG_FUNCTION_ARGS) +{ + int32 bitset_bytes = PG_GETARG_INT32(0); + int32 bytes_set = PG_GETARG_INT32(1); + const char *damage = text_to_cstring(PG_GETARG_TEXT_PP(2)); + AnserBloomPartHeader *header; + char *payload; + Size payload_len; + + if (strcmp(damage, "null") == 0) + PG_RETURN_BOOL(AnserBloomPartWorthSending(NULL, 1024)); + + if (bitset_bytes < 0 || bytes_set < 0 || bytes_set > bitset_bytes) + elog(ERROR, "anser_test_worth_delivering: bad bitset arguments"); + + payload_len = sizeof(AnserBloomPartHeader) + (Size) bitset_bytes; + payload = palloc0(payload_len); + header = (AnserBloomPartHeader *) payload; + header->magic = ANSER_BLOOM_PART_MAGIC; + header->version = ANSER_BLOOM_PART_VERSION; + header->part_index = 0; + header->total_parts = 1; + + if (strcmp(damage, "magic") == 0) + header->magic = ANSER_BLOOM_PART_MAGIC + 1; + else if (strcmp(damage, "version") == 0) + header->version = ANSER_BLOOM_PART_VERSION + 1; + else if (strcmp(damage, "parts") == 0) + header->total_parts = 0; + else if (damage[0] != '\0') + elog(ERROR, "anser_test_worth_delivering: unknown damage \"%s\"", damage); + + if (bytes_set > 0) + memset(payload + sizeof(AnserBloomPartHeader), 0xff, (Size) bytes_set); + + PG_RETURN_BOOL(AnserBloomPartWorthSending(payload, payload_len)); +} + +/* + * Producer end to end, on the coordinator-local path: build a filter for + * (total_elems, cap), insert 'n_keys' distinct keys, publish, then consume. + * + * Returns ":", where construction is built/no-filter + * and delivery is delivered/cancelled/missing. Every combination that can + * occur says something different: + * + * built:delivered the normal case + * built:cancelled the filter saturated, so publication became a cancel + * no-filter:cancelled construction declined, cancelled before any scan + * + * Each call takes a fresh condition_id, since several rows of one query share a + * session and command counter and would otherwise collide on one channel. + */ +Datum +anser_test_producer_decision(PG_FUNCTION_ARGS) +{ + static uint32 next_condition_id = 1000; + + int64 total_elems = PG_GETARG_INT64(0); + int64 cap = PG_GETARG_INT64(1); + int32 n_keys = PG_GETARG_INT32(2); + AnserChannelKey key; + AnserBloomFilterProduceState *producer; + const char *construction; + const char *delivery; + void *payload = NULL; + Size payload_len = 0; + bool cancelled = false; + int32 i; + + MemSet(&key, 0, sizeof(key)); + key.gp_session_id = gp_session_id; + key.gp_command_count = gp_command_count; + key.condition_id = next_condition_id++; + snprintf(key.condition_key, ANSER_CONDITION_KEY_SIZE, "anser_rf_%u", + key.condition_id); + + producer = ExecInitAnserBloomFilterProduce(&key, total_elems, (Size) cap, + 0, 1); + if (producer == NULL) + PG_RETURN_TEXT_P(cstring_to_text("no-producer:missing")); + + construction = ExecAnserBloomFilterProduceHasFilter(producer) + ? "built" : "no-filter"; + + for (i = 0; i < n_keys; i++) + { + Datum value = Int32GetDatum(i); + + ExecAnserBloomFilterProduceAddDatum(producer, value, false); + } + + (void) ExecAnserBloomFilterProducePublish(producer); + ExecEndAnserBloomFilterProduce(producer); + + if (!AnserDispatchLocalConsume(&key, ANSER_PAYLOAD_BLOOM, &payload, + &payload_len, &cancelled)) + delivery = cancelled ? "cancelled" : "missing"; + else + delivery = "delivered"; + + if (payload != NULL) + pfree(payload); + + PG_RETURN_TEXT_P(cstring_to_text(psprintf("%s:%s", construction, delivery))); +} + +/* + * --------------------------------------------------------------------------- + * The wire protocol. + * + * Not exhaustive -- a fuzzer would be the right tool for that, and pg_regress + * is not one. What these cover is the part of the format that is easy to get + * wrong and expensive to get wrong: that the header is a fixed width with the + * fields where they are documented, that a payload cannot be mistaken for + * framing (the reason the newline-delimited format was replaced), that the + * length cross-check rejects a message that does not add up, and that the + * checksum rejects a single altered byte anywhere it is supposed to cover -- + * and does not reject one where it deliberately does not. + * --------------------------------------------------------------------------- + */ + +PG_FUNCTION_INFO_V1(anser_test_wire_format); +PG_FUNCTION_INFO_V1(anser_test_wire_roundtrip); +PG_FUNCTION_INFO_V1(anser_test_push_crc); + +/* Fixed channel coordinates, so the golden headers below are stable. */ +#define ANSER_TEST_WIRE_SESSION 42 +#define ANSER_TEST_WIRE_COMMAND 7 +#define ANSER_TEST_WIRE_CONDITION 3 +#define ANSER_TEST_WIRE_PART 1 +#define ANSER_TEST_WIRE_TOTAL 3 + +static void +anser_test_wire_key(AnserChannelKey *key, const char *condition_key) +{ + MemSet(key, 0, sizeof(*key)); + key->gp_session_id = ANSER_TEST_WIRE_SESSION; + key->gp_command_count = ANSER_TEST_WIRE_COMMAND; + key->condition_id = ANSER_TEST_WIRE_CONDITION; + strlcpy(key->condition_key, condition_key, ANSER_CONDITION_KEY_SIZE); +} + +/* + * The formatted message, verbatim. + * + * Returning it as text is itself a check: a NOTIFY payload travels through + * pq_sendstring and so must be free of NUL bytes, and a text Datum cannot + * carry one -- which is why the body is base64 even though the header is not. + * Pass a body containing NULs and the length in the expected output proves it. + */ +Datum +anser_test_wire_format(PG_FUNCTION_ARGS) +{ + char kind = PG_GETARG_CHAR(0); + char payload_type = PG_GETARG_CHAR(1); + int32 flags = PG_GETARG_INT32(2); + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); + bytea *body = PG_GETARG_BYTEA_PP(4); + AnserChannelKey key; + char *msg; + + anser_test_wire_key(&key, condition_key); + msg = AnserWireFormat(&key, kind, payload_type, ANSER_TEST_WIRE_PART, + ANSER_TEST_WIRE_TOTAL, flags, + VARDATA_ANY(body), VARSIZE_ANY_EXHDR(body)); + + PG_RETURN_TEXT_P(cstring_to_text(msg)); +} + +/* + * Format a message, optionally alter one byte of it, then parse it back and + * report what the reader made of it. + * + * The offsets touched are derived from the format macros, never hardcoded, so + * this keeps working if a field is added. Outcomes mirror what the notify + * handler does with each: "malformed" is rejected by framing before anything + * is allocated, "unknown-type" has no registry entry, "checksum-mismatch" is + * well-framed but altered, and "ok" means every field and the decoded body + * came back exactly as they went in. + */ +Datum +anser_test_wire_roundtrip(PG_FUNCTION_ARGS) +{ + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(0)); + bytea *body = PG_GETARG_BYTEA_PP(1); + char *tamper = text_to_cstring(PG_GETARG_TEXT_PP(2)); + char payload_type = PG_GETARG_CHAR(3); + AnserChannelKey key; + AnserWireMsg parsed; + char *msg; + Size msg_len; + const char *raw_body = VARDATA_ANY(body); + int raw_len = VARSIZE_ANY_EXHDR(body); + char *decoded = NULL; + int decoded_len = 0; + + anser_test_wire_key(&key, condition_key); + msg = AnserWireFormat(&key, ANSER_WIRE_KIND_PART, payload_type, + ANSER_TEST_WIRE_PART, ANSER_TEST_WIRE_TOTAL, 0, + raw_len > 0 ? raw_body : NULL, (Size) raw_len); + msg_len = strlen(msg); + + /* + * One byte, or one length, altered -- see the case table in the test. An + * empty 'tamper' falls through every branch and leaves the message intact. + */ + if (strcmp(tamper, "truncate") == 0) + msg[msg_len - 1] = '\0'; + else if (strcmp(tamper, "append") == 0) + { + char *longer = palloc(msg_len + 2); + + memcpy(longer, msg, msg_len); + longer[msg_len] = 'x'; + longer[msg_len + 1] = '\0'; + msg = longer; + } + else if (strcmp(tamper, "bodylen") == 0) + { + /* Last digit of the bodylen field: it ends one space before the CRC. */ + char *digit = msg + ANSER_WIRE_CRC_OFFSET - 2; + + *digit = (*digit == '9') ? '8' : (char) (*digit + 1); + } + else if (strcmp(tamper, "tag") == 0) + msg[0] = 'x'; + else if (strcmp(tamper, "kind") == 0) + msg[sizeof(ANSER_WIRE_TAG)] = ANSER_WIRE_KIND_SUBSCRIBE; + else if (strcmp(tamper, "type") == 0) + msg[sizeof(ANSER_WIRE_TAG) + 2] = 'Z'; + else if (strcmp(tamper, "crc") == 0) + { + char *digit = msg + ANSER_WIRE_HDR_LEN - 1; + + *digit = (*digit == '0') ? '1' : '0'; + } + else if (strcmp(tamper, "key") == 0) + { + char *first = msg + ANSER_WIRE_HDR_LEN; + + *first = (*first == 'a') ? 'b' : 'a'; + } + else if (strcmp(tamper, "body") == 0) + { + /* Stay inside the base64 alphabet so this tests the CRC, not decoding. */ + char *first = msg + ANSER_WIRE_HDR_LEN + strlen(condition_key); + + *first = (*first == 'A') ? 'B' : 'A'; + } + else if (strcmp(tamper, "") != 0) + elog(ERROR, "anser_test_wire_roundtrip: unknown tamper \"%s\"", tamper); + + if (!AnserWireParse(msg, &parsed)) + PG_RETURN_TEXT_P(cstring_to_text("malformed")); + + if (AnserPayloadLookup(parsed.payload_type) == NULL) + PG_RETURN_TEXT_P(cstring_to_text("unknown-type")); + + if (parsed.body_len > 0) + { + int maxlen = pg_b64_dec_len(parsed.body_len); + + decoded = palloc(maxlen); + decoded_len = pg_b64_decode(parsed.body, parsed.body_len, decoded, + maxlen); + if (decoded_len < 0) + PG_RETURN_TEXT_P(cstring_to_text("undecodable")); + } + + if (!AnserWireCheckCrc(&parsed, decoded, (Size) decoded_len)) + PG_RETURN_TEXT_P(cstring_to_text("checksum-mismatch")); + + /* Framed and vouched for: now every field must have survived the trip. */ + if (parsed.kind != ANSER_WIRE_KIND_PART) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: kind")); + if (parsed.payload_type != payload_type) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: payload_type")); + if (parsed.key.gp_session_id != ANSER_TEST_WIRE_SESSION || + parsed.key.gp_command_count != ANSER_TEST_WIRE_COMMAND || + parsed.key.condition_id != ANSER_TEST_WIRE_CONDITION) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: channel")); + if (parsed.part_index != ANSER_TEST_WIRE_PART || + parsed.total_parts != ANSER_TEST_WIRE_TOTAL) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: part")); + if (parsed.flags != 0) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: flags")); + if (strcmp(parsed.key.condition_key, condition_key) != 0) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: condition_key")); + if (decoded_len != raw_len || + (raw_len > 0 && memcmp(decoded, raw_body, raw_len) != 0)) + PG_RETURN_TEXT_P(cstring_to_text("mismatch: body")); + + PG_RETURN_TEXT_P(cstring_to_text("ok")); +} + +/* + * The QD -> QE checksum, as an integer so the test can compare two of them. + * + * What matters is not the value but which inputs change it: every routing field + * and the key always, and the body only for a payload type that asks for its + * body to be covered. + */ +Datum +anser_test_push_crc(PG_FUNCTION_ARGS) +{ + char payload_type = PG_GETARG_CHAR(0); + int32 condition_id = PG_GETARG_INT32(1); + int32 flags = PG_GETARG_INT32(2); + char *condition_key = text_to_cstring(PG_GETARG_TEXT_PP(3)); + bytea *body = PG_GETARG_BYTEA_PP(4); + + PG_RETURN_INT64((int64) (uint32) + AnserWirePushCrc(payload_type, (uint32) condition_id, + (uint32) flags, condition_key, + (int) strlen(condition_key), + VARDATA_ANY(body), + (int) VARSIZE_ANY_EXHDR(body))); +} diff --git a/gpcontrib/anser/src/anserbloomconsume.c b/gpcontrib/anser/src/anserbloomconsume.c new file mode 100644 index 00000000000..2da6dd5c01a --- /dev/null +++ b/gpcontrib/anser/src/anserbloomconsume.c @@ -0,0 +1,183 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserbloomconsume.c + * Standalone Anser Bloom filter consumer executor helper. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserbloomconsume.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserbloom.h" +#include "anserfilter.h" +#include "anserpayload.h" +#include "ansersideband.h" +#include "cdb/cdbvars.h" + +/* + * State for one Bloom filter consumer. Consumes the merged payload for a + * channel exactly once (consumed), either from the coordinator's shared + * coordinator. cancelled records that the producer side aborted instead of + * delivering the payload. + */ +struct AnserBloomFilterConsumeState +{ + AnserChannelKey channel_key; + bloom_filter *filter; + 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 ExecAnserBloomFilterConsumeSideband(AnserBloomFilterConsumeState *state, + long timeout_ms); +static bool ExecAnserBloomFilterConsumeFinish(AnserBloomFilterConsumeState *state, + void *payload, Size payload_len); + +AnserBloomFilterConsumeState * +ExecInitAnserBloomFilterConsume(const AnserChannelKey *channel_key, + int64 total_elems, Size max_payload_bytes, + uint32 expected_parts) +{ + 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; + return state; +} + +bool +ExecAnserBloomFilterConsume(AnserBloomFilterConsumeState *state, + long registration_timeout_ms) +{ + if (state == NULL) + return false; + + if (state->consumed) + return state->filter != NULL; + + return ExecAnserBloomFilterConsumeSideband(state, registration_timeout_ms); +} + +/* + * Dispatch-connection consume path. + * + * On a segment this blocks on our own dispatch socket until the coordinator + * pushes the merged filter; on the coordinator the merge happened in this very + * process, so there is nothing to wait for and we just read it. + */ +static bool +ExecAnserBloomFilterConsumeSideband(AnserBloomFilterConsumeState *state, + long timeout_ms) +{ + void *payload = NULL; + Size payload_len = 0; + bool cancelled = false; + bool got; + + if (Gp_role == GP_ROLE_EXECUTE) + got = AnserSidebandConsumeWait(&state->channel_key, ANSER_PAYLOAD_BLOOM, + &payload, &payload_len, &cancelled, + timeout_ms); + else + got = AnserDispatchLocalConsume(&state->channel_key, ANSER_PAYLOAD_BLOOM, + &payload, &payload_len, &cancelled); + + if (!got || cancelled) + { + if (payload != NULL) + pfree(payload); + state->cancelled = cancelled; + state->consumed = true; + return false; + } + + return ExecAnserBloomFilterConsumeFinish(state, payload, payload_len); +} + +/* + * Turn a merged payload into this consumer's filter. + * + * The coordinator unions every segment's part into one before delivery, so a + * single chunk is deserialized rather than N; the merged header's total_parts + * records how many were folded, which we surface as the received count. + */ +static bool +ExecAnserBloomFilterConsumeFinish(AnserBloomFilterConsumeState *state, + void *payload, Size payload_len) +{ + uint32 part_index = 0; + uint32 folded = 0; + + state->filter = AnserBloomDeserializePart(payload, payload_len, + state->total_elems, + state->max_payload_bytes, + state->seed, + &part_index, &folded); + state->received_parts = (state->filter != NULL) ? folded : 0; + + if (payload != NULL) + pfree(payload); + state->consumed = true; + return state->filter != NULL; +} + +bloom_filter * +ExecAnserBloomFilterConsumerGetFilter(AnserBloomFilterConsumeState *state) +{ + return state != NULL ? state->filter : NULL; +} + +uint32 +ExecAnserBloomFilterConsumerReceivedParts(AnserBloomFilterConsumeState *state) +{ + return state != NULL ? state->received_parts : 0; +} + +bool +ExecAnserBloomFilterConsumerWasCancelled(AnserBloomFilterConsumeState *state) +{ + return state != NULL && state->cancelled; +} + +void +ExecEndAnserBloomFilterConsume(AnserBloomFilterConsumeState *state) +{ + if (state == NULL) + return; + + if (state->filter != NULL) + bloom_free(state->filter); + pfree(state); +} diff --git a/gpcontrib/anser/src/anserbloomproduce.c b/gpcontrib/anser/src/anserbloomproduce.c new file mode 100644 index 00000000000..1bf5d584439 --- /dev/null +++ b/gpcontrib/anser/src/anserbloomproduce.c @@ -0,0 +1,219 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserbloomproduce.c + * Standalone Anser Bloom filter producer executor helper. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserbloomproduce.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserbloom.h" +#include "anserfilter.h" +#include "anserpayload.h" +#include "ansersideband.h" +#include "cdb/cdbvars.h" + +/* + * State for a single bloom filter producer: the target channel, the filter + * being built, and this producer's identity within total_parts. published + * and cancelled guard against double publication and drive teardown. + */ +struct AnserBloomFilterProduceState +{ + AnserChannelKey channel_key; + bloom_filter *filter; + uint32 part_index; + uint32 total_parts; + bool published; + bool cancelled; +}; + +/* + * Publish one part. + * + * A segment sends it to the coordinator over the dispatch connection; a + * coordinator-local producer hands it to the same per-query channel table the + * coordinator merges into, since it has no connection to itself. total_parts + * doubles as expected_producers: each producer contributes exactly one part. + */ +static bool +AnserProducePublishPart(AnserBloomFilterProduceState *state, + const void *payload, Size payload_len, bool cancelled) +{ + if (Gp_role == GP_ROLE_EXECUTE) + return AnserSidebandPublish(&state->channel_key, ANSER_PAYLOAD_BLOOM, + state->part_index, state->total_parts, + payload, payload_len, cancelled); + + return AnserDispatchLocalPublish(&state->channel_key, ANSER_PAYLOAD_BLOOM, + state->part_index, state->total_parts, + payload, payload_len, cancelled); +} + +AnserBloomFilterProduceState * +ExecInitAnserBloomFilterProduce(const AnserChannelKey *channel_key, + int64 total_elems, + Size max_payload_bytes, + uint32 part_index, + uint32 total_parts) +{ + 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; + 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) +{ + /* + * A NULL filter means AnserBloomCreate declined to build one -- too many + * keys for the payload cap. There is nothing to add to, and the node has + * already published the cancel. + */ + if (state == NULL || state->filter == NULL || state->published || isnull) + return; + + bloom_add_element(state->filter, (unsigned char *) &value, sizeof(Datum)); +} + +/* Does this producer have a filter to fill? False means "already hopeless". */ +bool +ExecAnserBloomFilterProduceHasFilter(AnserBloomFilterProduceState *state) +{ + return state != NULL && state->filter != NULL; +} + +bool +ExecAnserBloomFilterProducePublish(AnserBloomFilterProduceState *state) +{ + Size payload_size; + Size payload_len = 0; + void *payload; + bool ok; + + if (state == NULL || state->published) + { + ANSER_DEBUG("anser: publish skipped (%s)", + state == NULL ? "no producer state" : "already published"); + return false; + } + + if (state->cancelled || state->filter == NULL) + { + ANSER_DEBUG("anser: publishing a cancel (%s)", + state->cancelled ? "producer cancelled" : "no filter"); + state->published = true; + return AnserProducePublishPart(state, NULL, 0, true); + } + + /* + * Too saturated to be worth anything? Cancel rather than send. + * + * This is the check the planner cannot make: it decided the filter was + * worth building from a row estimate, and estimates are wrong. A filter + * whose bits are nearly all set matches nearly every probe row, so shipping + * it would buy the consumers a hash and k probes per row in exchange for + * almost no rows eliminated -- pure overhead on both sides. + */ + if (bloom_false_positive_rate(state->filter) > ANSER_BLOOM_MAX_FPR) + { + ANSER_DEBUG("anser: publishing a cancel: filter is %.1f%% full, est. FPR %.1f%% above the %.0f%% limit", + bloom_prop_bits_set(state->filter) * 100.0, + bloom_false_positive_rate(state->filter) * 100.0, + ANSER_BLOOM_MAX_FPR * 100.0); + state->published = true; + return AnserProducePublishPart(state, NULL, 0, true); + } + + ANSER_DEBUG("anser: filter is %.1f%% full, est. FPR %.2f%%", + bloom_prop_bits_set(state->filter) * 100.0, + bloom_false_positive_rate(state->filter) * 100.0); + + /* + * Serialize as a self-contained single part (index 0 of 1). The coordinator + * stores the first part verbatim and OR-folds each later part, bumping the + * 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); + else + ANSER_DEBUG("anser: publish skipped: could not serialize part (size=%zu)", + payload_size); + + 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); +} + +/* + * Free the producer state. + * + * Deliberately does NOT publish a cancel for an unpublished producer: whether + * silence means "ran and was abandoned" or "never ran at all" is knowable only + * to the node, and the two need opposite handling (see anser_produce_end in + * anserplanexec.c). + */ +void +ExecEndAnserBloomFilterProduce(AnserBloomFilterProduceState *state) +{ + if (state == NULL) + return; + + if (state->filter != NULL) + bloom_free(state->filter); + pfree(state); +} diff --git a/gpcontrib/anser/src/anserdispatch.c b/gpcontrib/anser/src/anserdispatch.c new file mode 100644 index 00000000000..a48e3a0d1f7 --- /dev/null +++ b/gpcontrib/anser/src/anserdispatch.c @@ -0,0 +1,713 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserdispatch.c + * Coordinator side of the dispatch-connection transport. + * + * Everything here runs in the QD backend, reached from cdbdisp_notify_hook + * while the dispatcher drains QE messages -- which happens from inside the + * interconnect wait loop, so this code is on the query's critical path. It + * must stay cheap and must not throw for anything recoverable: an error here + * lands in a running query, whereas losing a filter only costs us an + * unfiltered scan. + * + * Because producer merge and consumer delivery both happen in this one + * process, the accumulator is an ordinary palloc'd buffer. There is no shared + * memory, no DSM segment to attach, and no separate worker to hand data to; + * parts are folded in place as they arrive, so only the final fold is on the + * critical path. + * + * Nothing here knows what a part contains. Merging is delegated to the payload + * type's fold() (anserpayload.h), and the only other thing this file asks about + * a type is whether its body is covered by the message checksum -- which is why + * it does not include anserfilter.h at all. + * + * A note on libpq linkage. The connections we write to were created by the + * copy of libpq that is statically linked into the postgres binary, and every + * symbol listed in libpq's exports.txt is deliberately made *local* in that + * binary (see the version-script hack in src/backend/Makefile) precisely so a + * module cannot end up driving one connection through two copies of libpq. + * So this file must not call the public PQ* API: linking libpq.so to obtain it + * would create exactly the mixture that hack exists to prevent. The internal + * pq* writers are not in exports.txt and so remain global in the backend, + * which is how pqPutMsgStart() and friends resolve here; the two accessors we + * would otherwise want are inlined below, straight off the struct. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserdispatch.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq-fe.h" +#include "libpq-int.h" + +#include "anser.h" +#include "anserpayload.h" +#include "ansersideband.h" +#include "cdb/cdbconn.h" +#include "cdb/cdbdisp.h" +#include "cdb/cdbdispatchresult.h" +#include "cdb/cdbvars.h" +#include "common/base64.h" +#include "lib/stringinfo.h" +#include "nodes/pg_list.h" +#include "utils/hsearch.h" +#include "utils/memutils.h" + +/* + * One channel's merge state, living for the duration of the query that created + * it. Keyed by AnserChannelKey, which must be the first field. + */ +typedef struct AnserDispChannel +{ + AnserChannelKey key; + const AnserPayloadOps *ops; /* what this carries; NULL until a part says */ + char *payload; /* merged part, or NULL before the first one */ + Size payload_len; + int parts_received; + int expected_parts; /* 0 until a part tells us; from total_parts */ + bool cancelled; + bool complete; /* every expected part folded, or cancelled */ + List *subscribers; /* PGconn * of QEs awaiting delivery */ +} AnserDispChannel; + +static HTAB *AnserDispChannels = NULL; +static MemoryContext AnserDispContext = NULL; + +static AnserDispChannel *anser_disp_lookup(const AnserChannelKey *key, bool create); +static void anser_disp_apply_part(AnserDispChannel *chan, + const AnserPayloadOps *ops, + const void *payload, Size payload_len, + int total_parts, bool cancelled); +static void anser_disp_deliver(AnserDispChannel *chan); +static bool anser_disp_push(PGconn *conn, AnserDispChannel *chan); + +/* + * PQstatus() / PQerrorMessage() equivalents. See the linkage note above for + * why these are not the real thing; libpq-int.h gives us the full struct. + */ +static inline bool +anser_conn_ok(const PGconn *conn) +{ + return conn != NULL && conn->status == CONNECTION_OK; +} + +static inline const char * +anser_conn_error(const PGconn *conn) +{ + if (conn == NULL) + return "connection pointer is NULL"; + if (PQExpBufferBroken(&conn->errorMessage)) + return "out of memory"; + return conn->errorMessage.data; +} + +/* + * Per-query state lives in its own context so it can be dropped wholesale. + * Channels are keyed by (session, command, condition), so entries from an + * earlier command in the same transaction are distinct and simply unused + * until the reset. + */ +static void +anser_disp_init(void) +{ + HASHCTL hctl; + + if (AnserDispChannels != NULL) + return; + + AnserDispContext = AllocSetContextCreate(TopMemoryContext, + "Anser dispatch transport", + ALLOCSET_DEFAULT_SIZES); + + MemSet(&hctl, 0, sizeof(hctl)); + hctl.keysize = sizeof(AnserChannelKey); + hctl.entrysize = sizeof(AnserDispChannel); + hctl.hcxt = AnserDispContext; + + AnserDispChannels = hash_create("Anser dispatch channels", 32, &hctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +void +AnserDispatchReset(void) +{ + if (AnserDispChannels == NULL) + return; + + hash_destroy(AnserDispChannels); + AnserDispChannels = NULL; + MemoryContextDelete(AnserDispContext); + AnserDispContext = NULL; +} + +static AnserDispChannel * +anser_disp_lookup(const AnserChannelKey *key, bool create) +{ + AnserDispChannel *chan; + bool found; + + anser_disp_init(); + + chan = (AnserDispChannel *) hash_search(AnserDispChannels, key, + create ? HASH_ENTER : HASH_FIND, + &found); + if (chan == NULL) + return NULL; + + if (create && !found) + { + /* hash_search only fills the key; initialize the rest. */ + chan->ops = NULL; + chan->payload = NULL; + chan->payload_len = 0; + chan->parts_received = 0; + chan->expected_parts = 0; + chan->cancelled = false; + chan->complete = false; + chan->subscribers = NIL; + } + + return chan; +} + +/* + * cdbdisp_notify_hook: is this one of ours, and if so, handle it. + * + * Returns false for any notify on another channel so the dispatcher can carry + * on with its own handling. A malformed payload is consumed (it is addressed + * to us) but otherwise ignored: the affected consumers will time out and run + * unfiltered. + */ +bool +AnserDispatchNotifyHandler(struct CdbDispatchResult *dispatchResult, + struct pgNotify *notify) +{ + PGnotify *n = (PGnotify *) notify; + CdbDispatchResult *dr = (CdbDispatchResult *) dispatchResult; + int sender = (dr != NULL && dr->segdbDesc != NULL) + ? dr->segdbDesc->segindex : -99; + AnserWireMsg msg; + const AnserPayloadOps *ops; + AnserDispChannel *chan; + MemoryContext oldcxt; + + if (n == NULL || n->relname == NULL || + strcmp(n->relname, ANSER_NOTIFY_CHANNEL) != 0) + return false; + + if (n->extra == NULL || !AnserWireParse(n->extra, &msg)) + { + elog(LOG, "anser: ignoring malformed message from a segment (len=%zu)", + n->extra != NULL ? strlen(n->extra) : (size_t) 0); + return true; + } + + /* + * Resolve what the message carries before doing anything with it. An + * unregistered type means a producer was taught to send something this + * coordinator does not know how to merge -- almost certainly a payload type + * added to anserpayload.h but not to AnserPayloadTable. + */ + ops = AnserPayloadLookup(msg.payload_type); + if (ops == NULL) + { + elog(LOG, "anser: unknown payload type '%c' from seg%d (cond=%u); ignoring", + msg.payload_type, sender, msg.key.condition_id); + return true; + } + + /* + * A message with no body can be checked here, before anything is + * allocated; one with a body is checked in the PART branch below, once it + * has been decoded. + */ + if (msg.body_len == 0 && !AnserWireCheckCrc(&msg, NULL, 0)) + { + elog(LOG, "anser: ignoring corrupted message from seg%d (cond=%u kind=%c)", + sender, msg.key.condition_id, msg.kind); + return true; + } + + anser_disp_init(); + oldcxt = MemoryContextSwitchTo(AnserDispContext); + + chan = anser_disp_lookup(&msg.key, true); + if (chan == NULL) + { + MemoryContextSwitchTo(oldcxt); + return true; + } + + if (msg.kind == ANSER_WIRE_KIND_SUBSCRIBE) + { + PGconn *conn = dr->segdbDesc->conn; + + /* + * A consumer can subscribe after the channel is already complete -- + * producers on other segments may well have finished first -- so + * deliver immediately in that case rather than recording interest. + */ + ANSER_DEBUG("anser: QD subscribe cond=%u from seg%d (channel %s)", + msg.key.condition_id, sender, + chan->complete ? "complete, delivering now" : "still collecting"); + if (chan->complete) + (void) anser_disp_push(conn, chan); + else + chan->subscribers = lappend(chan->subscribers, conn); + } + else if (msg.kind == ANSER_WIRE_KIND_PART) + { + char *raw = NULL; + int raw_len = 0; + + if (!(msg.flags & ANSER_WIRE_F_CANCELLED) && msg.body_len > 0) + { + int maxlen = pg_b64_dec_len(msg.body_len); + + raw = palloc(maxlen); + raw_len = pg_b64_decode(msg.body, msg.body_len, raw, maxlen); + + /* + * Undecodable, oversized or corrupted: cancel the channel rather + * than guess. Consumers then run unfiltered, which is slower but + * correct -- folding in a part we cannot vouch for risks clearing a + * bit that should be set, and a filter missing a key silently drops + * joinable rows. + */ + if (raw_len < 0 || raw_len > gp_anser_max_info_size || + !AnserWireCheckCrc(&msg, raw, (Size) raw_len)) + { + elog(LOG, "anser: unusable part for condition %u from seg%d; cancelling channel", + msg.key.condition_id, sender); + pfree(raw); + raw = NULL; + raw_len = 0; + msg.flags |= ANSER_WIRE_F_CANCELLED; + } + } + + anser_disp_apply_part(chan, ops, raw, (Size) raw_len, msg.total_parts, + (msg.flags & ANSER_WIRE_F_CANCELLED) != 0); + ANSER_DEBUG("anser: QD %s part cond=%u from seg%d (says part %d of %d) %d/%d bytes=%d -> %s", + ops->name, msg.key.condition_id, sender, msg.part_index, + msg.total_parts, chan->parts_received, + chan->expected_parts, raw_len, + chan->cancelled ? "cancelled" : + chan->complete ? "complete" : "collecting"); + if (raw != NULL) + pfree(raw); + } + + if (chan->complete) + anser_disp_deliver(chan); + + MemoryContextSwitchTo(oldcxt); + return true; +} + +/* + * Fold one part into the channel's accumulator. + * + * The first part is kept verbatim and becomes the accumulator; later parts are + * merged into it in place by the payload type's fold(), so no part is ever + * copied twice and the accumulator is never reallocated. Nothing here knows + * what the bytes are -- for a bloom filter the fold is a bitwise OR, for a row + * count it would be a sum, and this function reads the same either way. + */ +static void +anser_disp_apply_part(AnserDispChannel *chan, const AnserPayloadOps *ops, + const void *payload, Size payload_len, int total_parts, + bool cancelled) +{ + if (chan->cancelled) + return; /* already dead; nothing to do */ + + if (chan->ops == NULL) + chan->ops = ops; + else if (chan->ops != ops) + { + /* + * Two producers disagree about what this channel carries. Merging + * across types is meaningless, so give up on the channel: consumers run + * unfiltered, which is always correct. + */ + elog(LOG, "anser: cond=%u carries both '%s' and '%s'; cancelling channel", + chan->key.condition_id, chan->ops->name, ops->name); + chan->cancelled = true; + chan->complete = true; + chan->payload = NULL; + chan->payload_len = 0; + return; + } + + if (total_parts > chan->expected_parts) + { + /* + * Take the largest count any producer claims. Letting a later part + * lower it would complete the channel early and deliver a filter + * missing another segment's keys -- a false negative, which drops + * joinable rows. Disagreement means the producers computed their + * slice width differently and is worth seeing. + */ + if (chan->expected_parts != 0) + elog(LOG, "anser: cond=%u producer count changed %d -> %d", + chan->key.condition_id, chan->expected_parts, total_parts); + chan->expected_parts = total_parts; + } + + if (cancelled) + { + chan->cancelled = true; + chan->complete = true; + chan->payload = NULL; + chan->payload_len = 0; + return; + } + + if (payload == NULL || payload_len == 0) + { + /* An empty part still counts toward completion. */ + chan->parts_received++; + } + else if (chan->payload == NULL) + { + chan->payload = palloc(payload_len); + memcpy(chan->payload, payload, payload_len); + chan->payload_len = payload_len; + chan->parts_received++; + } + else if (ops->fold != NULL && + ops->fold(chan->payload, chan->payload_len, payload, payload_len)) + { + chan->parts_received++; + } + else + { + /* + * The parts cannot be merged -- for a bloom filter, sizes or filter + * parameters disagree. That should not happen (every part on a channel + * is built from the same plan parameters), but if it does the only safe + * answer is to give up on the channel. A type with no fold() reaches + * here too, which is right: it should never have carried a body. + */ + elog(LOG, "anser: incompatible '%s' part for condition %u; cancelling channel", + ops->name, chan->key.condition_id); + chan->cancelled = true; + chan->complete = true; + chan->payload = NULL; + chan->payload_len = 0; + return; + } + + if (chan->expected_parts > 0 && chan->parts_received >= chan->expected_parts) + { + chan->complete = true; + + /* + * Every part is in, so this is the first and last chance to judge the + * merged result. A type that declines it here saves each consumer both + * the delivery and the per-row probing it would have paid for. + */ + if (chan->payload != NULL && ops->worth_delivering != NULL && + !ops->worth_delivering(chan->payload, chan->payload_len)) + { + elog(LOG, "anser: merged '%s' payload for condition %u is not worth delivering; cancelling channel", + ops->name, chan->key.condition_id); + chan->cancelled = true; + chan->payload = NULL; + chan->payload_len = 0; + } + } +} + +/* Push the finished channel to everyone waiting, then forget them. */ +static void +anser_disp_deliver(AnserDispChannel *chan) +{ + ListCell *lc; + + ANSER_DEBUG("anser: QD delivering cond=%u to %d subscriber(s)", + chan->key.condition_id, list_length(chan->subscribers)); + foreach(lc, chan->subscribers) + (void) anser_disp_push((PGconn *) lfirst(lc), chan); + + list_free(chan->subscribers); + chan->subscribers = NIL; +} + +/* + * Write one merged payload to a QE as a GP_SIDEBAND_MESSAGE. + * + * Delivery is per consumer: a write that fails costs that one segment its + * filter (it will time out and run unfiltered) and leaves the others alone. + * This is the same "try to reach every consumer" rule the shared-memory send + * service followed. + */ +static bool +anser_disp_push(PGconn *conn, AnserDispChannel *chan) +{ + int flags = chan->cancelled ? ANSER_WIRE_F_CANCELLED : 0; + int keylen = (int) strlen(chan->key.condition_key); + int paylen = chan->cancelled ? 0 : (int) chan->payload_len; + pg_crc32c crc; + char paytype; + + if (!anser_conn_ok(conn)) + return false; + + /* + * A channel that was cancelled before any part arrived has no type. That + * is fine: the delivery carries no body, and a consumer only checks the + * type of a body it actually received. + */ + paytype = chan->ops != NULL ? chan->ops->code : ANSER_PAYLOAD_NONE; + + crc = AnserWirePushCrc(paytype, chan->key.condition_id, (uint32) flags, + chan->key.condition_key, keylen, + chan->payload, paylen); + + /* + * Raw binary: pqPutnchar performs no encoding conversion, so unlike the + * QE -> QD direction this needs no base64. + */ + if (pqPutMsgStart(GP_SIDEBAND_MESSAGE, conn) < 0 || + pqPutInt((int) paytype, 4, conn) < 0 || + pqPutInt((int) crc, 4, conn) < 0 || + pqPutInt((int) chan->key.condition_id, 4, conn) < 0 || + pqPutInt(flags, 4, conn) < 0 || + pqPutInt(keylen, 4, conn) < 0 || + pqPutnchar(chan->key.condition_key, keylen, conn) < 0 || + pqPutInt(paylen, 4, conn) < 0 || + (paylen > 0 && pqPutnchar(chan->payload, paylen, conn) < 0) || + pqPutMsgEnd(conn) < 0 || + pqFlush(conn) < 0) + { + elog(LOG, "anser: could not deliver filter for condition %u: %s", + chan->key.condition_id, anser_conn_error(conn)); + return false; + } + + ANSER_DEBUG("anser: QD pushed cond=%u type=%c bytes=%d cancelled=%d", + chan->key.condition_id, paytype, paylen, + chan->cancelled ? 1 : 0); + return true; +} + +/* + * Parse a QE -> QD payload: fixed-width header, then the condition key, then + * the base64 body. ansersideband.h documents the layout and the reasoning; the + * checks here are what make it trustworthy: + * + * - the tag must match, so a message from a different format is not + * misinterpreted as this one; + * - the header is a constant length, so the key and the body start at offsets + * that no payload byte can influence; + * - the two lengths must account for the message exactly, which catches a + * truncated or over-long message before any of it is used; + * - the CRC is verified once the body has been decoded, in the caller. + * + * The payload type is carried through as the raw byte; resolving it against the + * registry is the caller's job, so that an unregistered type is reported as + * exactly that rather than as a framing error. + */ +bool +AnserWireParse(const char *msg, AnserWireMsg *out) +{ + Size msglen = strlen(msg); + char kind; + char paytype; + uint32 ssid, + ccnt, + condid, + part, + total, + flags, + keylen, + bodylen, + crc; + + if (msglen < ANSER_WIRE_HDR_LEN) + return false; + if (strncmp(msg, ANSER_WIRE_TAG " ", sizeof(ANSER_WIRE_TAG)) != 0) + return false; + + if (sscanf(msg, ANSER_WIRE_HDR_SCANF, + &kind, &paytype, &ssid, &ccnt, &condid, &part, &total, &flags, + &keylen, &bodylen, &crc) != 11) + return false; + + if (kind != ANSER_WIRE_KIND_PART && kind != ANSER_WIRE_KIND_SUBSCRIBE) + return false; + if (keylen >= ANSER_CONDITION_KEY_SIZE) + return false; + if (msglen != ANSER_WIRE_HDR_LEN + (Size) keylen + (Size) bodylen) + return false; + + MemSet(out, 0, sizeof(*out)); + out->wire = msg; + out->kind = kind; + out->payload_type = paytype; + out->key.gp_session_id = (int) ssid; + out->key.gp_command_count = (int) ccnt; + out->key.condition_id = condid; + memcpy(out->key.condition_key, msg + ANSER_WIRE_HDR_LEN, keylen); + out->key.condition_key[keylen] = '\0'; + out->key_len = (int) keylen; + out->part_index = (int) part; + out->total_parts = (int) total; + out->flags = (int) flags; + out->body = msg + ANSER_WIRE_HDR_LEN + keylen; + out->body_len = (int) bodylen; + out->crc = crc; + + return true; +} + +/* + * Verify a parsed message against its checksum. + * + * 'body' is the decoded body, which is what the producer checksummed -- so + * where it is covered this validates the base64 round trip as well. Pass + * NULL/0 for a message that has no body, and note that a type which opts out of + * checksumming its body still has its header and key checked. + */ +bool +AnserWireCheckCrc(const AnserWireMsg *msg, const void *body, Size body_len) +{ + pg_crc32c crc; + + INIT_CRC32C(crc); + COMP_CRC32C(crc, msg->wire, ANSER_WIRE_CRC_OFFSET); + COMP_CRC32C(crc, msg->key.condition_key, msg->key_len); + if (body != NULL && body_len > 0 && + AnserPayloadChecksumsBody(msg->payload_type)) + COMP_CRC32C(crc, body, body_len); + FIN_CRC32C(crc); + + return (uint32) crc == msg->crc; +} + +/* + * Coordinator-local producer. + * + * A producer running on the QD has no dispatch connection to itself, so it + * folds straight into the same channel table the hook uses. + */ +bool +AnserDispatchLocalPublish(const AnserChannelKey *channel_key, char payload_type, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, + bool cancelled) +{ + const AnserPayloadOps *ops; + AnserDispChannel *chan; + MemoryContext oldcxt; + + if (channel_key == NULL) + return false; + if (!cancelled && payload_len > (Size) gp_anser_max_info_size) + cancelled = true; + + ops = AnserPayloadLookup(payload_type); + if (ops == NULL) + { + elog(LOG, "anser: unknown payload type '%c' for condition %u; ignoring", + payload_type, channel_key->condition_id); + return false; + } + + anser_disp_init(); + oldcxt = MemoryContextSwitchTo(AnserDispContext); + + chan = anser_disp_lookup(channel_key, true); + if (chan != NULL) + { + anser_disp_apply_part(chan, ops, payload, payload_len, + (int) total_parts, cancelled); + ANSER_DEBUG("anser: QD local %s part cond=%u (part %u of %u) %d/%d bytes=%zu -> %s", + ops->name, channel_key->condition_id, part_index, total_parts, + chan->parts_received, chan->expected_parts, payload_len, + chan->cancelled ? "cancelled" : + chan->complete ? "complete" : "collecting"); + if (chan->complete) + anser_disp_deliver(chan); + } + + MemoryContextSwitchTo(oldcxt); + return chan != NULL; +} + +/* + * Coordinator-local consumer. + * + * Does not wait: on the coordinator the producer side of the join has already + * run by the time the probe side asks for the filter, so either the channel is + * complete or it never will be (a squelched producer, say) and we fail open. + */ +bool +AnserDispatchLocalConsume(const AnserChannelKey *channel_key, char payload_type, + void **payload, Size *payload_len, bool *cancelled) +{ + AnserDispChannel *chan; + + if (payload != NULL) + *payload = NULL; + if (payload_len != NULL) + *payload_len = 0; + if (cancelled != NULL) + *cancelled = false; + + if (channel_key == NULL || AnserDispChannels == NULL) + return false; + + chan = anser_disp_lookup(channel_key, false); + if (chan == NULL || !chan->complete) + return false; + + if (chan->cancelled || chan->payload == NULL) + { + if (cancelled != NULL) + *cancelled = true; + return false; + } + + /* Same check the segment path makes on delivery; see anser_inbox_take. */ + if (chan->ops == NULL || chan->ops->code != payload_type) + { + elog(WARNING, "anser: condition %u holds payload type '%c', expected '%c'", + channel_key->condition_id, + chan->ops != NULL ? chan->ops->code : '?', payload_type); + if (cancelled != NULL) + *cancelled = true; + return false; + } + + if (payload != NULL) + { + *payload = palloc(chan->payload_len); + memcpy(*payload, chan->payload, chan->payload_len); + } + if (payload_len != NULL) + *payload_len = chan->payload_len; + + return true; +} diff --git a/gpcontrib/anser/src/anserfilter.c b/gpcontrib/anser/src/anserfilter.c new file mode 100644 index 00000000000..88933861748 --- /dev/null +++ b/gpcontrib/anser/src/anserfilter.c @@ -0,0 +1,376 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserfilter.c + * Bloom-filter payload helpers for Anser channels. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserfilter.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserfilter.h" +#include "common/hashfn.h" +#include "port/pg_bitutils.h" + +static bool AnserBloomValidateHeader(const AnserBloomPartHeader *header, + Size payload_len); + +/* + * Map Anser's payload budget to bloom_create's work_mem (KB): the space left for + * the bitset after the part header. Callers must have checked that + * max_payload_bytes leaves room for a header. + */ +static int +AnserBloomWorkMemKb(Size max_payload_bytes) +{ + return (int) ((max_payload_bytes - sizeof(AnserBloomPartHeader)) / 1024); +} + +uint64 +AnserBloomSeed(const char *condition_key) +{ + if (condition_key == NULL) + return 0; + + return hash_bytes_extended((const unsigned char *) condition_key, + strlen(condition_key), 0); +} + +/* + * Build an empty bloom filter for a channel from the caller's parameters. + * + * The producer builds its filter here; the consumer rebuilds an identical one + * from the received bitset (AnserBloomDeserializePart -> bloom_create_from_bitset) + * using the SAME (total_elems, max_payload_bytes, seed) -- carried in the plan + * node's custom_private and derived from the shared condition key -- so every + * segment and the consumer realize a byte-for-byte identical filter shape. This + * is why the serialized part header does not need to carry the bitset parameters: + * the reconstructing side already knows them. + * + * Sizing is bloom_create's, but the decision to keep the result is ours. Two + * ways it can come back unusable: + * + * - Too large to send. bloom_create ends with Max(1 MB, bitset), a floor + * that overrides the work_mem cap, so a payload cap below 1 MB yields a + * bitset that cannot be shipped. + * - Too thin to help. bloom_create has no minimum density (unlike + * bloom_create_aggresive, which refuses below 1.6 bits/key), so a large + * enough total_elems against a fixed cap gets you a filter that matches + * almost everything. + * + * Both are judged from the filter's own accessors rather than by re-deriving + * bloom_create's arithmetic here: my_bloom_power() and the floor are private to + * lib/bloomfilter.c, and a copy of them would go quietly out of date on a + * kernel rebase. The cost of learning the answer this way is one palloc0 that + * is immediately freed -- against a build-side scan, which is what returning + * NULL here avoids, that is nothing. + */ +bloom_filter * +AnserBloomCreate(int64 total_elems, Size max_payload_bytes, uint64 seed) +{ + bloom_filter *filter; + Size serialized; + double bits_per_key; + + /* + * These were an Assert on the grounds that internal callers always pass a + * payload cap with room for a header. They are checks because the caller + * is a plan node, and its parameters arrived from the coordinator in + * custom_private -- not somewhere an assertion belongs. The cap also + * cannot merely be >= the header: AnserBloomWorkMemKb subtracts the header + * from a Size, so a smaller cap would underflow to an enormous work_mem. + */ + if (total_elems <= 0 || max_payload_bytes <= sizeof(AnserBloomPartHeader)) + return NULL; + + filter = bloom_create(total_elems, AnserBloomWorkMemKb(max_payload_bytes), + seed); + serialized = AnserBloomSerializedSize(filter); + bits_per_key = (double) bloom_total_bits(filter) / (double) total_elems; + + if (serialized > max_payload_bytes) + { + ANSER_DEBUG("anser: not building a filter: smallest bitset serializes to %zu bytes, cap is %zu", + serialized, max_payload_bytes); + bloom_free(filter); + return NULL; + } + + if (bits_per_key < ANSER_BLOOM_MIN_BITS_PER_KEY) + { + ANSER_DEBUG("anser: not building a filter for %ld key(s) in %zu bytes: %.2f bits/key, below the %.1f floor", + (long) total_elems, max_payload_bytes, bits_per_key, + ANSER_BLOOM_MIN_BITS_PER_KEY); + bloom_free(filter); + return NULL; + } + + return filter; +} + +/* + * Is this serialized payload still selective enough to be worth delivering? + * + * Counts the set bits in the wire form, so the coordinator can ask it of a + * merged accumulator without rebuilding a filter -- which is the point, because + * the union of N parts is denser than any one of them. Three parts at 60% fill + * OR together to as much as 94%, so the producers' own checks do not protect + * consumers from the merged result; this is the check that does. + * + * Fill, not false positive rate: the serialized part carries no hash count, so + * fill is all there is to go on here. ANSER_BLOOM_MERGED_MAX_FILL is set + * accordingly -- high enough that it cannot reject a filter that is still good + * at a large k, which makes it a backstop rather than a tuning knob. + */ +bool +AnserBloomPartWorthSending(const void *payload, Size payload_len) +{ + const char *bits; + Size bitset_bytes; + uint64 bits_set; + double fill; + + if (!AnserBloomLooksLikePart(payload, payload_len)) + return false; + + bitset_bytes = payload_len - sizeof(AnserBloomPartHeader); + if (bitset_bytes == 0) + return false; + + bits = (const char *) payload + sizeof(AnserBloomPartHeader); + bits_set = pg_popcount(bits, (int) bitset_bytes); + fill = (double) bits_set / (double) (bitset_bytes * BITS_PER_BYTE); + + if (fill > ANSER_BLOOM_MERGED_MAX_FILL) + { + ANSER_DEBUG("anser: merged filter is %.1f%% full, above the %.0f%% limit; not worth delivering", + fill * 100.0, ANSER_BLOOM_MERGED_MAX_FILL * 100.0); + return false; + } + + return true; +} + +Size +AnserBloomSerializedSize(const bloom_filter *filter) +{ + if (filter == NULL) + return 0; + + return sizeof(AnserBloomPartHeader) + bloom_bitset_bytes(filter); +} + +/* + * Does this payload look like a serialized bloom part? Used by the in-place fold + * to confirm both the accumulator and the incoming payload are well-formed parts + * before OR-ing their bitsets. A false positive is effectively impossible: a + * part must carry the ABF1 magic, a known version, and sane part counts. + */ +bool +AnserBloomLooksLikePart(const void *payload, Size payload_len) +{ + if (payload == NULL || payload_len < sizeof(AnserBloomPartHeader)) + return false; + + return AnserBloomValidateHeader((const AnserBloomPartHeader *) payload, + payload_len); +} + +/* + * Fold an incoming part into an accumulator part IN PLACE. + * + * When a merged part and the incoming part are the same serialized size (they + * share bitset params derived from the condition key), the union is a pure + * bitwise OR of the two bitsets plus a bump of the merged header's fold count -- + * no reallocation. This mutates `acc` directly, so the caller must hold whatever + * lock guards the buffer (AnserChannelLock, for the channel payload). + * + * Returns true only when the in-place union applied. It returns false -- and + * leaves `acc` untouched (all checks run before any write) -- when the union + * cannot be done by raw OR: sizes differ, either side is not a valid part, or + * the filter parameters disagree. Since every part on a channel shares the same + * (condition-key-derived) parameters and therefore the same serialized size, the + * first part is stored verbatim and every later part folds in here; a false + * return means a malformed/mismatched payload and the caller cancels the channel. + */ +bool +AnserBloomFoldPartInPlace(void *acc, Size acc_len, + const void *part, Size part_len) +{ + AnserBloomPartHeader *ah; + unsigned char *abits; + const unsigned char *pbits; + Size bitset_bytes; + Size i; + + /* Only a same-sized, valid part-vs-part union can be done by raw OR. */ + if (acc_len != part_len || + !AnserBloomLooksLikePart(acc, acc_len) || + !AnserBloomLooksLikePart(part, part_len)) + return false; + + ah = (AnserBloomPartHeader *) acc; + + /* + * Equal serialized size is sufficient: every part on a channel is built by + * bloom_create from the same (condition-key-derived) parameters, so equal + * length implies an identical bitset shape. The bitset is whatever follows + * the header, so its length is the payload length minus the header. + */ + bitset_bytes = acc_len - sizeof(AnserBloomPartHeader); + abits = (unsigned char *) acc + sizeof(AnserBloomPartHeader); + pbits = (const unsigned char *) part + sizeof(AnserBloomPartHeader); + + for (i = 0; i < bitset_bytes; i++) + abits[i] |= pbits[i]; + + /* One more segment part folded into the running merged part. */ + ah->total_parts += 1; + + return true; +} + +/* + * Serialize one filter as a wire part: an AnserBloomPartHeader followed by the + * raw bitset. Fails (returns false) on bogus arguments or a too-small buffer. + */ +bool +AnserBloomSerializePart(const bloom_filter *filter, uint32 part_index, + uint32 total_parts, void *buffer, Size buffer_size, + Size *payload_len) +{ + AnserBloomPartHeader header; + Size bitset_bytes; + Size total_len; + + if (payload_len != NULL) + *payload_len = 0; + + if (filter == NULL || buffer == NULL || total_parts == 0 || + part_index >= total_parts) + return false; + + bitset_bytes = bloom_bitset_bytes(filter); + total_len = sizeof(AnserBloomPartHeader) + bitset_bytes; + if (buffer_size < total_len) + return false; + + MemSet(&header, 0, sizeof(header)); + header.magic = ANSER_BLOOM_PART_MAGIC; + header.version = ANSER_BLOOM_PART_VERSION; + header.part_index = part_index; + header.total_parts = total_parts; + + memcpy(buffer, &header, sizeof(header)); + memcpy((char *) buffer + sizeof(header), bloom_bitset_data(filter), + bitset_bytes); + + if (payload_len != NULL) + *payload_len = total_len; + return true; +} + +/* + * Rebuild the filter from a received merged part. + * + * The bitset parameters are NOT taken from the wire header: the caller passes + * the same (total_elems, max_payload_bytes, seed) used to produce the filter -- + * it holds them in the plan node, so both ends agree by construction. We rebuild + * the empty filter from those, then load the received bitset into it. The wire + * header is still validated (magic/version/counts) and, crucially, the received + * bitset length must exactly match the size the local parameters imply; any + * mismatch (version/parameter skew, truncation) returns NULL so the consumer + * fails open rather than loading a wrongly-shaped bitset. part_index/total_parts + * are surfaced from the header for diagnostics. + */ +bloom_filter * +AnserBloomDeserializePart(const void *payload, Size payload_len, + int64 total_elems, Size max_payload_bytes, uint64 seed, + uint32 *part_index, uint32 *total_parts) +{ + const AnserBloomPartHeader *header; + bloom_filter *filter; + + if (part_index != NULL) + *part_index = 0; + if (total_parts != NULL) + *total_parts = 0; + + if (payload == NULL || payload_len < sizeof(AnserBloomPartHeader)) + return NULL; + + header = (const AnserBloomPartHeader *) payload; + if (!AnserBloomValidateHeader(header, payload_len)) + return NULL; + + if (max_payload_bytes <= sizeof(AnserBloomPartHeader)) + return NULL; + if (total_elems < 1) + total_elems = 1; + + /* + * Build the filter straight from the received bitset, sized by our own + * parameters. bloom_create_from_bitset returns NULL unless the received + * length is exactly the size those parameters imply, so the fail-open + * described above happens here. A filter is thus only ever populated at + * construction and, from then on, only grown by add/union -- never re-set. + */ + filter = bloom_create_from_bitset(total_elems, + AnserBloomWorkMemKb(max_payload_bytes), + seed, + (const unsigned char *) payload + + sizeof(AnserBloomPartHeader), + payload_len - sizeof(AnserBloomPartHeader)); + if (filter == NULL) + return NULL; + + if (part_index != NULL) + *part_index = header->part_index; + if (total_parts != NULL) + *total_parts = header->total_parts; + return filter; +} + +/* + * Validate the wire framing of a part header. The bitset parameters are not + * carried on the wire (both ends rebuild the filter from the shared plan + * parameters), so this only checks the framing: magic/version, a sane fold + * count, and that the payload carries a header plus at least some bitset. The + * authoritative size check -- that the received bitset matches the size the local + * parameters imply -- is done in AnserBloomDeserializePart. + */ +static bool +AnserBloomValidateHeader(const AnserBloomPartHeader *header, Size payload_len) +{ + if (header == NULL) + return false; + + if (header->magic != ANSER_BLOOM_PART_MAGIC || + header->version != ANSER_BLOOM_PART_VERSION) + return false; + + if (header->total_parts == 0 || header->part_index >= header->total_parts) + return false; + + return payload_len > sizeof(AnserBloomPartHeader); +} diff --git a/gpcontrib/anser/src/anserinit.c b/gpcontrib/anser/src/anserinit.c new file mode 100644 index 00000000000..ff64d1fc44c --- /dev/null +++ b/gpcontrib/anser/src/anserinit.c @@ -0,0 +1,225 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserinit.c + * Module entry point: GUCs and the core hooks Anser hangs off. + * + * Anser is a shared_preload_libraries extension. Everything it needs from the + * server is reached through an existing extensibility point: + * + * planner_hook runtime-filter injection + * RegisterCustomScanMethods the injected plan nodes + * cdbdisp_notify_hook parts arriving from segments + * ExecutorEnd_hook dropping a query's channels + * + * It must be preloaded, because a segment backend deserializing a dispatched + * plan has no opportunity to load the library first. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserinit.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/xact.h" +#include "anser.h" +#include "anserplan.h" +#include "ansersideband.h" +#include "cdb/cdbdisp.h" +#include "cdb/cdbvars.h" +#include "executor/executor.h" +#include "miscadmin.h" +#include "optimizer/planner.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); + +bool gp_anser_enable = false; +bool gp_anser_runtime_filter = false; +bool gp_anser_debug = false; +int gp_anser_max_info_size = 64 * 1024 * 1024 + 1024 * 1024; +int gp_anser_timeout_ms = 1000; + +static void anser_define_gucs(void); +static PlannedStmt *anser_planner(Query *parse, const char *query_string, + int cursorOptions, ParamListInfo boundParams, + OptimizerOptions *optimizer_options); + +static planner_hook_type prev_planner_hook = NULL; +static ExecutorEnd_hook_type prev_ExecutorEnd_hook = NULL; + +static void anser_executor_end(QueryDesc *queryDesc); +static void anser_xact_callback(XactEvent event, void *arg); + +void +_PG_init(void) +{ + anser_define_gucs(); + + /* + * Only a preloaded library can be relied on to have installed its hooks in + * every backend. Loaded any other way, Anser stays inert: the GUCs exist + * (so a stray setting is not an error) but nothing is wired up. + */ + if (!process_shared_preload_libraries_in_progress) + return; + + prev_planner_hook = planner_hook; + planner_hook = anser_planner; + + /* + * Channels live in backend memory on both ends, so they need a point to be + * dropped. ExecutorEnd covers the normal path; the transaction callback + * catches queries that end by erroring. + */ + prev_ExecutorEnd_hook = ExecutorEnd_hook; + ExecutorEnd_hook = anser_executor_end; + RegisterXactCallback(anser_xact_callback, NULL); + + /* + * Handle Anser notifies arriving from QEs on the dispatch connections. + * Installed unconditionally: it is inert until a segment sends one, and a + * QE that never dispatches never calls it. + */ + cdbdisp_notify_hook = AnserDispatchNotifyHandler; + + /* + * The producer and consumer nodes travel to the segments inside dispatched + * plans, so every backend must be able to resolve their CustomScan methods + * by name. Registering here covers QD and QE alike. + */ + AnserRegisterRuntimeFilterMethods(); +} + +/* + * The subsystem's GUCs. All are "anser.*"-qualified because they belong to a + * loadable module; the C variables keep their gp_anser_ names. + */ +static void +anser_define_gucs(void) +{ + DefineCustomBoolVariable("anser.enable", + "Enables the Anser adaptive information sharing subsystem.", + "When disabled, the plan pass never injects anything and no filters are exchanged.", + &gp_anser_enable, + false, + PGC_SIGHUP, + 0, + NULL, NULL, NULL); + + DefineCustomBoolVariable("anser.runtime_filter", + "Enables injection of Anser runtime bloom filters into plans.", + "Requires anser.enable; the plan pass is a no-op otherwise.", + &gp_anser_runtime_filter, + false, + PGC_USERSET, + GUC_EXPLAIN, + NULL, NULL, NULL); + + DefineCustomBoolVariable("anser.debug", + "Logs each step of the Anser filter exchange.", + "Traces publish, merge, delivery and receive in the log of the process each happens in.", + &gp_anser_debug, + false, + PGC_USERSET, + 0, + NULL, NULL, NULL); + + DefineCustomIntVariable("anser.max_info_size", + "Sets the maximum byte size of one Anser information record.", + "Caps the serialized payload a channel may carry, and with it the effective bloom-filter size. The default holds a full 64 MB bitset plus its serialized-part header.", + &gp_anser_max_info_size, + 64 * 1024 * 1024 + 1024 * 1024, 1, INT_MAX, + PGC_USERSET, + 0, + NULL, NULL, NULL); + + DefineCustomIntVariable("anser.timeout_ms", + "Sets how long an Anser consumer waits for its filter.", + "On expiry the consumer runs unfiltered. The deadline matters because a producer that gets squelched never publishes at all.", + &gp_anser_timeout_ms, + 1000, 0, INT_MAX, + PGC_USERSET, + GUC_UNIT_MS, + NULL, NULL, NULL); + + MarkGUCPrefixReserved("anser"); +} + +/* + * Plan the query as usual, then hand the finished tree to the runtime-filter + * pass. Wrapping the hook this way covers both optimizers, because ORCA is + * dispatched from inside standard_planner(). + */ +static PlannedStmt * +anser_planner(Query *parse, const char *query_string, int cursorOptions, + ParamListInfo boundParams, OptimizerOptions *optimizer_options) +{ + PlannedStmt *result; + + if (prev_planner_hook) + result = prev_planner_hook(parse, query_string, cursorOptions, + boundParams, optimizer_options); + else + result = standard_planner(parse, query_string, cursorOptions, + boundParams, optimizer_options); + + AnserApplyRuntimeFilters(result); + + return result; +} + +/* + * Drop the transport's per-query state. + * + * Nested executor runs (a function body, say) must not clear state the outer + * query is still using, so only the outermost end resets. + */ +static void +anser_executor_end(QueryDesc *queryDesc) +{ + static int nesting_level = 0; + + nesting_level++; + PG_TRY(); + { + if (prev_ExecutorEnd_hook) + prev_ExecutorEnd_hook(queryDesc); + else + standard_ExecutorEnd(queryDesc); + } + PG_FINALLY(); + { + nesting_level--; + } + PG_END_TRY(); + + if (nesting_level == 0) + AnserSidebandResetAll(); +} + +static void +anser_xact_callback(XactEvent event, void *arg) +{ + if (event == XACT_EVENT_ABORT || event == XACT_EVENT_PARALLEL_ABORT) + AnserSidebandResetAll(); +} diff --git a/gpcontrib/anser/src/anserpayload.c b/gpcontrib/anser/src/anserpayload.c new file mode 100644 index 00000000000..2926a15a3e4 --- /dev/null +++ b/gpcontrib/anser/src/anserpayload.c @@ -0,0 +1,108 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserpayload.c + * The registry of payload types. + * + * One row per kind of runtime information a channel can carry. See + * anserpayload.h for what a row means and how to add one. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserpayload.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anserfilter.h" +#include "anserpayload.h" + +/* + * Indexed by the wire byte itself, so a lookup is one load and registering a + * type is one line (the pattern syscache.c:154 uses for its cache + * descriptors). Unregistered slots are all-zero, which is the answer we want + * for every question asked of them: no fold, no checksum, no name. + * + * Only two of the 256 slots are used, so this costs 8 KB of read-only data to + * hold mostly zeroes. That is the whole point: the alternative ways to reach a + * descriptor in constant time -- a second array mapping code to row, or a + * switch -- are extra code that has to be kept in step with the table, and the + * 8 KB buys them all away. A linear search over a two-row table would be just + * as fast in practice; what it would not be is impossible to get wrong. + */ +static const AnserPayloadOps AnserPayloadTable[PG_UINT8_MAX + 1] = +{ + /* + * A subscription: header and key only. It has no body to fold or to + * checksum, but its header still is checksummed -- that is what protects + * the condition key it is asking to be served on. + */ + [(unsigned char) ANSER_PAYLOAD_NONE] = { + ANSER_PAYLOAD_NONE, "none", false, NULL, NULL + }, + + /* + * A bloom filter part. AnserBloomFoldPartInPlace already has exactly the + * fold contract -- it validates both sides before touching the accumulator + * and ORs the bitsets -- so it is used directly rather than wrapped, as is + * the fill check for worth_delivering. + */ + [(unsigned char) ANSER_PAYLOAD_BLOOM] = { + ANSER_PAYLOAD_BLOOM, "bloom", true, AnserBloomFoldPartInPlace, + AnserBloomPartWorthSending + } +}; + +/* + * Resolve a code, or NULL if nothing has registered it. + * + * The NULL matters: 'code' reaches us straight off the wire, so "no such type" + * is a case that has to be reportable rather than silently defaulted. It is + * also the diagnostic that catches a payload type given a code in the header + * but no row in the table. + */ +const AnserPayloadOps * +AnserPayloadLookup(char code) +{ + /* + * Cast before indexing. 'char' is signed on x86, and the code is an + * untrusted byte: without this, a 0x80-0xff code from a corrupted or + * hostile message would index the table at a negative offset. + */ + const AnserPayloadOps *ops = &AnserPayloadTable[(unsigned char) code]; + + if (ops->code == '\0') + return NULL; + + /* The index is authoritative; a row filed under the wrong code is a typo. */ + Assert(ops->code == code); + + return ops; +} + +bool +AnserPayloadChecksumsBody(char code) +{ + /* + * No presence check: an unregistered code lands on a zeroed slot, and + * "false" is exactly right for it -- there is nothing to checksum that we + * would know how to use anyway. + */ + return AnserPayloadTable[(unsigned char) code].checksum_body; +} diff --git a/gpcontrib/anser/src/anserplan.c b/gpcontrib/anser/src/anserplan.c new file mode 100644 index 00000000000..6d91f6584df --- /dev/null +++ b/gpcontrib/anser/src/anserplan.c @@ -0,0 +1,432 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserplan.c + * Post-planning transformation that injects Anser runtime bloom-filter + * producer/consumer CustomScan nodes into a finished plan tree. + * + * The pass runs once from planner() (after both the Postgres planner and ORCA, + * and after set_plan_references / the cdbllize slice passes), recognizes one + * supported join shape, and inserts a producer on the hash build side and a + * consumer above the probe scan. See anserplan.h for the + * rationale. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserplan.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "anserfilter.h" +#include "anserplan.h" +#include "cdb/cdbvars.h" +#include "catalog/pg_type.h" +#include "nodes/nodeFuncs.h" +#include "nodes/pg_list.h" +#include "utils/acl.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" + +/* Runtime-filter bloom size bounds (realized bitset bytes). */ +#define ANSER_RF_MIN_BYTES (1024 * 1024) /* bloom_create's 1 MB floor */ +#define ANSER_RF_MAX_BYTES (64 * 1024 * 1024) +#define ANSER_RF_HEADER_ROOM 64 + +/* Per-statement state for the injection pass. */ +typedef struct AnserInjectCtx +{ + uint32 next_condition_id; + int next_plan_node_id; + List *consumer_keys; /* condition_keys already given a consumer node; + * enforces one consumer per channel (see + * anser_try_inject) */ +} AnserInjectCtx; + +static int anser_max_plan_node_id(Plan *plan); +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; + + 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. + */ +bool +AnserRuntimeFilterSize(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 */ + + /* + * The element count stays the honest estimate; only the *size* is clamped. + * + * It is tempting to clamp elems instead -- it is the number that drives the + * size, so bounding it bounds the bitset in one step. That is a trap. + * total_elems is also what optimal_k() sizes the hash count from, so + * understating it understates k: a 128M-row build side reported as 33.5M + * gets k=10, when the optimum for 128M keys in a 512 Mbit filter is k=3. + * Since the producer inserts all 128M keys regardless of what we wrote + * down, the result is a filter with a 38% false positive rate where 15% was + * available. Lowering the declared count never makes a filter fit; it only + * relabels it, and then misconfigures it. + * + * INT32 is the real bound on elems: custom_private carries it as an Integer + * node. The density check below refuses anything remotely near that. + */ + elems = (est_rows > 0.0) ? (int64) est_rows : 1; + if (elems < 1) + elems = 1; + if (elems > PG_INT32_MAX) + elems = PG_INT32_MAX; + + target_bytes = Min(cap_bytes, Max((int64) ANSER_RF_MIN_BYTES, elems * 2)); + realized = ANSER_RF_MIN_BYTES; + while ((realized << 1) <= target_bytes) + realized <<= 1; + + /* + * Refuse a join whose build side cannot be usefully summarized within the + * payload cap. + * + * Refusing here is the cheapest possible outcome: no producer, no consumer, + * no channel, and nothing for a consumer to wait on and time out against. + * The runtime checks in anserfilter.c exist for when this estimate turns + * out to be wrong, not instead of this one. + */ + if (realized * (double) BITS_PER_BYTE / (double) elems + < ANSER_BLOOM_MIN_BITS_PER_KEY) + return false; + + *total_elems = elems; + *max_payload = cap_bytes + ANSER_RF_HEADER_ROOM; + *planned_bytes = realized; + return true; +} + +/* + * Match a single-column equijoin over plain (by-value) Vars and return the + * inner (build) and outer (probe) key attnos. After set_plan_references the + * operands are INNER_VAR / OUTER_VAR references into the join's child tlists. + */ +static bool +anser_hashjoin_keys(HashJoin *hj, AttrNumber *inner_attno, AttrNumber *outer_attno) +{ + OpExpr *op; + Node *l; + Node *r; + Var *outer_var; + Var *inner_var; + + if (list_length(hj->hashclauses) != 1) + return false; + op = (OpExpr *) linitial(hj->hashclauses); + if (!IsA(op, OpExpr) || list_length(op->args) != 2) + return false; + + l = (Node *) linitial(op->args); + r = (Node *) lsecond(op->args); + while (l != NULL && IsA(l, RelabelType)) + l = (Node *) ((RelabelType *) l)->arg; + while (r != NULL && IsA(r, RelabelType)) + r = (Node *) ((RelabelType *) r)->arg; + if (l == NULL || r == NULL || !IsA(l, Var) || !IsA(r, Var)) + return false; + + if (((Var *) l)->varno == OUTER_VAR && ((Var *) r)->varno == INNER_VAR) + { + outer_var = (Var *) l; + inner_var = (Var *) r; + } + else if (((Var *) l)->varno == INNER_VAR && ((Var *) r)->varno == OUTER_VAR) + { + outer_var = (Var *) r; + inner_var = (Var *) l; + } + else + return false; + + /* + * Producer and consumer hash the raw Datum bytes, which is only correct + * when SQL equality coincides with bitwise Datum equality of the key. + * That requires the SAME by-value type on both sides: cross-type equijoins + * (e.g. float4 = float8, date = timestamp) hash different bit patterns for + * equal values, and floats are excluded even same-typed because -0.0 and + * 0.0 compare equal but are not bitwise equal. Anything looser could + * prune rows that actually join. + */ + if (inner_var->vartype != outer_var->vartype) + return false; + if (!get_typbyval(inner_var->vartype)) + return false; + if (inner_var->vartype == FLOAT4OID || inner_var->vartype == FLOAT8OID) + return false; + + *inner_attno = inner_var->varattno; + *outer_attno = outer_var->varattno; + return true; +} + +/* + * Follow the build side down from the Hash to the base SeqScan, mapping the key + * attno through each passthrough targetlist. Wrapping the base scan (rather than + * an intermediate Motion) keeps the injected CustomScan's custom_scan_tlist made + * of base-relation Vars, which (a) deparses cleanly in EXPLAIN and (b) is the + * proven-safe "leaf child" case for MPP slice/gang setup. Only plain single- + * child passthroughs (Hash, Motion) with Var targetlist entries are supported. + * On success *parent_out is the node whose outerPlan is the base scan. + */ +static bool +anser_resolve_build_scan(Plan *hash, AttrNumber inner_attno, Plan **parent_out, + Plan **scan_out, AttrNumber *attno_out) +{ + Plan *node = hash; + AttrNumber attno = inner_attno; + + for (;;) + { + TargetEntry *tle; + Var *var; + Plan *child; + + if (node == NULL || + attno < 1 || attno > list_length(node->targetlist)) + return false; + + tle = (TargetEntry *) list_nth(node->targetlist, attno - 1); + if (tle == NULL || !IsA(tle->expr, Var)) + return false; + var = (Var *) tle->expr; + if (var->varno != OUTER_VAR) /* single-child passthrough only */ + return false; + + child = outerPlan(node); + if (child == NULL) + return false; + + if (IsA(child, SeqScan)) + { + *parent_out = node; + *scan_out = child; + *attno_out = var->varattno; + return true; + } + if (!IsA(child, Hash) && !IsA(child, Motion)) + return false; + + node = child; + attno = var->varattno; + } +} + +/* + * If this HashJoin is the supported shape, inject a producer above the build + * base scan and a consumer above the probe scan. + */ +static void +anser_try_inject(HashJoin *hj, AnserInjectCtx *ctx) +{ + Plan *hash = innerPlan(hj); /* build side */ + Plan *probe = outerPlan(hj); /* probe side */ + Plan *build_parent; + Plan *build_scan; + AttrNumber inner_attno; + AttrNumber outer_attno; + AttrNumber build_attno; + int64 total_elems; + int64 max_payload; + int64 planned_bytes; + uint32 condition_id; + char condition_key[ANSER_CONDITION_KEY_SIZE]; + CustomScan *producer; + CustomScan *consumer; + ListCell *lc; + + if (hj->join.jointype != JOIN_INNER && hj->join.jointype != JOIN_RIGHT) + return; + if (hash == NULL || !IsA(hash, Hash)) + return; + if (probe == NULL || !IsA(probe, SeqScan)) + return; + + if (!anser_hashjoin_keys(hj, &inner_attno, &outer_attno)) + return; + if (!anser_resolve_build_scan(hash, inner_attno, &build_parent, &build_scan, + &build_attno)) + return; + if (!AnserRuntimeFilterSize(hash->plan_rows, &total_elems, &max_payload, &planned_bytes)) + return; + + condition_id = ctx->next_condition_id++; + snprintf(condition_key, sizeof(condition_key), "anser_rf_%u", condition_id); + + /* + * One consumer per channel. Two consumer nodes sharing a channel would + * both be served -- the coordinator pushes to every subscriber -- but they + * would also both count toward nothing: the channel's part count comes from + * the producers, so a second consumer only multiplies deliveries. More to + * the point, the two would be indistinguishable in the debug trace and in + * any future per-channel accounting, so keep the invariant. + * + * Minting a unique condition_id per injection makes this hold by + * construction, so the check never fires. It stays as a guard against + * 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); + 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); + consumer->scan.plan.plan_node_id = ctx->next_plan_node_id++; + outerPlan(hj) = (Plan *) consumer; + + /* Record the channel so no later join can add a second consumer on it. */ + ctx->consumer_keys = lappend(ctx->consumer_keys, pstrdup(condition_key)); +} + +/* + * Manual in-place traversal (recurse spine + custom_plans). We do not use + * plan_tree_mutator, whose CustomScan arm does not descend into custom_plans. + */ +static void +anser_inject_walk(Plan *plan, AnserInjectCtx *ctx) +{ + if (plan == NULL) + return; + + if (IsA(plan, HashJoin)) + anser_try_inject((HashJoin *) plan, ctx); + + anser_inject_walk(outerPlan(plan), ctx); + anser_inject_walk(innerPlan(plan), ctx); + if (IsA(plan, CustomScan)) + { + ListCell *lc; + + foreach(lc, ((CustomScan *) plan)->custom_plans) + anser_inject_walk((Plan *) lfirst(lc), ctx); + } +} diff --git a/gpcontrib/anser/src/anserplanexec.c b/gpcontrib/anser/src/anserplanexec.c new file mode 100644 index 00000000000..bcfc4575c66 --- /dev/null +++ b/gpcontrib/anser/src/anserplanexec.c @@ -0,0 +1,707 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * anserplanexec.c + * CustomScan providers that carry the Anser runtime bloom filter through + * the executor: a pass-through "producer" above the hash build input that + * observes the build join key and publishes a bloom filter, and a + * pass-through "consumer" above the probe scan that prunes rows whose key is + * definitely absent from the received (unioned) filter. + * + * Both are thin drivers over the helper library in + * anserbloom.h, which selects transport by role + * (segment -> libpq to QD; coordinator -> direct shmem) and unions parts. The + * plan-injection pass (anserplan.c) builds the nodes and stashes the parameters + * in CustomScan.custom_private using the layout in AnserRfPrivateIndex below. + * + * IDENTIFICATION + * gpcontrib/anser/src/anserplanexec.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/skey.h" +#include "anser.h" +#include "anserbloom.h" +#include "anserfilter.h" +#include "anserplan.h" +#include "cdb/cdbutil.h" +#include "cdb/cdbvars.h" +#include "commands/explain.h" +#include "executor/executor.h" +#include "lib/bloomfilter.h" +#include "nodes/extensible.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "nodes/pg_list.h" +#include "nodes/value.h" +#include "utils/guc.h" + +/* + * Positional layout of CustomScan.custom_private shared with anserplan.c. + * Integer nodes except the last two, which are Strings. Both providers read + * the same list (each ignores fields it does not need). + */ +typedef enum AnserRfPrivateIndex +{ + ANSER_RF_PRIV_CONDITION_ID = 0, /* Integer: channel condition_id */ + ANSER_RF_PRIV_KEY_ATTNO, /* Integer: build/probe key attno */ + ANSER_RF_PRIV_TOTAL_ELEMS, /* Integer: bloom sizing (producer) */ + ANSER_RF_PRIV_MAX_PAYLOAD, /* Integer: bloom sizing (producer) */ + ANSER_RF_PRIV_PLANNED_BYTES, /* Integer: planned bitset bytes (EXPLAIN) */ + ANSER_RF_PRIV_CONDITION_KEY, /* String: channel condition_key */ + ANSER_RF_PRIV__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; + bool started; /* this process actually executed the node */ + 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; + bool received; /* have we run the receive/union yet? */ + bool pushed_down; /* filter handed to the child scan as an + * SK_BLOOM_FILTER scan key (the scan filters, + * we pass through) */ +} AnserBloomConsumeScanState; + +/* Provider callbacks. */ +static Node *anser_produce_create_state(CustomScan *cscan); +static void anser_produce_begin(CustomScanState *node, EState *estate, int eflags); +static TupleTableSlot *anser_produce_exec(CustomScanState *node); +static void anser_produce_end(CustomScanState *node); +static void anser_produce_rescan(CustomScanState *node); +static void anser_produce_explain(CustomScanState *node, List *ancestors, + ExplainState *es); + +static Node *anser_consume_create_state(CustomScan *cscan); +static void anser_consume_begin(CustomScanState *node, EState *estate, int eflags); +static TupleTableSlot *anser_consume_exec(CustomScanState *node); +static void anser_consume_end(CustomScanState *node); +static void anser_consume_rescan(CustomScanState *node); +static void anser_consume_explain(CustomScanState *node, List *ancestors, + ExplainState *es); + +static const CustomScanMethods anser_produce_scan_methods = +{ + .CustomName = "Anser Bloom Producer", + .CreateCustomScanState = anser_produce_create_state, +}; + +static const CustomScanMethods anser_consume_scan_methods = +{ + .CustomName = "Anser Bloom Consumer", + .CreateCustomScanState = anser_consume_create_state, +}; + +static const CustomExecMethods anser_produce_exec_methods = +{ + .CustomName = "Anser Bloom Producer", + .BeginCustomScan = anser_produce_begin, + .ExecCustomScan = anser_produce_exec, + .EndCustomScan = anser_produce_end, + .ReScanCustomScan = anser_produce_rescan, + .ExplainCustomScan = anser_produce_explain, +}; + +static const CustomExecMethods anser_consume_exec_methods = +{ + .CustomName = "Anser Bloom Consumer", + .BeginCustomScan = anser_consume_begin, + .ExecCustomScan = anser_consume_exec, + .EndCustomScan = anser_consume_end, + .ReScanCustomScan = anser_consume_rescan, + .ExplainCustomScan = anser_consume_explain, +}; + +void +AnserRegisterRuntimeFilterMethods(void) +{ + RegisterCustomScanMethods(&anser_produce_scan_methods); + RegisterCustomScanMethods(&anser_consume_scan_methods); +} + +/* ---- node builders (called from the injection pass in anserplan.c) ---- */ + +/* + * Identity output targetlist for a pass-through CustomScan: a Var per child + * column referencing the scan tuple via INDEX_VAR, so ExecScan projects the + * child tuple through unchanged. (Post-setrefs we build this by hand rather + * than relying on set_customscan_references.) + */ +static List * +anser_rf_identity_tlist(List *child_tlist) +{ + List *tlist = NIL; + ListCell *lc; + AttrNumber attno = 0; + + foreach(lc, child_tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + Var *var; + + attno++; + var = makeVar(INDEX_VAR, attno, + exprType((Node *) tle->expr), + exprTypmod((Node *) tle->expr), + exprCollation((Node *) tle->expr), + 0); + tlist = lappend(tlist, + makeTargetEntry((Expr *) var, attno, + tle->resname ? pstrdup(tle->resname) : NULL, + tle->resjunk)); + } + + return tlist; +} + +static CustomScan * +anser_build_rf_scan(const CustomScanMethods *methods, Plan *child, + AttrNumber key_attno, uint32 condition_id, + const char *condition_key, int64 total_elems, + Size max_payload_bytes, int64 planned_bytes) +{ + 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))); + + 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) +{ + return anser_build_rf_scan(&anser_produce_scan_methods, child, key_attno, + condition_id, condition_key, total_elems, + max_payload_bytes, planned_bytes); +} + +CustomScan * +AnserBuildBloomConsumerScan(Plan *child, AttrNumber key_attno, + uint32 condition_id, const char *condition_key, + int64 total_elems, Size max_payload_bytes, + int64 planned_bytes) +{ + return anser_build_rf_scan(&anser_consume_scan_methods, child, key_attno, + condition_id, condition_key, total_elems, + max_payload_bytes, planned_bytes); +} + +/* ---- 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); +} + +/* + * 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->started = false; + 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); + ANSER_DEBUG("anser: producer init cond=%u part=%u/%u elems=" INT64_FORMAT " payload=%zu state=%s", + key.condition_id, part_index, total_parts, total_elems, + max_payload, st->produce != NULL ? "ok" : "NULL"); + + node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); +} + +/* + * 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; + + /* + * Every process that deserializes the plan builds this node, but only the + * ones running its slice ever execute it; the rest must stay silent. + */ + if (!st->started) + { + st->started = true; + + /* + * No filter means the estimate that justified this node was wrong by + * enough that one cannot usefully be built. Say so now, on the first + * tuple, rather than after scanning the whole build side: the consumers + * are sitting on their timeout, and every millisecond of it is wasted. + * + * This is also the earliest point at which cancelling is safe. Doing + * it at ExecInit time would cancel channels from the coordinator, which + * builds this node for every slice but executes none of them. + */ + if (st->produce != NULL && !st->published && + !ExecAnserBloomFilterProduceHasFilter(st->produce)) + { + ANSER_DEBUG("anser: producer has no usable filter; cancelling before the scan"); + (void) ExecAnserBloomFilterProduceCancel(st->produce); + st->published = true; + } + } + + slot = ExecProcNode(child); + + if (TupIsNull(slot)) + { + if (!st->published) + { + ANSER_DEBUG("anser: producer child exhausted, publishing (state=%s)", + st->produce != NULL ? "ok" : "NULL"); + (void) ExecAnserBloomFilterProducePublish(st->produce); + st->published = true; + } + 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; + + /* + * A producer that ran but never published was abandoned mid-scan (squelch, + * or an error upstream), and consumers must be told so rather than left + * waiting for a part that will never come. A producer that never ran at + * all belongs to another slice: cancelling here would destroy a channel + * this process has no part in -- which is exactly what the coordinator + * used to do to every runtime filter. + */ + if (st->started && !st->published && st->produce != NULL) + { + (void) ExecAnserBloomFilterProduceCancel(st->produce); + st->published = true; + } + + if (st->produce != NULL) + ExecEndAnserBloomFilterProduce(st->produce); + st->produce = NULL; + 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->filter = NULL; + st->received = false; + st->pushed_down = false; + + anser_rf_build_key(cscan, &key); + anser_rf_part_info(&part_index, &expected_parts); + + st->consume = ExecInitAnserBloomFilterConsume(&key, total_elems, max_payload, + expected_parts); + + node->custom_ps = list_make1(ExecInitNode(child, estate, eflags)); +} + +/* + * On first call, block to receive and union the bloom parts. A NULL filter + * (channel cancelled / unreachable / feature degraded) means fail open: pass + * every row through unfiltered. + */ +static void +anser_consume_receive(AnserBloomConsumeScanState *st) +{ + if (st->received) + return; + st->received = true; + + if (st->consume == NULL) + return; + + if (ExecAnserBloomFilterConsume(st->consume, ANSER_RF_REGISTRATION_TIMEOUT_MS)) + st->filter = ExecAnserBloomFilterConsumerGetFilter(st->consume); + else + st->filter = NULL; + + /* + * Diagnostic: a NULL filter means we fail open (no pruning). Log why -- + * cancelled delivery vs. too few bloom parts unioned -- so runtime-filter + * misbehavior is visible in the server log. + */ + if (st->filter == NULL) + elog(LOG, + "anser bloom consumer: no filter, failing open (cancelled=%d, received_parts=%u)", + ExecAnserBloomFilterConsumerWasCancelled(st->consume), + ExecAnserBloomFilterConsumerReceivedParts(st->consume)); +} + +/* + * Hand the received filter to the child SeqScan as an SK_BLOOM_FILTER scan + * key -- the exact structure PassByBloomFilter consumes (nodeSeqscan.c), so + * the scan does the pruning itself and a table AM that supports + * SCAN_SUPPORT_RUNTIME_FILTER may additionally use the key for block-level + * pruning. Runs before the child's first ExecProcNode, so the key is in + * node->filters when the scan descriptor is created. The filter stays owned + * by us; anser_consume_end ends the child before freeing it. + */ +static void +anser_consume_pushdown(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + PlanState *child = (PlanState *) linitial(node->custom_ps); + SeqScanState *sss; + ScanKey sk; + MemoryContext oldcxt; + + if (!gp_enable_runtime_filter_pushdown || !IsA(child, SeqScanState)) + return; + sss = (SeqScanState *) child; + if (!sss->filter_in_seqscan) + return; + + oldcxt = MemoryContextSwitchTo(node->ss.ps.state->es_query_cxt); + sk = (ScanKey) palloc0(sizeof(ScanKeyData)); + sk->sk_attno = st->key_attno; + sk->sk_flags = SK_BLOOM_FILTER; + sk->sk_argument = PointerGetDatum(st->filter); + sss->filters = lappend(sss->filters, sk); + MemoryContextSwitchTo(oldcxt); + + st->pushed_down = true; +} + +static TupleTableSlot * +anser_consume_next(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + PlanState *child = (PlanState *) linitial(node->custom_ps); + + anser_consume_receive(st); + + /* + * When the pushdown machinery is enabled, hand the filter to the scan and + * pass tuples through untouched; otherwise probe the filter here. + */ + if (st->filter != NULL && !st->pushed_down) + anser_consume_pushdown(node); + + for (;;) + { + TupleTableSlot *slot = ExecProcNode(child); + bool isnull; + Datum value; + + if (TupIsNull(slot)) + return NULL; + + /* + * Fail open (no usable filter) or pushed down (the scan filters via + * PassByBloomFilter): pass everything. + */ + if (st->filter == NULL || st->pushed_down) + return anser_rf_child_slot(node, slot); + + value = slot_getattr(slot, st->key_attno, &isnull); + + /* NULLs never match an equijoin key; let the join handle them. */ + if (!isnull) + { + /* + * Count lookups/prunes via instrumentation (nfiltered1/nfiltered2), + * not local state: these fields ride the CdbExplain_StatInst wire + * format back to the QD, so EXPLAIN ANALYZE shows them for + * segment-executed nodes too. + */ + InstrCountFiltered1(node, 1); + if (bloom_lacks_element(st->filter, (unsigned char *) &value, + sizeof(Datum))) + { + /* Definitely absent from the build side -> prune. */ + InstrCountFiltered2(node, 1); + continue; + } + } + + return anser_rf_child_slot(node, slot); + } +} + +static TupleTableSlot * +anser_consume_exec(CustomScanState *node) +{ + return ExecScan(&node->ss, + (ExecScanAccessMtd) anser_consume_next, + (ExecScanRecheckMtd) anser_rf_recheck); +} + +static void +anser_consume_end(CustomScanState *node) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + + /* + * End the child first: when the filter was pushed down, the child's scan + * key references it, so the filter must outlive the child node. + */ + if (node->custom_ps != NIL) + ExecEndNode((PlanState *) linitial(node->custom_ps)); + if (st->consume != NULL) + ExecEndAnserBloomFilterConsume(st->consume); + st->consume = NULL; + st->filter = NULL; +} + +static void +anser_consume_rescan(CustomScanState *node) +{ + if (node->custom_ps != NIL) + ExecReScan((PlanState *) linitial(node->custom_ps)); +} + +static void +anser_consume_explain(CustomScanState *node, List *ancestors, ExplainState *es) +{ + AnserBloomConsumeScanState *st = (AnserBloomConsumeScanState *) node; + + ExplainPropertyInteger("Bloom Filter Size", "bytes", st->planned_bytes, es); + + if (es->analyze && node->ss.ps.instrument != NULL) + { + char buf[128]; + double nloops = node->ss.ps.instrument->nloops; + double nfiltered = node->ss.ps.instrument->nfiltered2; + double nchecked = node->ss.ps.instrument->nfiltered1; + + /* + * checked/rejected come from nfiltered1/nfiltered2, which cdbexplain + * transports from the segments and deposits into this node's + * instrument. As with all per-node EXPLAIN ANALYZE stats in MPP, + * these are the winning segment's values (max ntuples/nloops), not a + * cluster-wide sum. memory is planned_bytes: the unioned filter's + * size is identical on every segment by construction (anser_rf_size). + * When the filter was pushed into the child scan, pruning happens + * there instead (its "Rows Removed by Pushdown Runtime Filter" line), + * so our counters stay zero. + */ + if (st->pushed_down) + snprintf(buf, sizeof(buf), + "memory=" INT64_FORMAT "kB (pushed down to Seq Scan)", + st->planned_bytes / 1024); + else + snprintf(buf, sizeof(buf), + "memory=" INT64_FORMAT "kB checked=%.0f rejected=%.0f", + st->planned_bytes / 1024, nchecked, nfiltered); + ExplainPropertyText("Bloom Filter Stats", buf, es); + + if (!st->pushed_down && nloops > 0) + ExplainPropertyFloat("Rows Removed by Bloom Filter", NULL, + nfiltered / nloops, 0, es); + } +} diff --git a/gpcontrib/anser/src/ansersideband.c b/gpcontrib/anser/src/ansersideband.c new file mode 100644 index 00000000000..556fe0d0a26 --- /dev/null +++ b/gpcontrib/anser/src/ansersideband.c @@ -0,0 +1,589 @@ +/*------------------------------------------------------------------------- + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * ansersideband.c + * Segment side of the dispatch-connection transport. + * + * A producer sends its part as a NOTIFY and moves on; a consumer subscribes, + * then blocks on its own dispatch socket until the coordinator pushes the + * merged filter back. Both directions reuse the connection the dispatcher + * already owns, so there is no second connection to open and nothing to + * authenticate. + * + * Reading the frontend socket in the middle of executing a query is the + * pattern cdb_sequence_nextval_qe() established (commands/sequence.c); the + * loop below mirrors its use of pq_startmsgread/pq_getbyte_if_available, but + * sleeps on the socket instead of spinning. + * + * IDENTIFICATION + * gpcontrib/anser/src/ansersideband.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "anser.h" +#include "ansersideband.h" +#include "cdb/cdbvars.h" +#include "commands/async.h" +#include "common/base64.h" +#include "libpq/libpq-be.h" +#include "libpq/libpq.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" +#include "nodes/pg_list.h" +#include "port/pg_bswap.h" +#include "port/pg_crc32c.h" +#include "storage/latch.h" +#include "tcop/dest.h" +#include "tcop/tcopprot.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" +#include "utils/wait_event.h" + +/* How long to sleep between wakeups while waiting for a delivery. */ +#define ANSER_SIDEBAND_POLL_MS 100L + +/* + * A delivery that arrived while we were waiting for a different channel. + * + * One slice can host consumers for more than one condition, and the + * coordinator pushes each channel as soon as it completes, so messages can + * arrive in an order we did not ask for. Rather than discard them (which + * would cost that consumer its filter), park them here and check the inbox + * before touching the socket. + */ +typedef struct AnserInboxEntry +{ + AnserChannelKey key; + char payload_type; + char *payload; /* NULL when cancelled */ + Size payload_len; + bool cancelled; +} AnserInboxEntry; + +static List *AnserInbox = NIL; + +static bool anser_sideband_send(const char *payload); +static bool anser_inbox_take(const AnserChannelKey *key, char payload_type, + void **payload, Size *payload_len, bool *cancelled); +static bool anser_sideband_read_one(long timeout_ms); + +/* + * Publish one part. Fire-and-forget: the coordinator does not acknowledge, + * because nothing on this side needs to wait for it. + */ +bool +AnserSidebandPublish(const AnserChannelKey *channel_key, char payload_type, + uint32 part_index, uint32 total_parts, + const void *payload, Size payload_len, bool cancelled) +{ + char *msg; + int flags = cancelled ? ANSER_WIRE_F_CANCELLED : 0; + bool ok; + + if (channel_key == NULL) + return false; + + if (!cancelled && payload_len > (Size) gp_anser_max_info_size) + { + /* Too large to ship; tell the coordinator so consumers stop waiting. */ + flags = ANSER_WIRE_F_CANCELLED; + payload = NULL; + payload_len = 0; + } + + msg = AnserWireFormat(channel_key, ANSER_WIRE_KIND_PART, payload_type, + part_index, total_parts, flags, + (flags & ANSER_WIRE_F_CANCELLED) ? NULL : payload, + (flags & ANSER_WIRE_F_CANCELLED) ? 0 : payload_len); + ok = anser_sideband_send(msg); + ANSER_DEBUG("anser: seg%d published cond=%u type=%c part=%u/%u bytes=%zu cancelled=%d sent=%d", + GpIdentity.segindex, channel_key->condition_id, payload_type, + part_index, total_parts, payload_len, + (flags & ANSER_WIRE_F_CANCELLED) ? 1 : 0, ok ? 1 : 0); + pfree(msg); + + return ok; +} + +/* + * Subscribe, then wait for the merged payload. + * + * Returns true with *payload set, or false for "run unfiltered" -- including + * on timeout. The deadline exists because a producer that gets squelched + * never publishes anything: ExecSquelchNode does not call CustomScan + * callbacks, it only marks the node (execAmi.c), so without a deadline this + * wait could outlive the reason for it. + */ +bool +AnserSidebandConsumeWait(const AnserChannelKey *channel_key, char payload_type, + void **payload, Size *payload_len, + bool *cancelled, long timeout_ms) +{ + char *msg; + TimestampTz start; + int reads = 0; + + if (payload != NULL) + *payload = NULL; + if (payload_len != NULL) + *payload_len = 0; + if (cancelled != NULL) + *cancelled = false; + + if (channel_key == NULL || MyProcPort == NULL || + MyProcPort->sock == PGINVALID_SOCKET) + return false; + + /* It may already be here: the coordinator pushes as soon as it can. */ + if (anser_inbox_take(channel_key, payload_type, payload, payload_len, cancelled)) + return payload != NULL && *payload != NULL; + + /* + * A subscription carries nothing, so its payload type is NONE -- what this + * consumer expects to receive is checked on delivery, not announced here. + */ + msg = AnserWireFormat(channel_key, ANSER_WIRE_KIND_SUBSCRIBE, + ANSER_PAYLOAD_NONE, 0, 0, 0, NULL, 0); + if (!anser_sideband_send(msg)) + { + pfree(msg); + return false; + } + pfree(msg); + ANSER_DEBUG("anser: seg%d subscribed cond=%u, waiting up to %ld ms", + GpIdentity.segindex, channel_key->condition_id, timeout_ms); + + start = GetCurrentTimestamp(); + for (;;) + { + CHECK_FOR_INTERRUPTS(); + + if (anser_inbox_take(channel_key, payload_type, payload, payload_len, + cancelled)) + return payload != NULL && *payload != NULL; + + if (timeout_ms >= 0 && + TimestampDifferenceExceeds(start, GetCurrentTimestamp(), timeout_ms)) + { + /* + * Report what we saw, not just that we waited: "nothing arrived" + * and "something arrived for another channel" are different bugs. + */ + ANSER_DEBUG("anser: seg%d gave up on cond=%u after %ld ms (read %d message(s), %d unclaimed)", + GpIdentity.segindex, channel_key->condition_id, + timeout_ms, reads, list_length(AnserInbox)); + return false; + } + + /* + * Any message we read lands in the inbox; the loop then rechecks + * whether it was the one we wanted. A read failure means the + * connection is gone, which the interconnect will report far more + * usefully than we can -- stop waiting and let the query run + * unfiltered. + */ + if (!anser_sideband_read_one(ANSER_SIDEBAND_POLL_MS)) + return false; + reads = list_length(AnserInbox); + } +} + +/* + * Wait briefly for one sideband message and stash it in the inbox. + * + * Returns false only when the connection is unusable; a timeout with nothing + * to read is a normal true. + */ +static bool +anser_sideband_read_one(long timeout_ms) +{ + unsigned char qtype; + int retval; + StringInfoData buf; + AnserInboxEntry *entry; + MemoryContext oldcxt; + char paytype; + uint32 crc; + int condid; + int flags; + int keylen; + int paylen; + const char *keyptr; + const char *payptr; + + pq_startmsgread(); + retval = pq_getbyte_if_available(&qtype); + if (retval == 0) + { + /* Nothing buffered: sleep on the socket rather than spinning. */ + pq_endmsgread(); + + ResetLatch(MyLatch); + (void) WaitLatchOrSocket(MyLatch, + WL_LATCH_SET | WL_SOCKET_READABLE | + WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + MyProcPort->sock, + timeout_ms, + PG_WAIT_EXTENSION); + return true; + } + + if (retval == EOF) + { + elog(LOG, "anser: dispatch connection closed while awaiting a filter"); + return false; + } + + if (qtype != GP_SIDEBAND_MESSAGE) + { + /* + * Nothing else should reach us here. Do not try to interpret or skip + * it: message-boundary sync is at stake, so leave it for the command + * loop and give up on the filter. + */ + pq_endmsgread(); + elog(LOG, "anser: unexpected message type '%c' while awaiting a filter", + (char) qtype); + return false; + } + + initStringInfo(&buf); + if (pq_getmessage(&buf, gp_anser_max_info_size + ANSER_CONDITION_KEY_SIZE + 64) != 0) + { + /* + * pq_getmessage clears the reading-message flag when it succeeds, but + * not on its EOF paths; clear it by hand so we do not trip the + * assertion in a later pq_startmsgread. + */ + pq_endmsgread(); + pfree(buf.data); + elog(LOG, "anser: could not read filter message"); + return false; + } + + paytype = (char) pq_getmsgint(&buf, 4); + crc = (uint32) pq_getmsgint(&buf, 4); + condid = pq_getmsgint(&buf, 4); + flags = pq_getmsgint(&buf, 4); + keylen = pq_getmsgint(&buf, 4); + if (keylen < 0 || keylen >= ANSER_CONDITION_KEY_SIZE) + { + pfree(buf.data); + elog(LOG, "anser: filter message has a bad condition key"); + return false; + } + keyptr = pq_getmsgbytes(&buf, keylen); + paylen = pq_getmsgint(&buf, 4); + if (paylen < 0 || paylen > gp_anser_max_info_size) + { + pfree(buf.data); + elog(LOG, "anser: filter message has a bad length"); + return false; + } + payptr = paylen > 0 ? pq_getmsgbytes(&buf, paylen) : NULL; + + if (crc != (uint32) AnserWirePushCrc(paytype, (uint32) condid, + (uint32) flags, keyptr, keylen, + payptr, paylen)) + { + /* + * Drop it and keep waiting: the deadline will expire and this consumer + * will run unfiltered. Using the filter anyway is the one thing we must + * not do -- a bit corrupted 1 -> 0 silently drops joinable rows. + */ + pfree(buf.data); + elog(LOG, "anser: checksum mismatch on filter for condition %u; discarding", + (uint32) condid); + return true; + } + + /* + * The inbox outlives this call and the memory context it was reached in, + * so anchor it somewhere stable; AnserSidebandResetAll drops it. + */ + oldcxt = MemoryContextSwitchTo(TopMemoryContext); + entry = palloc0(sizeof(AnserInboxEntry)); + entry->key.gp_session_id = gp_session_id; + entry->key.gp_command_count = gp_command_count; + entry->key.condition_id = (uint32) condid; + memcpy(entry->key.condition_key, keyptr, keylen); + entry->key.condition_key[keylen] = '\0'; + entry->payload_type = paytype; + entry->cancelled = (flags & ANSER_WIRE_F_CANCELLED) != 0; + if (!entry->cancelled && paylen > 0) + { + entry->payload = palloc(paylen); + memcpy(entry->payload, payptr, paylen); + entry->payload_len = paylen; + } + AnserInbox = lappend(AnserInbox, entry); + MemoryContextSwitchTo(oldcxt); + ANSER_DEBUG("anser: seg%d received cond=%u type=%c bytes=%zu cancelled=%d", + GpIdentity.segindex, (uint32) condid, paytype, + entry->payload_len, entry->cancelled ? 1 : 0); + + pfree(buf.data); + return true; +} + +/* Claim a delivery for this channel, if one has arrived. */ +static bool +anser_inbox_take(const AnserChannelKey *key, char payload_type, void **payload, + Size *payload_len, bool *cancelled) +{ + ListCell *lc; + + foreach(lc, AnserInbox) + { + AnserInboxEntry *entry = (AnserInboxEntry *) lfirst(lc); + + if (entry->key.condition_id != key->condition_id || + strncmp(entry->key.condition_key, key->condition_key, + ANSER_CONDITION_KEY_SIZE) != 0) + continue; + + /* + * Right channel, wrong kind of information. The coordinator stamps + * the type from the parts it folded, so this means a producer and a + * consumer disagree about what the channel carries -- claim the entry + * to stop waiting on it, and treat it as a cancellation so this + * consumer runs unfiltered. An empty delivery carries no type to + * check. + */ + if (!entry->cancelled && entry->payload != NULL && + entry->payload_type != payload_type) + { + elog(WARNING, "anser: condition %u delivered payload type '%c', expected '%c'", + key->condition_id, entry->payload_type, payload_type); + entry->cancelled = true; + } + + if (cancelled != NULL) + *cancelled = entry->cancelled; + if (!entry->cancelled && entry->payload != NULL) + { + if (payload != NULL) + { + *payload = palloc(entry->payload_len); + memcpy(*payload, entry->payload, entry->payload_len); + } + if (payload_len != NULL) + *payload_len = entry->payload_len; + } + + AnserInbox = foreach_delete_current(AnserInbox, lc); + if (entry->payload != NULL) + pfree(entry->payload); + pfree(entry); + return true; + } + + return false; +} + +/* Drop any deliveries nobody claimed. */ +void +AnserSidebandResetInbox(void) +{ + ListCell *lc; + + foreach(lc, AnserInbox) + { + AnserInboxEntry *entry = (AnserInboxEntry *) lfirst(lc); + + if (entry->payload != NULL) + pfree(entry->payload); + pfree(entry); + } + list_free(AnserInbox); + AnserInbox = NIL; +} + +void +AnserSidebandResetAll(void) +{ + AnserSidebandResetInbox(); + AnserDispatchReset(); +} + +/* + * Build a QE -> QD payload: fixed-width header, then the key, then the base64 + * body. See ansersideband.h for the layout and AnserWireParse() for the + * reader. + */ +char * +AnserWireFormat(const AnserChannelKey *channel_key, char kind, + char payload_type, uint32 part_index, uint32 total_parts, + int flags, const void *payload, Size payload_len) +{ + StringInfoData buf; + char hdr[ANSER_WIRE_HDR_LEN + 1]; + int hdrlen; + int keylen = (int) strlen(channel_key->condition_key); + int bodylen = 0; + char *body = NULL; + pg_crc32c crc; + + /* + * What the checksum covers has to be exactly what goes on the wire, so the + * bytes it will cover are recorded here, as the body is encoded, rather + * than re-derived from the arguments afterwards. Deriving them again would + * mean covering a payload that was suppressed -- a cancelled message + * carries no body however it was called -- and the reader, seeing no body, + * would compute a different checksum and discard a perfectly good message. + */ + const void *crc_body = NULL; + Size crc_body_len = 0; + + if (!(flags & ANSER_WIRE_F_CANCELLED) && payload != NULL && payload_len > 0) + { + int maxlen = pg_b64_enc_len((int) payload_len); + + if (maxlen > ANSER_WIRE_MAX_BODYLEN) + { + /* + * No header can describe a body this large. gp_anser_max_info_size + * keeps us far away from this, so it is a belt-and-braces check on + * the field width rather than a reachable path. + */ + bodylen = 0; + flags |= ANSER_WIRE_F_CANCELLED; + } + else + { + body = palloc(maxlen + 1); + bodylen = pg_b64_encode((const char *) payload, (int) payload_len, + body, maxlen); + if (bodylen < 0) + { + pfree(body); + body = NULL; + bodylen = 0; + flags |= ANSER_WIRE_F_CANCELLED; + } + else + { + crc_body = payload; + crc_body_len = payload_len; + } + } + } + + Assert(keylen <= ANSER_WIRE_MAX_KEYLEN); + + /* + * Format the header with a zero CRC, checksum it together with the key and + * the raw body, then overwrite the CRC field in place -- it sits at a fixed + * offset, so one pass suffices and the checksum still covers every header + * field. Checksumming the body before base64 rather than after means a + * mangled encoding is caught too, and whether the body is covered at all is + * the payload type's call (anserpayload.h). + */ + hdrlen = snprintf(hdr, sizeof(hdr), ANSER_WIRE_HDR_FORMAT, + kind, payload_type, + (uint32) channel_key->gp_session_id, + (uint32) channel_key->gp_command_count, + channel_key->condition_id, + part_index, total_parts, + (uint32) flags, (uint32) keylen, (uint32) bodylen, 0U); + if (hdrlen != ANSER_WIRE_HDR_LEN) + { + /* + * Cannot happen -- every field is width-limited -- but complain rather + * than throw: this is reached from producer teardown, and the + * coordinator's length cross-check will reject the message anyway, + * which costs the filter and not the query. + */ + elog(WARNING, "anser: formatted a %d byte header, expected %d", + hdrlen, ANSER_WIRE_HDR_LEN); + } + + INIT_CRC32C(crc); + COMP_CRC32C(crc, hdr, ANSER_WIRE_CRC_OFFSET); + COMP_CRC32C(crc, channel_key->condition_key, keylen); + if (crc_body != NULL && AnserPayloadChecksumsBody(payload_type)) + COMP_CRC32C(crc, crc_body, crc_body_len); + FIN_CRC32C(crc); + snprintf(hdr + ANSER_WIRE_CRC_OFFSET, 9, "%08x", (uint32) crc); + + initStringInfo(&buf); + appendBinaryStringInfo(&buf, hdr, ANSER_WIRE_HDR_LEN); + appendBinaryStringInfo(&buf, channel_key->condition_key, keylen); + if (bodylen > 0) + appendBinaryStringInfo(&buf, body, bodylen); + + if (body != NULL) + pfree(body); + + return buf.data; +} + +/* + * Checksum of a QD -> QE push. Lives here, on the reading side, but is called + * from anserdispatch.c too: one definition means the writer and the reader + * cannot disagree about what is covered -- including whether the body is, which + * follows from the payload type. Integers go in network byte order so the + * value does not depend on the host. + */ +pg_crc32c +AnserWirePushCrc(char payload_type, uint32 condition_id, uint32 flags, + const char *key, int keylen, const void *body, int bodylen) +{ + pg_crc32c crc; + uint32 fields[4]; + uint32 belen; + + fields[0] = pg_hton32((uint32) (unsigned char) payload_type); + fields[1] = pg_hton32(condition_id); + fields[2] = pg_hton32(flags); + fields[3] = pg_hton32((uint32) keylen); + + INIT_CRC32C(crc); + COMP_CRC32C(crc, fields, sizeof(fields)); + if (keylen > 0) + COMP_CRC32C(crc, key, keylen); + belen = pg_hton32((uint32) bodylen); + COMP_CRC32C(crc, &belen, sizeof(belen)); + if (bodylen > 0 && AnserPayloadChecksumsBody(payload_type)) + COMP_CRC32C(crc, body, bodylen); + FIN_CRC32C(crc); + + return crc; +} + +/* + * Hand a payload to the coordinator. + * + * NotifyMyFrontEnd enforces no length limit of its own -- the ~8 KB cap + * applies to the SQL-level NOTIFY, which has to fit its queue page -- so a + * multi-megabyte part is fine here. The payload is base64, hence free of the + * NUL that would truncate it in pq_sendstring(). + */ +static bool +anser_sideband_send(const char *payload) +{ + if (whereToSendOutput != DestRemote) + return false; + + NotifyMyFrontEnd(ANSER_NOTIFY_CHANNEL, payload, gp_session_id); + pq_flush(); + return true; +} diff --git a/pom.xml b/pom.xml index cb2c25c20c4..be2742ae4a1 100644 --- a/pom.xml +++ b/pom.xml @@ -1284,6 +1284,9 @@ code or new licensing patterns. gpcontrib/reject_partition_fullscan/Makefile gpcontrib/reject_partition_fullscan/reject_partition_fullscan.control + gpcontrib/anser/anser.control + gpcontrib/anser/anser_test.control + diff --git a/src/backend/cdb/dispatcher/cdbdisp_async.c b/src/backend/cdb/dispatcher/cdbdisp_async.c index eb8e4714396..a2551658b86 100644 --- a/src/backend/cdb/dispatcher/cdbdisp_async.c +++ b/src/backend/cdb/dispatcher/cdbdisp_async.c @@ -116,6 +116,12 @@ static void cdbdisp_waitDispatchFinish_async(struct CdbDispatcherState *ds); static bool cdbdisp_checkForCancel_async(struct CdbDispatcherState *ds); static int *cdbdisp_getWaitSocketFds_async(struct CdbDispatcherState *ds, int *nsocks); +/* + * Lets an extension handle its own NOTIFY channels arriving from QEs; see the + * declaration in cdbdisp.h for the contract. + */ +cdbdisp_notify_hook_type cdbdisp_notify_hook = NULL; + DispatcherInternalFuncs DispatcherAsyncFuncs = { cdbdisp_checkForCancel_async, @@ -136,6 +142,8 @@ static void checkDispatchResult(CdbDispatcherState *ds, int timeout_sec); static bool processResults(CdbDispatchResult *dispatchResult); +static void processNotifies(CdbDispatchResult *dispatchResult); + static void signalQEs(CdbDispatchCmdAsync *pParms); @@ -988,6 +996,96 @@ send_sequence_response(PGconn *conn, Oid oid, int64 last, int64 cached, int64 in elog(ERROR, "Failed to send sequence response: %s", PQerrorMessage(conn)); } +/* + * Hand over any notifications this QE has sent us. + * + * Kept separate from processResults() because it must run on every path that + * consumed input, not only the one where the QE still has work to do. A + * notification that arrives in the same read as the QE's command completion is + * already parsed out of the socket and queued on the connection, but that + * connection is about to be dropped from the poll set (stillRunning goes + * false), so nothing would ever wake us for it again -- the notification would + * be silently lost. nextval() never hit this because its QE blocks for a + * reply, leaving the command incomplete; a fire-and-forget sender does hit it. + */ +static void +processNotifies(CdbDispatchResult *dispatchResult) +{ + SegmentDatabaseDescriptor *segdbDesc = dispatchResult->segdbDesc; + + PGnotify *qnotifies = PQnotifies(segdbDesc->conn); + while(qnotifies && elog_geterrcode() == 0) + { + if (strcmp(qnotifies->relname, CDB_NOTIFY_NEXTVAL) == 0) + { + /* + * If there was nextval request then respond back on this libpq + * connection with the next value. Check and process nextval + * message only if QD has not already hit the error. Since QD could + * have hit the error while processing the previous nextval_qd() + * request itself and since full error handling is not complete yet + * (ex: releasing all the locks, etc.), shouldn't attempt to call + * nextval_qd() again. + */ + + CHECK_FOR_INTERRUPTS(); + + int64 last; + int64 cached; + int64 increment; + bool overflow; + Oid dbid; + Oid seq_oid; + + if (sscanf(qnotifies->extra, "%u:%u", &dbid, &seq_oid) != 2) + elog(ERROR, "invalid nextval message"); + + if (dbid != MyDatabaseId) + elog(ERROR, "nextval message database id:%u doesn't match my database id:%u", + dbid, MyDatabaseId); + + PG_TRY(); + { + nextval_qd(seq_oid, &last, &cached, &increment, &overflow); + } + PG_CATCH(); + { + send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, true /* error */); + PG_RE_THROW(); + } + PG_END_TRY(); + /* respond back on this libpq connection with the next value */ + send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, false /* error */); + } + else if (strcmp(qnotifies->relname, CDB_NOTIFY_ENDPOINT_ACK) == 0) + { + qnotifies->next = (struct pgNotify *) dispatchResult->ackPGNotifies; + dispatchResult->ackPGNotifies = qnotifies; + + /* Don't free the notify here since it in queue now */ + qnotifies = NULL; + } + else if (cdbdisp_notify_hook != NULL && + cdbdisp_notify_hook(dispatchResult, qnotifies)) + { + /* Consumed by an extension; nothing further to do here. */ + } + else + { + /* Got an unknown PGnotify, just record it in log */ + if (qnotifies->relname) + elog(LOG, "got an unknown notify message : %s", qnotifies->relname); + } + + if (qnotifies) + PQfreemem(qnotifies); + qnotifies = PQnotifies(segdbDesc->conn); + } + + forwardQENotices(); + +} + /* * Receive and process input from one QE. * @@ -1058,7 +1156,12 @@ processResults(CdbDispatchResult *dispatchResult) if (!pRes) { ELOG_DISPATCHER_DEBUG("%s -> idle", segdbDesc->whoami); - /* this is normal end of command */ + /* + * Normal end of command. Take any notifications with us: this + * connection is about to leave the poll set, so this is the last + * chance to see them. + */ + processNotifies(dispatchResult); return true; } @@ -1155,72 +1258,7 @@ processResults(CdbDispatchResult *dispatchResult) } forwardQENotices(); - - PGnotify *qnotifies = PQnotifies(segdbDesc->conn); - while(qnotifies && elog_geterrcode() == 0) - { - if (strcmp(qnotifies->relname, CDB_NOTIFY_NEXTVAL) == 0) - { - /* - * If there was nextval request then respond back on this libpq - * connection with the next value. Check and process nextval - * message only if QD has not already hit the error. Since QD could - * have hit the error while processing the previous nextval_qd() - * request itself and since full error handling is not complete yet - * (ex: releasing all the locks, etc.), shouldn't attempt to call - * nextval_qd() again. - */ - - CHECK_FOR_INTERRUPTS(); - - int64 last; - int64 cached; - int64 increment; - bool overflow; - Oid dbid; - Oid seq_oid; - - if (sscanf(qnotifies->extra, "%u:%u", &dbid, &seq_oid) != 2) - elog(ERROR, "invalid nextval message"); - - if (dbid != MyDatabaseId) - elog(ERROR, "nextval message database id:%u doesn't match my database id:%u", - dbid, MyDatabaseId); - - PG_TRY(); - { - nextval_qd(seq_oid, &last, &cached, &increment, &overflow); - } - PG_CATCH(); - { - send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, true /* error */); - PG_RE_THROW(); - } - PG_END_TRY(); - /* respond back on this libpq connection with the next value */ - send_sequence_response(segdbDesc->conn, seq_oid, last, cached, increment, overflow, false /* error */); - } - else if (strcmp(qnotifies->relname, CDB_NOTIFY_ENDPOINT_ACK) == 0) - { - qnotifies->next = (struct pgNotify *) dispatchResult->ackPGNotifies; - dispatchResult->ackPGNotifies = qnotifies; - - /* Don't free the notify here since it in queue now */ - qnotifies = NULL; - } - else - { - /* Got an unknown PGnotify, just record it in log */ - if (qnotifies->relname) - elog(LOG, "got an unknown notify message : %s", qnotifies->relname); - } - - if (qnotifies) - PQfreemem(qnotifies); - qnotifies = PQnotifies(segdbDesc->conn); - } - - forwardQENotices(); + processNotifies(dispatchResult); return false; /* we must keep on monitoring this socket */ } 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/tcop/postgres.c b/src/backend/tcop/postgres.c index 8eba8a6d227..213b5daa398 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -595,6 +595,11 @@ SocketBackend(StringInfo inBuf) doing_extended_query_message = false; break; + case GP_SIDEBAND_MESSAGE: /* Cloudberry QD -> QE sideband data */ + maxmsglen = PQ_LARGE_MESSAGE_LIMIT; + doing_extended_query_message = false; + break; + default: /* @@ -6417,6 +6422,18 @@ PostgresMain(const char *dbname, const char *username) */ break; + case GP_SIDEBAND_MESSAGE: /* Cloudberry QD -> QE sideband data */ + /* + * Accept but ignore this message. A QE reads sideband data + * explicitly at the point it expects it; reaching the command + * loop means nobody is waiting for it any more -- typically + * the waiter was interrupted (query cancel) after the QD had + * already written the message. Same reasoning as '?' above, + * but a wider window, since a sideband waiter may block for + * the lifetime of a query. + */ + break; + default: ereport(FATAL, (errcode(ERRCODE_PROTOCOL_VIOLATION), diff --git a/src/include/cdb/cdbdisp.h b/src/include/cdb/cdbdisp.h index 9fac725c5fd..3c7179fffe6 100644 --- a/src/include/cdb/cdbdisp.h +++ b/src/include/cdb/cdbdisp.h @@ -22,11 +22,34 @@ #define CDB_MOTION_LOST_CONTACT_STRING "Interconnect error master lost contact with segment." struct CdbDispatchResults; /* #include "cdb/cdbdispatchresult.h" */ +struct CdbDispatchResult; /* #include "cdb/cdbdispatchresult.h" */ struct CdbPgResults; struct Gang; /* #include "cdb/cdbgang.h" */ struct ResourceOwnerData; +struct pgNotify; /* #include "libpq-fe.h" */ enum GangType; +/* + * Hook for an extension to consume a NOTIFY sent by a QE on its dispatch + * connection, alongside the sequence and endpoint-ack channels the dispatcher + * handles itself. Return true if the notify was consumed, false to let the + * dispatcher fall through to its own handling. + * + * 'dispatchResult' identifies the sending QE and, through its meleeResults, + * gives access to every connection of the current dispatch -- which is what + * lets a handler answer on other QEs' connections (see GP_SIDEBAND_MESSAGE). + * + * The hook runs inside the QD's dispatch-result processing, which is reached + * from the interconnect wait loop while the query is executing. It must + * therefore be quick, must not re-enter the dispatcher, and should not throw + * for conditions it can recover from: an error here propagates into the + * running query. The PGnotify belongs to the caller and is freed after the + * hook returns, so a handler must copy anything it wants to keep. + */ +typedef bool (*cdbdisp_notify_hook_type) (struct CdbDispatchResult *dispatchResult, + struct pgNotify *notify); +extern PGDLLIMPORT cdbdisp_notify_hook_type cdbdisp_notify_hook; + /* * Types of message to QE when we wait for it. */ diff --git a/src/include/cdb/cdbvars.h b/src/include/cdb/cdbvars.h index b06f3480938..7ac077b0213 100644 --- a/src/include/cdb/cdbvars.h +++ b/src/include/cdb/cdbvars.h @@ -805,6 +805,19 @@ extern const char * lookup_autostats_mode_by_value(GpAutoStatsModeValue val); */ #define CDB_NOTIFY_ENDPOINT_ACK "ack_notify" +/* + * Message type for QD -> QE sideband data sent on the dispatch connection + * outside the normal query protocol, in the same spirit as the '?' sequence + * response. The QE reads it explicitly where it expects it; a message that + * arrives unexpectedly (typically because the QE was cancelled while waiting + * for one) is accepted and discarded by the command loop, so it cannot break + * message-boundary sync. + * + * Unlike '?', a sideband message may be large: it is read with + * PQ_LARGE_MESSAGE_LIMIT. + */ +#define GP_SIDEBAND_MESSAGE '!' + typedef enum WarehouseStatus { WAREHOUSE_STATUS_CREATING, diff --git a/src/include/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 */