This is an automated email from the ASF dual-hosted git repository.
lhotari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new ddd82dbd99d [improve][misc] Add AGENTS.md and split
build/contributor/coding/architecture/security docs (#25871)
ddd82dbd99d is described below
commit ddd82dbd99d6f1bb19f68d6a95e758311efee3ce
Author: Lari Hotari <[email protected]>
AuthorDate: Wed May 27 22:59:45 2026 +0300
[improve][misc] Add AGENTS.md and split
build/contributor/coding/architecture/security docs (#25871)
---
.github/PULL_REQUEST_TEMPLATE.md | 18 +-
.github/copilot-instructions.md | 329 +---------------------
.github/instructions/gradle-build.instructions.md | 10 +
.github/instructions/java-coding.instructions.md | 10 +
AGENTS.md | 81 ++++++
ARCHITECTURE.md | 150 ++++++++++
CLAUDE.md | 1 +
CODING.md | 319 +++++++++++++++++++++
CONTRIBUTING.md | 214 +++++++++++++-
README.md | 64 ++---
SECURITY.md | 101 ++++++-
11 files changed, 913 insertions(+), 384 deletions(-)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 69281b4a03b..9772d920173 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,18 +1,26 @@
<!--
### Contribution Checklist
-
- - PR title format should be *[type][component] summary*. For details, see
*[Guideline - Pulsar PR Naming
Convention](https://pulsar.apache.org/contribute/develop-semantic-title/)*.
+
+ - PR title format should be *[type][component] summary*. The valid `[type]`
and `[component]`/scope
+ prefixes are enforced by CI (see
`.github/workflows/ci-semantic-pull-request.yml`); for details,
+ see *[Guideline - Pulsar PR Naming
Convention](https://pulsar.apache.org/contribute/develop-semantic-title/)*.
- Fill out the template below to describe the changes contributed by the
pull request. That will give reviewers the context they need to do the review.
-
+
+ - The **Motivation** and **Modifications** sections are required: explain
*why* (the problem/context)
+ and *what/how* (the change). A title alone, or a description that only
restates the title, is not enough.
+
- Each pull request should address only one issue, not mix up code from
multiple issues.
-
+
- Each commit in the pull request has a meaningful commit message
+ - For the local build/test/PR workflow see `CONTRIBUTING.md`, and for coding
conventions see
+ `CODING.md`. If you use an AI coding assistant, see `AGENTS.md`.
+
- Once all items of the checklist are addressed, remove the above text and
this checklist, leaving only the filled out template below.
-->
-<!-- Either this PR fixes an issue, -->
+<!-- Either this PR fixes/closes an issue — use "Fixes #xyz" or, equivalently,
"Closes #xyz", -->
Fixes #xyz
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
deleted file mode 100644
index 54d832eefd0..00000000000
--- a/.github/copilot-instructions.md
+++ /dev/null
@@ -1,328 +0,0 @@
-The following instructions are only to be applied when performing a code
review.
-
-# Copilot Instructions for Apache Pulsar
-
-## Project Context
-
-Apache Pulsar is a distributed messaging and streaming platform designed for
high throughput, low latency, and
-horizontal scalability.
-
-The codebase contains performance-critical, asynchronous, and
concurrency-sensitive components such as brokers, storage
-clients, and networking layers.
-
-Code reviews should prioritize:
-
-* correctness
-* thread safety
-* performance
-* maintainability
-* backward compatibility
-
----
-
-# Java Coding Conventions
-
-Apache Pulsar follows the Sun Java Coding Conventions with additional
project-specific rules.
-
-## Formatting
-
-* Use **4 spaces** for indentation.
-* **Tabs must never be used**.
-* Always use **curly braces**, even for single-line `if` statements.
-
-Example:
-
-```java
-if (condition) {
- doSomething();
-}
-```
-
-## Javadoc
-
-* Do **not include `@author` tags** in Javadoc.
-
-## TODO Comments
-
-All TODO comments must reference a GitHub issue.
-
-Example:
-
-```java
-// TODO: https://github.com/apache/pulsar/issues/XXXX
-```
-
----
-
-# Dependencies
-
-Prefer existing dependencies instead of introducing new libraries.
-
-The Pulsar codebase commonly uses:
-
-* **Apache Commons libraries or Guava** for utilities
-* **FastUtil** for optimized type specific collections
-* **JCTools** for high performance concurrent data structures
-* **RoaringBitmap** for compressed bitmaps (bitsets)
-* **Caffeine** for caching
-* **Jackson** for JSON handling
-* **Prometheus Java simpleclient** (or newer **Prometheus Java Metrics
Library**) for metrics
-* **OpenTelemetry API** for metrics
-* **Netty** for networking and buffers
-
-When introducing a new dependency:
-
-* justify why existing dependencies are insufficient
-* ensure required license files and notices are updated
-
----
-
-# Logging Guidelines
-
-* Use **SLF4J** for logging.
-* Do **not use** `System.out` or `System.err`.
-* Assume production commonly runs at **INFO** log level.
-* Avoid excessive logging in hot paths.
-* Guard expensive `DEBUG` and `TRACE` logging with `log.isDebugEnabled()` or
`log.isTraceEnabled()`.
-* Avoid logging stack traces at `INFO` and lower levels.
-
-Log messages should be:
-
-* clear and descriptive
-* capitalized
-* understandable without reading the code
-
----
-
-# Resource and Memory Management
-
-Ensure resources are always closed correctly.
-
-Prefer:
-
-```java
-try (InputStream in = ...) {
- // use resource
-}
-```
-
-Avoid leaks for:
-
-* streams
-* network connections
-* executors
-* buffers
-
-For internal networking/messaging paths, prefer **Netty `ByteBuf`** over
`ByteBuffer` unless an external API requires
-`ByteBuffer`.
-
----
-
-# Configuration Guidelines
-
-When adding configuration options:
-
-* use clear and descriptive names
-* provide sensible default values
-* update default configuration files
-* document the configuration option
-
----
-
-# Concurrency Guidelines
-
-Pulsar is designed as a low-latency asynchronous system.
-
-Verify:
-
-* public classes are **thread-safe**
-* shared mutable state is protected
-* mutations occur on the intended thread
-* fine-grained synchronization is preferred
-* threads have meaningful names for diagnostics
-
-If a class is not thread-safe, annotate it with:
-
-```java
-@NotThreadSafe
-```
-
-Prefer **OrderedExecutor** for ordered asynchronous execution.
-
----
-
-# Asynchronous Programming Guidelines
-
-Pulsar relies heavily on `CompletableFuture` and asynchronous pipelines.
-
-Prefer `CompletableFuture` APIs over `ListenableFuture` for new code.
-
-## Avoid Blocking in Async Paths
-
-Do not introduce blocking operations in asynchronous execution paths.
-
-Examples of blocking operations:
-
-* `Thread.sleep`
-* `Future.get()`
-* `CompletableFuture.join()`
-* blocking IO operations
-
-Blocking calls must not run on event loop or async execution threads.
-
-## Avoid Nested Futures
-
-Avoid returning nested futures such as:
-
-```java
-CompletableFuture<CompletableFuture<T>>
-```
-
-Prefer flattening with `thenCompose`.
-
-## Asynchronous Exception Handling
-
-Methods returning `CompletableFuture` must not throw synchronous exceptions
directly.
-
-Incorrect:
-
-```java
-public CompletableFuture<Void> operation() {
- if (error) {
- throw new IllegalStateException("unexpected state");
- }
- return CompletableFuture.completedFuture(null);
-}
-```
-
-Use returned futures to propagate failures:
-
-```java
-return CompletableFuture.failedFuture(exception);
-```
-
-This also applies to argument validation in async methods:
-
-```java
-if (arg == null) {
- return CompletableFuture.failedFuture(new IllegalArgumentException("arg"));
-}
-```
-
-Throwing exceptions inside async stages such as `thenApply`, `thenCompose`,
`thenRun`, `handle`, or `whenComplete`
-is acceptable:
-
-```java
-return future.thenApply(v -> {
- if (error) {
- throw new IllegalStateException("unexpected state");
- }
- return result;
-});
-```
-
----
-
-# Backward Compatibility
-
-Apache Pulsar maintains strong compatibility guarantees.
-
-Changes must not break:
-
-* public APIs
-* client compatibility
-* wire protocol compatibility
-* metadata or serialized formats
-
-Servers must be compatible with both older and newer clients.
-
-Flag any change that may break compatibility.
-
----
-
-# Testing Guidelines
-
-## Unit testing
-
-* TestNG is used as the testing framework
-* Mocking uses Mockito
-* Assertions should prefer using AssertJ library with descriptions over using
TestNG assertions
-* Awaitility should be used to handle assertions with asynchronous conditions
together with AssertJ
-
-## Avoid Reflection in Tests
-
-Do **not** use reflection to access private fields or methods from tests. This
includes
-`WhiteboxImpl.getInternalState`, `WhiteboxImpl.setInternalState`,
`Field.setAccessible(true)`,
-`Method.setAccessible(true)`, and similar reflection helpers.
-
-Reflection-based test access is discouraged because it:
-
-* breaks during refactoring without any compile-time signal — renaming or
retyping a field
- silently invalidates the test
-* produces verbose, brittle, and hard-to-read test code
-* couples tests to implementation details that should be free to change
-
-Instead, expose what tests legitimately need through **package-private**
methods (or fields
-where appropriate) and annotate them with `@VisibleForTesting`:
-
-```java
-@VisibleForTesting
-Map<String, Subscription> getSubscriptions() {
- return subscriptions;
-}
-```
-
-The test then accesses state through a normal, statically-typed call:
-
-```java
-var subscriptions = persistentTopic.getSubscriptions();
-```
-
-instead of:
-
-```java
-ConcurrentOpenHashMap<String, PersistentSubscription> subscriptions =
- WhiteboxImpl.getInternalState(persistentTopic, "subscriptions");
-```
-
-Place the test in the same package as the class under test so package-private
visibility is
-sufficient — no need to widen visibility to `public` for testing.
-
-When reviewing PRs, flag any new use of reflection in tests and suggest a
`@VisibleForTesting`
-package-private accessor instead. See the dev@ thread
-[Stop using reflection to access private fields in
tests](https://lists.apache.org/thread/7gr04sqmzyttx4ln6ydtp3qv0xgo1o6m)
-for the full rationale.
-
-# Testing Expectations
-
-Every feature or bug fix should include tests.
-
-Verify:
-
-* unit tests exist
-* edge cases are covered
-* failure scenarios are tested
-* tests are deterministic and stable
-* tests avoid `sleep`-based timing assumptions
-* tests include timeouts to prevent hangs
-
-Integration tests may be required for distributed components.
-
----
-
-# Pull Request Review Guidance
-
-When reviewing a pull request, Copilot should:
-
-* verify Java coding conventions are followed
-* detect thread safety risks
-* flag blocking operations in async paths
-* detect improper `CompletableFuture` usage
-* detect unnecessary dependencies and missing license updates
-* ensure logging follows project guidelines
-* verify backward compatibility
-* suggest missing tests when appropriate
-* flag reflection-based access to private fields or methods in tests (e.g.
`WhiteboxImpl`,
- `setAccessible(true)`) and recommend a package-private `@VisibleForTesting`
accessor instead
-
-Focus feedback on correctness, reliability, and maintainability.
\ No newline at end of file
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 120000
index 00000000000..be77ac83a18
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+../AGENTS.md
\ No newline at end of file
diff --git a/.github/instructions/gradle-build.instructions.md
b/.github/instructions/gradle-build.instructions.md
new file mode 100644
index 00000000000..a5d5eca5ef7
--- /dev/null
+++ b/.github/instructions/gradle-build.instructions.md
@@ -0,0 +1,10 @@
+---
+description: Conventions for changing Apache Pulsar's Gradle build (build
scripts, version catalog, convention plugins).
+applyTo:
"**/*.gradle.kts,**/gradle.properties,gradle/libs.versions.toml,build-logic/**"
+---
+
+# Gradle build changes
+
+These files configure Apache Pulsar's **Gradle** build. Follow the guidance in
+[`ARCHITECTURE.md` → Changing the
build](../../ARCHITECTURE.md#changing-the-build) (and the
+surrounding *Build infrastructure* section) when editing or reviewing a build
file.
\ No newline at end of file
diff --git a/.github/instructions/java-coding.instructions.md
b/.github/instructions/java-coding.instructions.md
new file mode 100644
index 00000000000..10213079599
--- /dev/null
+++ b/.github/instructions/java-coding.instructions.md
@@ -0,0 +1,10 @@
+---
+description: Coding conventions for Apache Pulsar Java source and test code.
+applyTo: "**/*.java"
+---
+
+# Java code changes
+
+Apache Pulsar is performance-critical, asynchronous, and
concurrency-sensitive. The full reference is
+[`CODING.md`](../../CODING.md) (with
[`CONTRIBUTING.md`](../../CONTRIBUTING.md) for running tests) —
+follow it when editing or reviewing Java code.
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000000..e6479cf7229
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,81 @@
+# Agent guide for Apache Pulsar
+
+Supplemental guidance for AI coding assistants (Claude Code, Copilot, Cursor,
Gemini, Codex, Aider,
+and similar tools) working in this repository. Keep this file short — it is a
**router**. The detail
+lives in the human-facing docs linked below; read the one that fits your task
rather than pulling
+everything into context up front.
+
+Apache Pulsar is a distributed pub-sub messaging and streaming platform. The
codebase is
+performance-critical, heavily asynchronous, and concurrency-sensitive.
Prioritize **correctness,
+thread safety, performance, maintainability, and backward compatibility**.
+
+## Licensing and provenance (read first)
+
+Apache Pulsar is licensed under the Apache License 2.0, and all contributions
must meet the ASF's
+[Generative Tooling
guidance](https://www.apache.org/legal/generative-tooling.html):
+
+- **A human is in the loop and is accountable.** Every pull request and every
security report must be
+ submitted by a human contributor who has reviewed and verified the change
and takes responsibility
+ for it. Fully autonomous agents (e.g. OpenClaw-style bots) opening PRs or
filing reports on their own
+ are not acceptable — the AI assists, the human is accountable.
+- **Don't introduce code of incompatible or unknown provenance.** Never copy
verbatim from
+ GPL/AGPL/LGPL, proprietary, or unlicensed sources (including Stack Overflow
/ blog / forum snippets of
+ unclear licensing), and don't use a tool whose terms restrict the output
inconsistently with open
+ source. Reimplement from specifications or Apache-compatible sources, and
follow the
+ [ASF 3rd Party Licensing Policy](https://www.apache.org/legal/resolved.html).
+- **Every new source file needs the ASF license header** (Spotless enforces
this — copy the form from
+ an existing `.java` file).
+- **Consider attributing AI assistance.** When AI tooling assisted on a
change, consider adding an
+ `Assisted-by: <tool/version>` commit trailer (the default, since a human
still does the final
+ review); `Generated-by: <tool/version>` is for minimally-modified generated
output.
+
+## Canonical docs (read the one that fits the task)
+
+| Doc | Use for |
+|-----|---------|
+| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Local dev workflow: building
(prerequisites, build/lint commands), running tests & test groups, integration
tests, Personal CI, PR conventions, security reporting. |
+| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Big-picture module map, the Gradle
build infrastructure and how to change build files (convention plugins, version
catalog, the module-name-vs-directory gotcha), the concurrency model and
backpressure, and the `pip/` proposals. |
+| [`CODING.md`](CODING.md) | Coding conventions: style,
async/`CompletableFuture`, concurrency, logging
([slog](https://github.com/merlimat/slog)), dependencies, backward
compatibility, testing, and the review checklist. |
+| [`SECURITY.md`](SECURITY.md) | Reporting a vulnerability, disclosure
hygiene, and checking exposure to an already-public CVE. |
+
+The authoritative project documentation is at <https://pulsar.apache.org>,
whose source lives in the
+[`apache/pulsar-site`](https://github.com/apache/pulsar-site) repository
(where documentation changes
+are contributed). The files above and the website remain the source of truth —
this guide just layers
+AI-specific pointers on top.
+
+## Agent guardrails
+
+A few rules matter specifically when an AI tool makes the change, on top of
the canonical docs above:
+
+- **Verify, don't fabricate.** Before you use a class, method, field,
configuration key, Gradle task,
+ plugin, or DSL name, confirm it actually exists (search the code or the
build files) — don't invent
+ APIs, test assertions, or symbol names. Don't assert a CVE is "fixed" / "not
affected" without
+ checking the merged PRs and the shipped version.
+- **Confirm state-changing actions.** Get the user's explicit confirmation
before pushing, opening or
+ updating a PR, or posting a comment. Being asked to start a task is not
standing authorization for
+ these outward-facing actions — see *Licensing and provenance* above.
+- **A clean local run is weak evidence for concurrency/data race fixes**
(timing- and
+ platform-dependent). See
[`CODING.md`](CODING.md#reproducing-concurrency--memory-visibility-bugs).
+
+## Critical rules
+
+1. **Don't break backward compatibility** — public APIs, client compatibility,
wire protocol, and
+ serialized/metadata formats. Servers must interoperate with older and newer
clients.
+2. **Never block on async/event-loop threads;** methods returning
`CompletableFuture` must not throw
+ synchronously. See [`CODING.md`](CODING.md#asynchronous-programming).
+3. **Logging:** prefer [slog](https://github.com/merlimat/slog) via
`@CustomLog`; default new logs to
+ `TRACE`/`DEBUG`, not `INFO`.
+4. **Tests:** scope runs with `--tests`; no reflection into private state (use
a `@VisibleForTesting`
+ package-private accessor); release buffers and resources.
+5. **PRs:** semantic `[type][scope]` title; describe **motivation** and
**modifications**; do not
+ rebase once the PR is open in `apache/pulsar` — merge upstream `master`
instead.
+6. **Security:** never disclose a vulnerability — or the security nature of a
change — in a public
+ issue, PR, or commit. See [`SECURITY.md`](SECURITY.md).
+7. **Stay in scope.** Keep a change focused on its task; don't bundle
unrelated drive-by refactors or
+ generate broad mass-refactoring PRs. Discuss large refactorings on
`[email protected]` first.
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md#pull-requests).
+
+## Where to ask
+
+- Dev mailing list: <[email protected]> · Slack:
<https://apache-pulsar.slack.com/>
+- Issues: <https://github.com/apache/pulsar/issues> · Discussions:
<https://github.com/apache/pulsar/discussions>
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 00000000000..f4c9ae75a97
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,150 @@
+# Architecture
+
+Apache Pulsar is a distributed pub-sub messaging and streaming platform. The
codebase is
+performance-critical, heavily asynchronous, and concurrency-sensitive
(brokers, storage,
+networking).
+
+The authoritative documentation lives at <https://pulsar.apache.org> — see the
+[Architecture
Overview](https://pulsar.apache.org/docs/4.2.x/concepts-architecture-overview/)
for the
+conceptual model. For a deeper, generated architecture description see
+[DeepWiki](https://deepwiki.com/apache/pulsar); coding agents can install the
+[DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp) for richer
coverage of Pulsar's
+architecture. This file is a map of the repository for contributors (and AI
coding agents) who need to
+find their way around the modules quickly.
+
+## Big picture
+
+Pulsar separates a **stateless serving layer** (brokers) from **durable
storage** (Apache
+BookKeeper) and a **metadata store** (Oxia / ZooKeeper). The Gradle modules
layer
+accordingly:
+
+- **`pulsar-client-api`, `pulsar-client-admin-api`** — public,
backward-compatible interfaces only.
+ `pulsar-client-api-v5` / `pulsar-client-v5` are the newer V5 client API
(PIP-466/468).
+- **`pulsar-client` (`:pulsar-client-original`)** — the Java client
implementation
+ (producer/consumer/reader, connection pooling). `pulsar-client-admin`
implements the admin REST
+ client.
+- **`pulsar-common`** — wire protocol and shared types. Protobuf / lightproto
messages are
+ **generated** into `generated-lightproto/` / `generated-sources/` (excluded
from checkstyle and
+ spotless).
+- **`pulsar-metadata`** — pluggable metadata store abstraction (Oxia /
ZooKeeper, plus RocksDB and memory)
+ used by broker and bookkeeper.
+- **`managed-ledger`** — the storage abstraction over **Apache BookKeeper**:
append-only ledgers +
+ cursors that track consumer/subscription positions. This is the durability
layer the broker reads
+ and writes through.
+- **`pulsar-broker`** — the server. `PulsarService` is the composition root
wiring everything
+ together; `BrokerService` manages topics, subscriptions, and client
connections. Entry points:
+ `PulsarBrokerStarter` (broker), `PulsarStandalone` /
`PulsarStandaloneStarter` (all-in-one),
+ `PulsarClusterMetadataSetup` (cluster init).
+- **`pulsar-proxy`** — optional proxy/gateway in front of brokers.
+- **`pulsar-functions/*`** — serverless compute (Functions): `proto`,
`api-java`, `instance`,
+ `runtime`, `worker`, `localrun`.
+- **`pulsar-io/*`** — connector framework core only; most built-in connectors
were moved to the
+ separate `pulsar-connectors` repo (PIP-465).
+- **`pulsar-transaction/*`** — transaction coordinator and common types.
+- **`tiered-storage/*`, `offloaders/`** — offload ledger data to
cloud/filesystem storage.
+- **`pulsar-websocket`** — WebSocket-to-Pulsar bridge.
**`pulsar-client-tools`** — the
+ `pulsar-admin` / `pulsar-client` CLIs.
+- **Shaded / distribution** — `pulsar-client-shaded`, `pulsar-client-all`,
+ `pulsar-client-admin-shaded` produce relocated fat jars; `distribution/*`
assembles
+ server/shell/offloader tarballs.
+
+## Pulsar Improvement Proposals (`pip/`)
+
+The **`pip/`** directory holds **Pulsar Improvement Proposals** (`pip-<N>.md`)
— the design
+documents for significant changes, referenced as `PIP-<N>` throughout commit
messages and code (e.g.
+PIP-463 = Maven→Gradle migration, PIP-465 = IO connectors moved out,
PIP-466/468 = V5 client).
+`pip/README.md` describes the process and `pip/TEMPLATE.md` is the proposal
template. Consult the
+relevant PIP for the rationale behind a non-trivial feature or architectural
decision. A PIP **number
+is reserved by the first `[email protected]` thread that uses it** —
start the discussion to claim
+the next free number.
+
+## Concurrency model (a known gap)
+
+Pulsar does **not** have a clearly established, documented concurrency model,
which makes it hard to
+evaluate whether a given piece of code is correct by construction. (Contrast
Netty, which has a clear
+rule: all handling on the IO thread is non-blocking, which by extension means
avoiding synchronization
+and locks on that path.) Pulsar does not strictly follow such a rule; modern
JVMs and hardware
+optimize `synchronized` code well enough that this has not blocked high
performance, but it does make
+reasoning about correctness harder than it needs to be.
+
+Conventions that **should** be documented (and largely are not yet):
+
+- which work belongs on the network-connection **event loop** vs. other
threads;
+- how the various **thread pools** are intended to be used, and what kind of
work belongs on each;
+- how threads are expected to **hand off state** to each other;
+- when a `CompletableFuture`'s **completion thread should be switched** to
another thread, and which one;
+- **concurrency limits** for asynchronous tasks;
+- preferring the **single-writer principle** to avoid concurrent state
mutation.
+
+Until such a model is written down, follow the surrounding code's conventions
and the Java-Memory-Model
+rules in [`CODING.md`](CODING.md#concurrency). Once a model is defined, it
becomes far more tractable to
+"lift and shift" existing code toward it and enforce the rules consistently
rather than having each
+contributor rediscover the conventions case by case.
+
+## Backpressure
+
+Closely tied to the concurrency model is **backpressure** — how the system
avoids accepting more work
+than it can handle, particularly with respect to memory. The memory side is
described in
+[PIP-442 "Existing Broker Memory
Management"](pip/pip-442.md#existing-broker-memory-management). Broader
+backpressure (beyond memory) is not yet documented and would benefit from
being defined alongside the
+concurrency model.
+
+## Build infrastructure
+
+Apache Pulsar uses a **Gradle** build (migrated from Maven via PIP-463; some
older tooling and docs
+elsewhere still reference Maven). The wrapper `./gradlew` requires **JDK 21 or
25** (bytecode targets
+Java 17). See [`CONTRIBUTING.md` → Building](CONTRIBUTING.md#building) for the
build and lint commands.
+
+- `settings.gradle.kts` — all modules, organized in dependency tiers (Tier 0
has no internal deps,
+ higher tiers build on lower ones).
+- `build-logic/conventions/` — convention plugins (`pulsar.java-conventions`,
+ `pulsar.code-quality-conventions`, `pulsar.shadow-conventions`, etc.)
applied by modules. Shared
+ compile/test/dependency config lives here — edit it here rather than
duplicating across modules.
+- `gradle/libs.versions.toml` — version catalog (single source of truth for
dependency versions;
+ referenced as `libs.*` in build scripts).
+- `pulsar-dependencies` — enforced platform (BOM) pinning all dependency
versions; applied to every
+ module.
+
+The build enables both the **configuration cache**
(`org.gradle.configuration-cache=true`) and
+**configure-on-demand** (`org.gradle.configureondemand=true`).
+
+### Module name vs. directory name gotcha
+
+Several Gradle project paths do **not** match their directory because the
Maven artifactId is
+preserved. Most importantly:
+
+- Directory `pulsar-client/` → project **`:pulsar-client-original`**
+- Directory `pulsar-client-admin/` → project
**`:pulsar-client-admin-original`**
+- Directory `pulsar-functions/localrun/` → project
`:pulsar-functions:pulsar-functions-local-runner-original`
+
+Always use the Gradle project path (left of any `--tests`), e.g. `./gradlew
:pulsar-client-original:test`.
+Check `settings.gradle.kts` when a path is ambiguous.
+
+### Changing the build
+
+When editing `build-logic/`, `settings.gradle.kts`, a module
`build.gradle.kts`, `gradle.properties`,
+`gradle/libs.versions.toml`, or the `pulsar-dependencies` platform:
+
+- **Edit shared config in `build-logic/conventions/`**, not per-module.
+- **Versions come from `gradle/libs.versions.toml`** (`libs.*` /
`pulsar-dependencies`) — never
+ hardcode a version in a build script.
+- **Keep tasks configuration-cache and configure-on-demand compatible** (both
are enabled): no reading
+ of mutable state at execution time and no `Project` access in task actions —
use `Provider` / value
+ sources, and verify with `--configuration-cache`. Tasks reached by the
common flows (`assemble`,
+ `test`, `integrationTest`, `rat` / `spotlessCheck` / `checkstyle*`,
`checkBinaryLicense`, `docker*`)
+ must be compatible; one-off tooling tasks not part of those flows (e.g.
`verifyTestGroups`, ad-hoc
+ report tasks) may be exempt.
+- **Published modules must not depend on internal modules** at compile/runtime
scope — the artifact
+ would be unresolvable from Maven Central. A module is published only when it
applies
+ `pulsar.public-java-library-conventions`.
+- **After a dependency change**, run `./gradlew checkBinaryLicense` and update
the distribution
+ `LICENSE`/`NOTICE`; justify any genuinely new dependency (see
+ [`CODING.md` → Dependencies](CODING.md#dependencies)).
+- **Follow the [Gradle best
practices](https://docs.gradle.org/current/userguide/best_practices_index.html)**
+ — AI agents should read the
+ [AsciiDoc
source](https://github.com/gradle/gradle/blob/master/platforms/documentation/docs/src/docs/userguide/best-practices/best_practices_index.adoc),
+ which is plain text and cheaper to parse than the rendered HTML.
+
+Before finishing a build change, confirm the affected task and `./gradlew
help` run clean with
+`--configuration-cache`, and that `assemble` and `rat spotlessCheck
checkstyleMain checkstyleTest` pass
+(plus `checkBinaryLicense` if a dependency changed).
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 00000000000..47dc3e3d863
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/CODING.md b/CODING.md
new file mode 100644
index 00000000000..6623e342e75
--- /dev/null
+++ b/CODING.md
@@ -0,0 +1,319 @@
+# Coding guidelines
+
+Apache Pulsar follows the Sun Java Coding Conventions with additional
project-specific rules. The
+codebase is performance-critical, asynchronous, and concurrency-sensitive, so
code review prioritizes
+**correctness, thread safety, performance, maintainability, and backward
compatibility**. This file is
+the canonical coding reference for human contributors and AI coding agents;
see [`AGENTS.md`](AGENTS.md)
+for the agent-specific guardrails on top of it.
+
+## Style
+
+- **4 spaces** for indentation; **tabs must never be used**.
+- Always use **curly braces**, even for single-line `if` statements.
+- No `@author` tags in Javadoc.
+- Every `TODO` must reference a GitHub issue, e.g. `// TODO:
https://github.com/apache/pulsar/issues/XXXX`.
+- Checkstyle config: `buildtools/src/main/resources/pulsar/checkstyle.xml`.
Lombok is enabled.
+
+## Logging
+
+- Prefer **[slog](https://github.com/merlimat/slog)**
(`io.github.merlimat.slog`) via Lombok's
+ **`@CustomLog`** (wired in `lombok.config` to `Logger.get(TYPE)`). **SLF4J
is deprecated** for new
+ code; never use `System.out` / `System.err`.
+- **Default new logs to `TRACE`/`DEBUG`, not `INFO`** — Pulsar overuses `INFO`
and floods production
+ logs. Reserve `INFO` for low-frequency lifecycle/state-change events.
+- Attach data as **structured attributes** — `log.info().attr("topic",
topic).log("Published")` — not
+ interpolated into the message string.
+- For expensive `DEBUG`/`TRACE` values, don't guard with
`isDebugEnabled()`/`isTraceEnabled()`; use
+ slog's lazy form — `log.debug().attr("dump", () ->
expensiveDump()).log("...")` or
+ `log.debug(e -> e.attr("dump", expensiveDump()).log("..."))`.
+- Avoid logging on hot paths, and stack traces at `INFO` or lower.
+- Use `DEBUG` in a way where it could be enabled in production without causing
too many log entries. Use `TRACE` for more detailed information.
+
+## Asynchronous programming
+
+Pulsar relies heavily on `CompletableFuture`; prefer it over
`ListenableFuture` for new code.
+
+- **A method returning `CompletableFuture` must not throw synchronously.**
Propagate failures through
+ the returned future — `return CompletableFuture.failedFuture(e);` —
including for argument validation
+ (`if (arg == null) return CompletableFuture.failedFuture(new
IllegalArgumentException("arg"));`).
+ Throwing *inside* a stage (`thenApply`, `thenCompose`, `handle`,
`whenComplete`, …) is fine.
+
+ Avoid (escapes synchronously; a caller chaining `.exceptionally(...)` never
sees it):
+
+ ```java
+ CompletableFuture<T> process(String arg) {
+ if (arg == null) {
+ throw new IllegalArgumentException("arg");
+ }
+ return doProcessAsync(arg);
+ }
+ ```
+
+ Prefer (report the validation failure through the returned future):
+
+ ```java
+ CompletableFuture<T> process(String arg) {
+ if (arg == null) {
+ return CompletableFuture.failedFuture(new
IllegalArgumentException("arg"));
+ }
+ return doProcessAsync(arg);
+ }
+ ```
+- **Never block on event-loop / async-execution threads** — no `Thread.sleep`,
`Future.get()`,
+ `CompletableFuture.join()`, or blocking IO. An operation that performs IO
should return a future.
+- **Avoid nested futures** (`CompletableFuture<CompletableFuture<T>>`);
flatten with `thenCompose`.
+ Prefer **`OrderedExecutor`** for ordered asynchronous work.
+
+ Avoid (`thenApply` on a future-returning function yields
`CompletableFuture<CompletableFuture<R>>`):
+
+ ```java
+ return firstAsync(arg).thenApply(v -> secondAsync(v));
+ ```
+
+ Prefer (`thenCompose` flattens it to `CompletableFuture<R>`):
+
+ ```java
+ return firstAsync(arg).thenCompose(v -> secondAsync(v));
+ ```
+- **Converting a synchronous-throwing method to a failed future is not
mechanical** — some callers rely
+ on the throw happening *before* the async work starts, so evaluate each call
site. Use a shared
+ `checkArgumentAsync` helper (in `FutureUtil`) to validate without
duplicating try/catch.
+
+- **Limit concurrency and handle backpressure.** Firing many async operations
at once can overwhelm the
+ system. Options:
+ - **`com.spotify.futures.ConcurrencyReducer`** — caps in-flight futures at a
configurable limit (used
+ in the Admin client to bound concurrent requests per broker).
+ - **`org.apache.pulsar.common.util.FutureUtil.Sequencer`** — runs async
operations sequentially.
+ - **`org.apache.pulsar.common.semaphore.AsyncSemaphoreImpl`** — a
non-blocking semaphore with a
+ per-operation cost that queues callers instead of failing when the limit
is reached. Preferred over
+ `ConcurrencyReducer` for request-driven cases that need a timeout on
permit acquisition.
+
+## Testing conventions
+
+Most Pulsar **"unit tests"** (`src/test`, run with `./gradlew :<module>:test`)
are actually
+**integration-style** — they start a real in-JVM broker
(`MockedPulsarServiceBaseTest` /
+`pulsarTestContext`) rather than testing a class in isolation. The **container
integration tests**
+under `tests/` run against a Pulsar Docker image (see
+[`CONTRIBUTING.md`](CONTRIBUTING.md#integration-tests)). Ideally code is
factored so genuine units *can*
+be unit-tested in isolation with light mocking — excessive mocking is a design
smell, not the goal —
+but much existing code isn't, so integration-style is the pragmatic default.
See
+[`CONTRIBUTING.md`](CONTRIBUTING.md) for how to *run* tests (groups, `--tests`
scoping, retry count).
+
+- **TestNG + Mockito.** Prefer **AssertJ** assertions (with descriptions) over
TestNG asserts; use
+ **Awaitility** for async conditions instead of `sleep` timing, with timeouts
to prevent hangs.
+ `untilAsserted(...)` retries assertions, `until(...)` waits for a boolean —
don't swap them. Verify
+ async interactions with Mockito `timeout(...)`, not fixed sleeps.
+- Every feature or bug fix needs **deterministic** tests for edge and failure
cases. A bug-fix test
+ must **fail on the unpatched code for the real reason** — not because it
forces internal state.
+- For code not factored for isolation, prefer an integration-style test over
mocking a web of
+ collaborators: inject faults via the test infrastructure (e.g.
+ `pulsarTestContext.getMockBookKeeper().setReadHandleInterceptor(...)`) and
assert on logs with
+ `TestLogAppender`. It's fine to add a **clean new test class** rather than
extend an awkward one.
+- **No reflection into private state**
(`WhiteboxImpl.getInternalState`/`setInternalState`,
+ `setAccessible(true)`). Expose a **package-private `@VisibleForTesting`**
accessor and put the test in
+ the same package; flag new reflection in review ([dev@
rationale](https://lists.apache.org/thread/7gr04sqmzyttx4ln6ydtp3qv0xgo1o6m)).
+- **New integration-style tests: extend `SharedPulsarBaseTest`.** It shares
one `SharedPulsarCluster`
+ for the test-JVM lifecycle (`admin` / `pulsarClient` are per test class);
each method gets its own
+ namespace. Use `getNamespace()` and `newTopicName()` — never hardcode
namespace/topic names, since
+ the runtime is shared.
+- **Close/release what the test allocates.** A **`ByteBuf`/buffer leak**
(pooled-allocator detection,
+ `-Dpulsar.allocator.pooled=true`) is a **real bug** — fix the missing
`release()`. A **thread leak
+ from `ThreadLeakDetectorListener` is unreliable** (high false-positive rate,
notably with
+ `SharedPulsarBaseTest` and when `THREAD_LEAK_DETECTOR_WAIT_MILLIS` is too
low — ≈`10000` recommended,
+ only effective with the Gradle daemon disabled, `--no-daemon`); corroborate
before treating it as
+ real.
+- **Validate performance optimizations with a JMH benchmark** under
`microbench/`, simulating a
+ realistic production usage pattern (see `microbench/README.md`).
+
+## General recommendations
+
+- **Use the narrowest interface type** for fields, parameters, variables, and
returns (`Map`,
+ `SequencedMap`, `SortedMap`, `Collection`, `List`) rather than a concrete
type like `TreeMap`. Keep
+ the concrete type only where its behaviour is required (e.g. a `TreeMap` for
key-ordered iteration),
+ still exposed through the interface.
+- **Minimize method and constructor parameters.** For a constructor with many
parameters,
+ use a **builder** — the project uses Lombok `@Builder` for most internal
classes, and it works on a
+ `record` too. Consider refactoring by moving related methods to a separate
class when it's a better fit.
+- **Don't return generic tuples.** Instead of
`org.apache.commons.lang3.tuple.Pair<L, R>` (or a similar
+ tuple type), define a small, purpose-named **Java `record`** inline in the
class that declares the
+ method, with the **same visibility as the method** (`public`,
package-private, or `private`).
+
+ Avoid (positional and untyped; call sites read `getLeft()` / `getRight()`):
+
+ ```java
+ private Pair<Integer, Integer> minMax(Collection<Integer> values) { ... }
+ ```
+
+ Prefer (a purpose-named record with the same visibility as the method):
+
+ ```java
+ private record MinMax(int min, int max) {}
+ private MinMax minMax(Collection<Integer> values) { ... }
+ ```
+- **Prefer record keys over concatenated strings.** For a composite `Map` key,
use a small `record`
+ instead of concatenating a `String` (e.g. `a + ":" + b`) — correct
`equals`/`hashCode`, type-safe,
+ no delimiter/escaping bugs.
+
+ Avoid (delimiter collisions when a value contains `:`; no type safety):
+
+ ```java
+ Map<String, V> map = new HashMap<>();
+ map.get(a + ":" + b);
+ ```
+
+ Prefer (a small record key with correct `equals`/`hashCode`):
+
+ ```java
+ record Key(String a, String b) {}
+ Map<Key, V> map = new HashMap<>();
+ map.get(new Key(a, b));
+ ```
+- **Don't use `@Builder` on public client-API classes** (harder to maintain
backwards compatibility) — hand-write the builder.
+- **Name methods for intent.** A method's name should reveal what it does.
Query methods read like
+ queries (`shouldSkipChunk`, not `skipChunk`); methods that mutate state or
perform an action are
+ named for that action. **Reserve the `get` prefix for pure queries** — using
it for a method that
+ mutates state, or otherwise does more than return a value is strongly
discouraged.
+
+## Dependencies
+
+Prefer existing dependencies over new libraries. Pulsar commonly uses Apache
Commons / Guava
+(utilities), **FastUtil** (type-specific collections), **JCTools** (concurrent
structures),
+**RoaringBitmap** (compressed bitsets), **Caffeine** (caching), **Jackson**
(JSON), Prometheus /
+**OpenTelemetry** (metrics), and **Netty** (networking and buffers).
+
+A new dependency must be justified (why existing ones are insufficient) and
must update the
+bundled-dependency `LICENSE`/`NOTICE` — verify with `./gradlew
checkBinaryLicense`.
+
+## Backward compatibility
+
+Pulsar maintains strong compatibility guarantees. Changes must not break
public APIs, client
+compatibility, wire-protocol compatibility, or serialized/metadata formats —
servers must work with
+both older and newer clients. Flag any change that may break compatibility.
+
+**Plugin / SPI extension points are public API.** Many interfaces are selected
by a `*ClassName`
+configuration setting — e.g. `LoadManager`, `LedgerOffloaderFactory`,
`AuthorizationProvider` /
+`AuthenticationProvider`, `EntryFilter`, `TopicFactory`, `BrokerInterceptor`,
dispatcher /
+delayed-delivery-tracker factories, `CustomCommand` — and third parties ship
implementations. Changing
+such an interface, or a `protected` member of an extensible class
(`PulsarWebResource`,
+`PersistentTopic`, `Producer`), breaks them: it generally needs a PIP and must
not land in
+maintenance-branch backports.
+
+**Design interface changes for backward compatibility.** When you add a method
to such an interface,
+prefer a `default` implementation that delegates to an existing method, so
older third-party
+implementations keep working unchanged. If no sensible delegation exists, add
a separate
+capability-query method (e.g. `boolean supportsX()`) the broker checks at
runtime, so it can support
+older implementations gracefully instead of depending on the new method.
+
+**Don't leak third-party types through public/plugin interfaces.** Exposing
Netty or AsyncHttpClient
+classes breaks consumers of the **shaded** client (shaded vs. unshaded classes
differ) and couples
+callers to the implementation — provide a Pulsar-owned abstraction. Changing a
documented behaviour or
+guarantee (e.g. PIP-68 exclusive-producer guarantees, default rate-limiter
behaviour) needs a PIP and a
+dev@ discussion, not just a code change.
+
+**Introduce changes behind a backward-compatible default.** Make new/changed
behaviour opt-in via
+configuration rather than silently changing existing deployments. Behaviour
that risks data loss (e.g.
+skipping unrecoverable data) must be gated behind an explicit flag (such as
`autoSkipNonRecoverableData`),
+defaulting to the safe/old behaviour.
+
+## Resource and memory management
+
+- Always close resources (streams, connections, executors, buffers) — prefer
try-with-resources.
+- On internal networking/messaging paths, prefer **Netty `ByteBuf`** over
`ByteBuffer` unless an
+ external API requires it; release ref-counted buffers you allocate.
+- **Don't hand-optimize allocation away.** Pulsar runs on **ZGC** (very low
collection overhead), so
+ the extra short-lived allocations from favouring immutable objects (see
*Concurrency* below) are
+ cheap. Older code pools objects with Netty's `Recycler`; this is **no longer
recommended for new
+ code** — under ZGC the `Recycler` often *costs* more CPU than it saves.
Don't add new `Recycler`
+ usage. See [PIP-443](pip/pip-443.md).
+
+## Performance
+
+- **Back optimizations with evidence** — a JMH benchmark (see *Testing
conventions*) or a profile, not
+ intuition — measured on JIT-warmed code (see *Reproducing concurrency /
memory-visibility bugs*).
+- **On hot paths** (dispatch, IO, per-message): avoid `String.format` (build
strings directly),
+ `Enum.values()` (match explicitly), and unnecessary allocation/locking;
prefer lock-free or
+ single-writer designs.
+- **Don't add overhead to an already-overloaded system.** Avoid doing work
then discarding it (e.g.
+ reading entries only to drop them before dispatch) — extra work under load
causes cascading failures;
+ acquire/estimate up front and reconcile afterwards.
+- **Bound in-memory caches** (size or byte limit + eviction) and de-duplicate
repeated `String`s
+ (cluster/tenant/namespace ids) with
`org.apache.pulsar.common.util.StringInterner`.
+
+## Configuration
+
+When adding configuration options: use clear, descriptive names; provide
sensible defaults; update the
+default configuration files; and document the option.
+
+## Code review checklist
+
+When reviewing a PR, verify:
+
+- Java coding conventions followed; logging follows the guidelines above
(slog, levels, structured
+ attributes).
+- Thread-safety risks; no blocking in async paths; correct `CompletableFuture`
usage.
+- No unnecessary dependencies; LICENSE/NOTICE updated when dependencies change.
+- Backward compatibility preserved.
+- Tests exist and are appropriate; reflection into private state is flagged
with a `@VisibleForTesting`
+ accessor suggested instead.
+- The **PR description explains the change** — at minimum **Motivation
(why?)** and **Modifications
+ (what/how?)**, matching `.github/PULL_REQUEST_TEMPLATE.md`; a title alone
isn't sufficient.
+
+Focus feedback on correctness, reliability, and maintainability.
+
+## Concurrency
+
+- Public classes should be **thread-safe**; annotate non-thread-safe ones with
`@NotThreadSafe`.
+- Protect shared mutable state; prefer fine-grained synchronization; mutate on
the intended thread.
+ Prefer the **single-writer principle** (a given piece of state mutated by
only one thread) to avoid
+ concurrent mutation entirely.
+- **Minimize work while holding a lock.** Capture needed state into locals
inside the synchronized
+ block, then run callbacks, listeners, and IO *outside* it — never call out
to listener/callback code
+ while holding a lock (this has fixed real deadlocks and contention).
+- Give threads **meaningful names**. When creating thread pools, prefer Netty's
+ **`io.netty.util.concurrent.DefaultThreadFactory`** — it produces
**`FastThreadLocalThread`**
+ instances (lower overhead `FastThreadLocal` lookups, which matter on Netty
paths like the pooled
+ `ByteBuf` allocator) and assigns prefixed thread names.
+
+Pulsar has no documented, project-wide concurrency model yet; see
+[`ARCHITECTURE.md` → Concurrency
model](ARCHITECTURE.md#concurrency-model-a-known-gap) for the
+conventions that *should* govern threads, thread pools, and event loops.
+
+### The Java Memory Model is what makes concurrent code correct
+
+Several hard-to-investigate Pulsar bugs have come from misconceptions about
Java synchronization:
+
+- **A `synchronized` method or block is not, on its own, thread-safe.** It
provides its
+ visibility/ordering guarantees only when the **same monitor/lock guards both
the reads and the
+ writes** of the shared state.
+- On 64-bit JVMs a field's value is **never corrupted** — a read returns some
value that was actually
+ written. What breaks is **visibility**: without a happens-before
relationship, threads can observe
+ different values, or never see an update. Establish happens-before with
`synchronized`, `volatile`,
+ `final`, or `java.util.concurrent` constructs.
+- **A field accessed by more than one thread needs explicit visibility** —
make it `volatile` (or
+ guard every read *and* write with the same lock). `volatile` gives
single-field visibility but does
+ **not** make compound updates (read-modify-write, check-then-act) atomic —
use `java.util.concurrent`
+ atomics/locks for those.
+- Visibility is per-field, so a mutable object can be observed **partially
updated**.
+- The only way to be reliably correct is to **conform to the Java Memory
Model**. **Benign data races**
+ are sometimes acceptable, and some Pulsar code relies on this by design —
but only as a deliberate,
+ documented choice.
+- **Prefer immutable objects.** An object is **immutable** when all fields are
`final` *and* every
+ nested instance is itself immutable (a `record` is the common case;
immutability must hold for the
+ whole reachable graph). It is **effectively immutable** when never modified
after construction but
+ with non-`final` fields. Publication differs: an **immutable** object
benefits from the JMM's
+ final-field **safe initialization** (visible even when published via a data
race) and needs **no**
+ safe publication; an **effectively immutable** one must be shared via **safe
publication** (a `final`
+ or `volatile` field, or a `java.util.concurrent` construct such as
`ConcurrentHashMap`). See
+ [Safe
initialization](https://shipilev.net/blog/2014/safe-public-construction/#_safe_initialization).
+
+### Reproducing concurrency / memory-visibility bugs
+
+These bugs are timing- and platform-dependent and easily masked, so a clean
run is weak evidence a fix
+is correct:
+
+- Interpreted and JIT-compiled code behave differently. Reproductions often
need several **warm-up
+ rounds with a short pause** so the (tiered, asynchronous) JIT kicks in; a
short test may never
+ trigger compilation. JVM flags can force earlier compilation, and the
exercised paths affect what
+ gets compiled.
+- Some races surface only on specific **hardware/OS** — classically
**multi-socket / multi-NUMA**
+ machines, whose weaker cross-socket memory ordering exposes races a single
socket never shows.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index db83924c47c..db70206a1f7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,6 +19,216 @@
-->
-## Contributing to Apache Pulsar
+# Contributing to Apache Pulsar
-We would love for you to contribute to Apache Pulsar and make it even better!
Please check the [Apache Pulsar Contributing
Guide](https://pulsar.apache.org/contribute/) before starting to work on the
project.
+We would love for you to contribute to Apache Pulsar and make it even better!
The **authoritative**
+contributor guide is the [Apache Pulsar Contributing
Guide](https://pulsar.apache.org/contribute/) —
+please read it before starting. This file is a quick, in-repo reference for
the local development
+workflow (build, test, PR, CI). For the big-picture module map and the Gradle
build infrastructure see
+[`ARCHITECTURE.md`](ARCHITECTURE.md); for code style see
[`CODING.md`](CODING.md).
+
+## Building
+
+**JDK 21 or 25** is required to build `master` (bytecode targets Java 17;
`-PskipJavaVersionCheck`
+bypasses the check); `zip` is also needed. Use the bundled wrapper `./gradlew`
(Linux/macOS) or
+`gradlew.bat` (Windows) — no separate Gradle install. See the
+[build-tooling setup
guide](https://pulsar.apache.org/contribute/setup-buildtools/) and the
+[IDE setup guide](https://pulsar.apache.org/contribute/setup-ide/).
+
+```bash
+# Compile and assemble everything (or a single module)
+./gradlew assemble
+./gradlew :pulsar-broker:assemble
+
+# Lint / verify (license headers, formatting, checkstyle) — run before pushing
+./gradlew rat spotlessCheck checkstyleMain checkstyleTest
+./gradlew spotlessApply # auto-fix license headers/formatting
+
+# Verify bundled-dependency LICENSE/NOTICE coverage (after changing a runtime
dependency)
+./gradlew checkBinaryLicense
+
+# Start a standalone Pulsar service (broker + bookie + metadata in one JVM)
+bin/pulsar standalone
+
+# Build docker images apachepulsar/pulsar(-all):latest
+./gradlew docker # or docker-all
+```
+
+For the Gradle build infrastructure and how to change build files (convention
plugins, version
+catalog, configuration-cache rules), see
+[`ARCHITECTURE.md` → Build
infrastructure](ARCHITECTURE.md#build-infrastructure).
+
+## Running tests
+
+Most of these per-module "unit tests" are actually **integration-style** —
they start a real in-JVM
+broker (`MockedPulsarServiceBaseTest` / `pulsarTestContext`) rather than
testing a class in isolation.
+The **container-based integration tests** that run against a Pulsar Docker
image are separate; see
+[Integration tests](#integration-tests) below.
+
+```bash
+# Always scope test runs with --tests — running a whole module's test task is
slow.
+# Run a single test class
+./gradlew :pulsar-client-original:test --tests "ConsumerBuilderImplTest"
+# Run a single test method
+./gradlew :pulsar-client-original:test --tests
"ConsumerBuilderImplTest.<methodName>"
+# Run all tests in a specific package
+./gradlew :pulsar-broker:test --tests "org.apache.pulsar.broker.admin.*"
+```
+
+> Note the [module-name-vs-directory
gotcha](ARCHITECTURE.md#module-name-vs-directory-name-gotcha):
+> directory `pulsar-client/` is the Gradle project `:pulsar-client-original`.
+
+### Test groups (TestNG)
+
+Tests use **TestNG** and are tagged with `@Test(groups = "...")`. By default
the build **excludes the
+`quarantine` and `flaky` groups** (`excludedTestGroups` default =
`quarantine,flaky`), so to run a
+single test that lives in one of those groups you must clear the exclusion:
+
+```bash
+# Run a specific test that is in the flaky/quarantine group (otherwise
excluded by default)
+./gradlew :pulsar-broker:test -PexcludedTestGroups='' --tests "<SomeFlakyTest>"
+```
+
+CI selects whole groups with `-PtestGroups=<groups>` and
`-PexcludedTestGroups=<groups>` (e.g.
+`broker,broker-admin`); locally prefer `--tests` to scope to specific classes
instead of running an
+entire group. CI splits `pulsar-broker` tests into groups (see
+`pulsar-build/run_unit_group_gradle.sh` and
`gradle/verify-test-groups.gradle.kts`). Tests with no
+group are treated as `other` at runtime. `./gradlew verifyTestGroups` reports
group assignments and
+flags tests not covered by any CI group.
+
+Other test-related properties: `-PtestJavaVersion=17` (run tests on a
different JDK toolchain),
+`-PtestRetryCount=N`, `-PtestFailFast=true|false`, `-PprotobufVersion=4.31.1`
(protobuf v4
+compatibility tests).
+
+Failed tests are retried once by default (`testRetryCount=1`; `0` when running
inside the IDE). When
+running tests locally, prefer **`-PtestRetryCount=0`** to catch failures
(including flakiness) early
+instead of having retries mask them.
+
+### Integration tests
+
+Integration tests live in `tests/` (see `tests/README.md`). They use
+[Testcontainers](https://www.testcontainers.org/) to bring up Pulsar services
in Docker, so **Docker
+must be installed and running**. Build the test image first, then run the
tests.
+
+The full integration suite is heavy and slow. **In local development, always
run individual
+integration tests** rather than the whole suite — pass `--tests` to select a
class (TestNG then
+discovers it directly from the classpath):
+
+```bash
+./gradlew :tests:latest-version-image:dockerBuild # build the docker test
image
+./gradlew :tests:integration:integrationTest --tests
"org.apache.pulsar.tests.integration.<TestClass>"
+```
+
+To run the **entire** integration test set, use **Personal CI** (below) rather
than running it
+locally. (`integrationTest` also accepts `-PtestGroups` /
`-PexcludedTestGroups` and
+`-PintegrationTestSuiteFile=<suite>.xml` to pick a specific TestNG suite.)
+
+### Running the full CI pipeline (Personal CI)
+
+The full test suite is large and slow to run locally. While iterating on a
change, run only the
+narrowly-scoped tests relevant to the change (a single test class or package,
see above) rather than
+a module's entire test task. To validate a larger change against the **full**
CI pipeline, do not run
+everything locally — use **Personal CI**, which runs Pulsar's CI workflows in
the contributor's own
+GitHub fork.
+
+If Personal CI is not yet set up, follow the
+[Personal CI documentation](https://pulsar.apache.org/contribute/personal-ci/)
to enable it on your
+fork. Once it is set up, the loop is:
+
+1. Keep the local `master` up-to-date with `apache/pulsar` and rebase the
feature branch on it.
+2. Push the feature branch to the **fork** to trigger CI runs there. CI runs
against the PR opened in
+ your own fork (it is normal to have a PR open in the fork *and* a PR for
the same branch open in
+ `apache/pulsar` at the same time).
+3. Monitor CI status on the fork and fix failures.
+4. Open the PR to `apache/pulsar` only after the fork's CI is green.
+
+Once the PR to `apache/pulsar` has been opened, stop rebasing as part of this
loop: step 1's rebase
+no longer applies — bring in upstream changes by merging instead (see [Pull
requests](#pull-requests)).
+
+### Retrying CI after flaky-test failures
+
+Pulsar has a large number of flaky tests, so GitHub Actions jobs on a PR
sometimes fail for reasons
+unrelated to the change. When a failure appears to be flakiness rather than a
regression caused by the
+PR, comment **`/pulsarbot rerun`** on the PR to re-run the failed jobs of the
workflow run. It only
+takes effect once the workflow run has **completed** (all jobs finished), so
wait for the run to
+finish before commenting.
+
+For a PR from a fork, a project maintainer must **approve** the workflow runs
before they execute, and
+approval is required again whenever new changes are pushed. This adds latency
to each run and makes it
+slow to tell flaky failures apart from genuine ones. Setting up **Personal
CI** (above) sidesteps this
+— the full pipeline runs in your own fork without maintainer approval — so it
is especially useful when
+a PR has legitimate test failures that you need to iterate on.
+
+If a flaky test that is unrelated to your change blocks your PR, you can move
it to the `flaky` group
+or disable it within your PR to unblock merging — and report it (search the
+[existing flaky-test
issues](https://github.com/apache/pulsar/issues?q=is%3Aissue+state%3Aopen+flaky)
+first). Don't push empty "trigger CI" commits to force a rerun; use
`/pulsarbot rerun` instead.
+
+## Pull requests
+
+PRs must follow `.github/PULL_REQUEST_TEMPLATE.md`. PR titles follow the
+`[<type>][<optional scope>] <description>` convention (e.g. `[fix][broker]
...`,
+`[improve][build] ...`) — refer to
`.github/workflows/ci-semantic-pull-request.yml` for the valid
+`[type]` and `[scope]` prefixes, which are enforced by CI. The `<description>`
should be in
+imperative form, like a good commit message's subject line, and **should not
reference issue numbers**
+(put `Fixes #N` / `Closes #N` in the description instead).
+
+**Keep each PR focused on one change.** Don't bundle unrelated drive-by
refactoring (for example,
+de-duplicating code you happened to touch) into a feature or bug-fix PR — it
widens the review surface
+and the risk; note such improvements as follow-ups instead. Likewise, **don't
reformat unrelated files
+or lines that aren't part of your change** (whitespace, import reordering,
re-wrapping) — drive-by
+formatting hides the real change in review and pollutes `git blame`. **Discuss
large refactorings on
[email protected] before investing effort**: there are many similar code
duplications across
+Pulsar, and a PR for each creates more maintainer burden than value. AI agents
make it easy to generate
+large volumes of refactoring PRs, and the project pushes back on these. Every
change needs a real,
+identifiable contributor who takes responsibility for it; unattributed
AI-agent-style contributions —
+especially larger ones from anonymous profiles or from people not actually
using Pulsar — are typically
+rejected.
+
+**Describe the change.** The PR description must cover, at minimum, the
**Motivation (why?)** and the
+**Modifications (what / how?)** — these map to the corresponding sections of
the PR template. A title
+alone, or a description that only restates the title, is not sufficient. Link
the related issue with
+`Fixes #N` (or equivalently `Closes #N`) for an issue the PR resolves, `Main
Issue: #N` for one task
+of a larger issue, or `PIP: #N` for a proposal.
+
+**Never rebase a PR branch once the PR is opened in `apache/pulsar`.**
Rebasing rewrites history and
+disrupts reviewers (it invalidates review comments and makes incremental diffs
unreadable). To bring
+in upstream changes, instead fetch from the `apache/pulsar` remote and
**merge** its `master` into the
+PR branch:
+
+```bash
+git fetch <apache-pulsar-remote> # e.g. `upstream` or `apache`
+git merge <apache-pulsar-remote>/master
+```
+
+(Rebasing onto an updated `master` is fine *before* the PR is opened — see the
Personal CI loop above
+— but not after.)
+
+### Branches and backports
+
+Target `master` first. Once the change is merged, **project maintainers handle
backporting** a bug or
+security fix to the supported maintenance branches (`branch-4.2`,
`branch-4.0`, …) when
+the bug is also present there, per the
+[release/support
policy](https://pulsar.apache.org/contribute/release-policy/). New **features**
are
+**not** added to LTS / maintenance branches without a [email protected]
discussion (and usually a
+PIP), to avoid regressions in stable releases.
+
+Backporting is done by **cherry-picking commits in their original merge
order**, which avoids
+unnecessary merge conflicts; sometimes a dependent change must be
cherry-picked before the fix itself.
+AI tools are effective at resolving the merge conflicts that arise during a
backport.
+
+## Reporting security vulnerabilities
+
+See [`SECURITY.md`](SECURITY.md) (latest at
<https://github.com/apache/pulsar/security/policy>) and
+<https://pulsar.apache.org/security/>. In short: report a
+suspected vulnerability **privately** (never in a public issue, PR, or
commit), and never reveal the
+security nature of a change in public until it is announced. An
**already-public** CVE that you only
+want to check Pulsar's exposure to is *not* a private disclosure — search the
CVE id in apache/pulsar
+PRs/issues first, then ask via a GitHub issue or [email protected].
+
+## AI coding agents
+
+If you use an AI coding assistant (Claude Code, Copilot, Cursor, Gemini,
Codex, Aider, …), see
+[`AGENTS.md`](AGENTS.md) for the agent-facing guidance — a routing index into
this guide,
+[`ARCHITECTURE.md`](ARCHITECTURE.md), [`CODING.md`](CODING.md), and
[`SECURITY.md`](SECURITY.md), plus
+the guardrails that apply specifically to AI-made changes.
diff --git a/README.md b/README.md
index b729bf8135a..dae2d2fe263 100644
--- a/README.md
+++ b/README.md
@@ -95,7 +95,6 @@ components in the Pulsar ecosystem, including connectors,
adapters, and other la
- [Pulsar Connectors](https://github.com/apache/pulsar-connectors) (bundled as
[pulsar-io](pulsar-io))
- [Pulsar Translation](https://github.com/apache/pulsar-translation)
-- [Pulsar SQL (Pulsar Presto
Connector)](https://github.com/apache/pulsar-presto) (bundled as
[pulsar-sql](pulsar-sql))
- [Ruby Client](https://github.com/apache/pulsar-client-ruby)
## Pulsar Runtime Java Version Recommendation
@@ -163,6 +162,10 @@ Docker image Java runtime: 17
## Build Pulsar
+The quick start below covers the essentials; see [`CONTRIBUTING.md` →
Building](CONTRIBUTING.md#building)
+for the full build and lint commands, and [`ARCHITECTURE.md` → Build
infrastructure](ARCHITECTURE.md#build-infrastructure)
+for the Gradle build infrastructure and how to change build files.
+
### Requirements
- JDK
@@ -188,53 +191,16 @@ There is also a guide for [setting up the tooling for
building Pulsar](https://p
### Build
-Compile and assemble:
-
-```bash
-./gradlew assemble
-```
-
-Check source code license headers and formatting:
-
-```bash
-./gradlew rat spotlessCheck checkstyleMain checkstyleTest
-```
-
-Check that bundled dependencies are properly recorded in the binary
distribution `LICENSE` and `NOTICE` files. Run this after adding, removing, or
upgrading a runtime dependency to confirm the corresponding entry has been
added to (or removed from) the LICENSE file. The task builds the binary
distribution tarballs as needed:
-
-```bash
-./gradlew checkBinaryLicense
-```
-
-Compile and assemble individual module:
-
-```bash
-./gradlew :pulsar-broker:assemble
-```
-
-Run Unit Tests:
-
-```bash
-./gradlew test
-```
-
-Run Individual Unit Test:
-
```bash
-./gradlew :pulsar-client-original:test --tests "ConsumerBuilderImplTest"
+./gradlew assemble #
compile and assemble
+./gradlew :pulsar-client-original:test --tests "ConsumerBuilderImplTest" # run
a single test
+bin/pulsar standalone # run
a standalone service
```
-Run Selected Test packages:
-
-```bash
-./gradlew :pulsar-broker:test --tests "org.apache.pulsar.*"
-```
-
-Start standalone Pulsar service:
-
-```bash
-$ bin/pulsar standalone
-```
+For the full build, lint, test, and PR workflow — test groups, integration
tests, Personal CI, and PR
+conventions — see [`CONTRIBUTING.md`](CONTRIBUTING.md). For the module map and
the Gradle build
+infrastructure see [`ARCHITECTURE.md`](ARCHITECTURE.md), and for coding
conventions see
+[`CODING.md`](CODING.md). AI coding agents should start from
[`AGENTS.md`](AGENTS.md).
Check https://pulsar.apache.org for documentation and examples.
@@ -301,13 +267,15 @@ You can self-register at
https://communityinviter.com/apps/apache-pulsar/apache-
## Security Policy
-If you find a security issue with Pulsar then please [read the security
policy](https://pulsar.apache.org/security/#security-policy). It is critical to
avoid public disclosure.
+See the [Pulsar security
policy](https://github.com/apache/pulsar/security/policy) for the full policy.
In short: **never disclose a suspected vulnerability publicly** (issue, PR, or
discussion) before a fix is released, and a human must verify and take
responsibility for a report — autonomous agents must not file security reports
on their own.
### Reporting a security vulnerability
-To report a vulnerability for Pulsar, contact the [Apache Security
Team](https://www.apache.org/security/). When reporting a vulnerability to
[[email protected]](mailto:[email protected]), you can copy your email to
[[email protected]](mailto:[email protected]) to send your
report to the Apache Pulsar Project Management Committee. This is a private
mailing list.
+Report privately by email to the [Apache Security
Team](https://www.apache.org/security/) at
[[email protected]](mailto:[email protected]); you may also copy the Apache
Pulsar PMC's private list,
[[email protected]](mailto:[email protected]). See the [Pulsar
security policy](https://github.com/apache/pulsar/security/policy) for more
detail.
+
+### Checking exposure to an already-public CVE
-https://github.com/apache/pulsar/security/policy contains more details.
+An already-public CVE is **not** a private disclosure. Search PRs and issues
by the CVE id at <https://github.com/apache/pulsar/pulls> (including closed
ones), then ask via a GitHub issue or the [email protected] mailing list
if needed. Pulsar's supported versions are listed at
<https://pulsar.apache.org/contribute/release-policy/>; upgrade to a supported
version to receive security updates. See the [Pulsar security
policy](https://github.com/apache/pulsar/security/policy).
## License
diff --git a/SECURITY.md b/SECURITY.md
index acd8c0140b0..946f9dcaa39 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,102 @@
# Security Policy
-See also https://pulsar.apache.org/security/.
+The authoritative policy is at <https://pulsar.apache.org/security/>; the
Apache Software Foundation's
+general process is at <https://www.apache.org/security/>. The latest version
of this file is maintained
+at <https://github.com/apache/pulsar/security/policy> — in a fork, check there
rather than a
+possibly-stale local copy. The summary below is what contributors (and any AI
tooling acting on their
+behalf) must follow.
+
+## Reporting a vulnerability
+
+Do **not** open a public GitHub issue, pull request, or discussion for a
suspected vulnerability —
+that defeats coordinated disclosure.
+
+Report it privately by email to the Apache Security Team at
**[email protected]** (the ASF's central
+security address). When reporting to **[email protected]**, you can copy
your email to the Apache
+Pulsar PMC's private list, **[email protected]**, to send your report
to the Project Management
+Committee as well.
+
+**A human must verify and take responsibility for every security report.**
Deciding that some
+behaviour is actually a vulnerability requires judgement against Pulsar's
threat model (see
+*[Security model and scope](#security-model-and-scope)* below), and the
security team is staffed by
+volunteers — a stream of unverified or AI-hallucinated "vulnerabilities"
wastes their time and buries
+real issues. AI tooling may help *analyse* a suspected problem, but a human
contributor must
+independently verify it and own the report. **Autonomous agents must not file
security reports or open
+security issues on their own**, and a tool's confident-sounding output is not,
by itself, evidence of a
+vulnerability.
+
+See <https://pulsar.apache.org/security/#security-policy> for more details.
+
+## Disclosure hygiene for contributors
+
+The **project team commits the fix**, coordinated through the ASF security
vulnerability handling
+process
+([apache.org/security/committers.html#possible](https://apache.org/security/committers.html#possible)).
+The team may commit the fix to the public repository **before** the release,
using a neutral commit
+message that does not state its security nature; in severe cases the commit
and release are made in a
+private repository and the fix is made public only at release time.
+
+As a contributor, do **not** push, commit publicly, or open a PR for a fix to
a non-public security
+issue yourself — **including in a public fork**, since a public-fork commit or
PR is itself a
+disclosure. When reporting, you may include a suggested fix patch privately in
your report to
+`[email protected]` — never in a public commit or PR.
+
+The neutrality rules below are for **whoever commits the fix — i.e. the
project team**. Until the
+vulnerability has been publicly announced, the commit message and PR
title/body must **not** reveal its
+security nature, even when the fix touches security-adjacent code
(authentication/authorization,
+deserialization, TLS, networking). Describe the behaviour change neutrally; a
commit or PR that
+advertises "fixes the CVE", "security fix", or "patches the vulnerability"
discloses the issue before
+it is announced and defeats coordinated disclosure.
+
+The same discretion applies to **everyone** — and identically to any AI
tooling acting on a
+contributor's behalf — in public GitHub issues, discussions, and review
comments until the
+announcement.
+
+**Already-public CVEs in dependencies are an exception.** The rules above
concern *non-public* Pulsar
+vulnerabilities. A PR or commit that upgrades a dependency to address an
**already publicly disclosed**
+CVE in that dependency does **not** follow them — the CVE is already public.
Name its id directly in
+the PR title and/or description (use the description when there are several
CVE ids).
+
+## Checking exposure to an already-public CVE
+
+For a CVE that is **already public** (for example, in a dependency) and you
want to check Pulsar's
+exposure or whether it is already fixed, this is **not** a private disclosure
— the right venue is a
+GitHub issue on apache/pulsar or a question on the **[email protected]**
mailing list.
+
+Before asking, search PRs and issues with the CVE id at
<https://github.com/apache/pulsar/pulls>
+(also check issues and **closed** PRs/issues) — the fix may already be merged
or the question already
+answered.
+
+## Security model and scope
+
+Pulsar's security model is not formally/explicitly defined. Two long-standing
design assumptions
+matter when deciding whether something is actually a vulnerability:
+
+**Pulsar Functions and connectors execute fully trusted code.** The function
instance runtime exists
+to run user-provided code — *remote code execution is its core purpose, not a
flaw.* (There have been
+some reports claiming that "the function instance runtime allows running
user-provided code and results
+in an RCE"; this is expected, by-design behaviour, not a security issue.) The
available execution models also let the code modify its environment:
+
+- The **thread** and **process** runtimes can read or modify any files and
state accessible to the
+ process they run in.
+- The **Kubernetes** runtime, on its own, does not restrict access to
resources in the Kubernetes
+ cluster. The project provides hooks for custom Kubernetes-runtime
*hardening*, but such hardening is
+ **not** part of the project.
+
+Therefore, Pulsar Functions and connectors must only run code that is **fully
trusted**.
+
+**Clusters rely on network-perimeter security.** Pulsar is designed to be
deployed behind a trusted
+network perimeter where only trusted users can reach the cluster. The project
does **not** implement
+explicit controls against malicious **denial-of-service (DoS)** attacks. Rate
limiting exists to mitigate
+*unintentional* DoS — e.g. improper configuration or thundering-herd effects —
but it is **not** a
+defense against a deliberate DoS by an attacker.
+
+Reports that amount to "a trusted function can run code / modify its
environment" or "a
+perimeter-trusted client can cause a denial-of-service" generally fall
**outside** Pulsar's threat
+model. When in doubt, ask through the reporting channels above rather than
assuming.
+
+## Supported versions
+
+Pulsar's supported versions are listed at
<https://pulsar.apache.org/contribute/release-policy/>.
+Security fixes are made to supported versions; users should upgrade to a
supported version to receive
+security updates.