blob: 2ae22e60fbaeeb5ea7bb003956a43b5041336fbc [file] [view]
<!--
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.
-->
# Airflow Java SDK
A **JVM** SDK for Apache Airflow. You can use any JVM-compatible language to
write workflow bundles, and have Airflow consume the result.
The SDK and execution-time logic is implemented in Kotlin.
An example is bundled showing how the SDK can be used in Java.
## Building the SDK
```bash
./gradlew build
```
## Building documentation
```bash
./gradlew dokkaGenerate
```
This uses [Dokka](https://kotl.in/dokka) to build documentation of the Java SDK.
This generates both an HTML representation and Javadoc.
## Running the example
The SDK projects must first built and published:
```bash
./gradlew publishToMavenLocal -PskipSigning=true
```
After the build is successful, you should be able to see directories in `~/.m2/repository/org/apache/airflow/`.
Now `cd example` into the example project, and
* Package the example to `./example/build/bundle`
```bash
# We're now in the 'example' directory, so gradlew is in parent.
../gradlew bundle
```
* Put the [DAG with stub tasks](./example/src/resources/dags) to somewhere Airflow can find.
* Ensure the `java` command is available in the same environment the Airflow
task worker is in.
* Configure Airflow to route tasks in the *java* queue to be run with Java:
```bash
export AIRFLOW__SDK__COORDINATORS='{
"java": {
"classpath": "airflow.sdk.coordinators.java.JavaCoordinator",
"kwargs": {"jars_root": ["/opt/airflow/java-sdk/example/build/bundle"]}
}
}'
export AIRFLOW__SDK__QUEUE_TO_COORDINATOR='{"java": "java"}'
```
* Ensure the Connection and Variable needed by the example DAG are available:
```bash
export AIRFLOW_CONN_TEST_HTTP='{
"conn_type": "http",
"login": "user",
"password": "pass",
"host": "example.com",
"port": 1234,
"extra": {"param1": "val1", "param2": "val2"}
}'
export AIRFLOW_VAR_MY_VARIABLE=123
```
## Publishing
The SDK is published to Maven Central via the
[ASF Nexus staging repository](https://repository.apache.org).
The full release process follows the
[ASF Maven publishing guide](https://infra.apache.org/publishing-maven-artifacts.html).
### Prerequisites
* An ASF committer account with access to
[repository.apache.org](https://repository.apache.org).
* A GPG key that has been
[added to the project KEYS file](https://infra.apache.org/release-signing.html)
and uploaded to a public key server.
### Bump the version
Edit `gradle.properties` and set the version for this release:
```properties
projectVersion=1.0.0
```
Commit the change and push it to the release branch.
### Verify the POM locally
Before touching any remote repository, publish to your local Maven cache and
inspect the generated POM:
```bash
rm -rf ~/.m2/repository/org/apache/airflow/ # Start clean.
./gradlew publishToMavenLocal -PskipSigning=true
# The airflow-sdk runtime.
less ~/.m2/repository/org/apache/airflow/airflow-sdk/*/airflow-sdk-*.pom
# The bill of materials of airflow-sdk
less ~/.m2/repository/org/apache/airflow/airflow-sdk-bom/*/*.pom
# The annotation processor for the builder pattern.
less ~/.m2/repository/org/apache/airflow/airflow-sdk-processor/*/airflow-sdk-*.pom
# The Gradle plugin for bundling.
less ~/.m2/repository/org/apache/airflow/airflow-sdk-gradle-plugin/*/airflow-sdk-*.pom
# The Gradle plugin's registration.
less ~/.m2/repository/org/apache/airflow/sdk/org.apache.airflow.sdk.gradle.plugin/*/*.pom
```
Check that the coordinates, description, license, SCM, and organization fields
look correct.
### Dry-run against a local repository
To test the full publish flow without touching ASF infrastructure, override the
repository URL to a local directory
```bash
rm -rf /tmp/local-maven-repo # Start clean.
./gradlew publish -PmavenUrl=file:///tmp/local-maven-repo -PskipSigning=true
ls /tmp/local-maven-repo/org/apache/airflow/
# This should contain the same components in ~/.m2 as inspected in the previous step.
```
*NOTE:* Signing is not required since nothing goes to Maven Central. If you want
to test signing, set the GPG private key and passphrase as described in the next
section, and remove `-PskipSigning=true` from the above command.
### Publish to ASF Nexus staging
Store the credentials in `~/.gradle/gradle.properties` so they are not exposed
in your shell history:
```properties
mavenUsername=your-asf-nexus-token-username
mavenPassword=your-asf-nexus-token-password
signing.password=your-gpg-key-passphrase
```
Then run the publish task.
```bash
./gradlew publish -P"signing.key=$(gpg --armor --export-secret-keys your-gpg-key-fingerprint)"
```
*NOTE:* The signing key is supplied through the command line since it contains
newlines, which does not work well in a Gradle properties file.
*NOTE:* You can also use the following environment variables to provide the
credentials instead: `ASF_NEXUS_USERNAME`, `ASF_NEXUS_PASSWORD`, `SIGNING_KEY`,
and `SIGNING_PASSWORD`. This is especially useful on e.g. CI.
### Verify the upload
Verify all artifacts have been released correctly to the
[ASF Nexus server](https://repository.apache.org/#nexus-search;quick~org.apache.airflow).
Check *Updated by* (should be your ID), *Uploaded Date*, and *Last Modified*.
## Contributing
The user implements a Java application containing task methods annotated (or
registered) with the SDK. The application is packaged as a bundle and placed
where Airflow can find it.
When the Airflow supervisor identifies that a task should run with Java, it
launches the JVM application as a subprocess. The flow is:
1. `JavaCoordinator.execute_task()` (Python) scans `jars_root`, builds the
classpath, and spawns `java -cp <jars> <MainClass> --comm=<host>:<port>
--logs=<host>:<port>`.
2. `Server.kt` connects to both sockets immediately on startup.
3. The supervisor sends a `StartupDetails` MessagePack message; the JVM reads
it, looks up the matching task by `dag_id` + `task_id`, and calls the
user's task method.
4. During execution the JVM sends requests to the supervisor (GetVariable,
GetConnection, GetXCom, SetXCom, etc.) and the supervisor responds. All
frames are a 4-byte big-endian length prefix followed by a MessagePack
payload.
5. On completion (or exception) the JVM sends a `TaskState` message and closes
the socket. The JVM process then exits.
Log messages produced by the SDK (not by user code) are forwarded over the
`--logs` socket so the supervisor can append them to Airflow's log store.
The wire protocol is defined in
`task-sdk/src/airflow/sdk/execution_time/schema/schema.json`.
`execution/Comm.kt` implements the framing layer. Adding a new message type
requires changes in **both** `schema.json` (Python side) and
`execution/Comm.kt` + `execution/Client.kt` (JVM side).
See [Architectural Design Records](./adr) in the `adr` directory to learn more.
### Repository layout
```
java-sdk/
├── sdk/ # Core library: public API (org.apache.airflow.sdk) and internal
│ # execution layer (org.apache.airflow.sdk.execution)
├── processor/ # Annotation processor that generates *Builder classes
├── plugin/ # Gradle plugin (org.apache.airflow.sdk) — bundle task, manifest
│ # attribute injection, and verifyBundleMainClass
├── bom/ # Bill of Materials POM so consumers can import all SDK artifacts
│ # at a consistent version
├── slf4j/ # SLF4J logging provider; routes SLF4J calls to the Airflow log store
├── jul/ # java.util.logging handler; routes JUL records to the Airflow log store
├── jpl/ # Java Platform Logging provider (System.Logger, JEP 264); routes JPL
│ # calls to the Airflow log store
├── log4j2/ # Log4j 2 appender; routes Log4j 2 events to the Airflow log store
├── example/ # End-to-end example bundle (annotation + interface APIs, Java source)
├── adr/ # Architectural Decision Records for the Java SDK
└── buildSrc/ # Shared Gradle convention plugins (Java version, lint, etc.)
```
The Python coordinator that launches the JVM subprocess lives outside this directory:
```
task-sdk/src/airflow/sdk/coordinators/java/ # JavaCoordinator (SubprocessCoordinator subclass)
task-sdk/tests/coordinators/java/ # Python-side unit and integration tests
```
### Testing
```bash
# Run all JVM tests
./gradlew test
# Run a specific test class
./gradlew :sdk:test --tests "org.apache.airflow.sdk.execution.CommTest"
```
For the Python coordinator, use Breeze (never run pytest on the host directly):
```bash
breeze testing task-sdk-tests -- task_sdk/coordinators/java
```
End-to-end tests that exercise a real Airflow environment:
```bash
E2E_TEST_MODE=java_sdk uv run --project airflow-e2e-tests pytest \
tests/airflow_e2e_tests/java_sdk_tests/ -xvs
```
### Coding conventions
- All SDK and processor source is **Kotlin**; Java is the *public API target*,
not the implementation language.
- Keep `sdk/src/main/kotlin/` (the public API surface) free of internal
implementation details; those belong in the `execution/` sub-package.
- The annotation processor (`BuilderProcessor.kt`) uses `kapt`. When adding a
new annotation, define it in `Builder.kt`, handle it in
`BuilderProcessor.kt`, and add a golden-output test in
`processor/src/test/kotlin/`.
- The Python coordinator subclasses `SubprocessCoordinator`. Do not reach into
the JVM process from Python beyond what `_build_execute_task_command`
provides.
- Run `./gradlew ktLintCheck spotlessCheck` (or `ktLintFormat spotlessApply`)
before submitting — the project enforces Kotlin and Java formatting.
- All new files need the Apache License header.
### Common tasks
**Adding a new `Client` method** (e.g. a new Airflow API call):
1. Regenerate POJO classes from `schema.json` if the message type is new.
2. Add Kotlin request/response data classes in `execution/Comm.kt` or a new file.
3. Add the public-facing method to `Client.kt` which delegates to
`execution/Client.kt` for the supervisor wire call.
4. Write a unit test in `sdk/src/test/kotlin/.../ClientTest.kt` mocking the
socket layer.
5. Update `airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst`
if the change is user-visible.
**Adding a new annotation**:
1. Define the annotation interface in `Builder.kt`.
2. Handle it in `BuilderProcessor.kt` — generate the appropriate code in the
`*Builder` class.
3. Add a test in `BuilderTest.kt` with expected generated output.
4. Update the annotation table in `java.rst`.
**Fixing a framing or protocol bug**: focus on `execution/Comm.kt` and
`execution/Frame.kt`. `CommTest.kt` covers encode/decode round-trips; add a
regression test reproducing the bug before fixing it.
### PR checklist
- Run `./gradlew build test` (JVM) and the relevant pytest suite (Python
coordinator).
- Confirm the example bundle still compiles (see "Running the example" above up
to the bundling step)
- If `schema.json` changed, verify both sides (JVM + Python) handle the
new/changed fields.
- Add or update tests for every changed behaviour.
- For user-visible changes to `task-sdk/`, add a newsfragment under
`airflow-core/newsfragments/`.