kestra-io / kestra-io/plugin-serdes
Evaluate Hardwood as a Hadoop-free Parquet reader for ParquetToIon
- Dominant language
- Java
- Stars
- 1
- Forks
- 20
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 36
Description
## Summary
Evaluate replacing the `parquet-avro` + Hadoop stack used by `ParquetToIon` with [Hardwood](https://hardwood.dev/latest/), a zero-Hadoop, zero-transitive-dependency Parquet reader for the JVM. Hardwood targets the exact pain point of `ParquetToIon` today: pulling in `parquet-avro`, `hadoop-common`, `hadoop-auth`, and `hadoop-mapreduce-client-core` just to stream rows out of a Parquet file.
## Motivation
- `ParquetToIon` currently drags in the full Hadoop dependency tree (`hadoop-common`, `hadoop-auth`, `hadoop-mapreduce-client-core`) purely to satisfy `parquet-avro`'s `HadoopInputFile` API — none of Hadoop's distributed-filesystem or MapReduce machinery is actually used, since the task always reads from a local temp file.
- That dependency tree is a recurring source of shaded-jar bloat, transitive CVE noise, and classloading friction (see the existing `ParquetTools.handleLogger()` / `initSnappy()` workarounds already needed to tame Hadoop's logging and native codec initialization).
- Hardwood is purpose-built for this: it reads Parquet files with "zero transitive dependencies beyond optional compression libraries," is multi-threaded for page decoding, and documents throughput on par with or better than `parquet-java`.
- Smaller, Hadoop-free dependencies reduce the plugin's attack surface and shadow-jar size, which benefits every Kestra user pulling this plugin, not just Parquet-heavy workflows.
## Context
⚠️ This started as a **read-path only** investigation. Hardwood 1.0 (`1.0.0.Final`, released 2026-06-25) shipped read-only.
**Update (2026-09-09)**: Hardwood **1.1.0.Beta1** (released 2026-08-31) adds Parquet **write** support — still **Beta**. Object-store output, page-index/Bloom-filter writing, and parallel column encoding remain forward-looking roadmap items with no committed dates. `IonToParquet` (the write path) should stay on `parquet-avro` until Hardwood's writer reaches GA; re-evaluate migration once it stabilizes (see updated Suggested Tasks below).
## API Reference
- **Official docs**: https://hardwood.dev/latest/
- **GitHub**: https://github.com/hardwood-hq/hardwood
- **License**: Apache License 2.0 (confirmed on the GitHub repo; the docs site content itself is CC BY-SA 4.0, but that only covers the prose, not the library code)
- **Authentication**: none required for local/temp-file reads (Kestra already downloads the Parquet file from internal storage before parsing); Hardwood also ships native S3 read support if a future task ever reads directly from S3
- **Java requirement**: JAR runs on Java 21+ (some codec paths need 22+) — compatible with this plugin's `targetJavaVersion = JavaVersion.VERSION_21` in `build.gradle`
- **API surface**: `RowReader` for row-by-row access (fits `ParquetToIon`'s current `Flux`-based streaming model), experimental `ColumnReader` for columnar access
- **Write support**: added in `1.1.0.Beta1` (2026-08-31) — Beta, not yet recommended for `IonToParquet`
## Gradle Dependencies
Add to `build.gradle`, replacing the `parquet-avro` + `hadoop-*` block used for reading:
```groovy
// Hardwood — zero-Hadoop Parquet reader
implementation "dev.hardwood:hardwood-core:1.0.0.Final"
```
> Remove (or scope down to `IonToParquet`'s write path only, if the Hadoop dependency can be isolated): `org.apache.hadoop:hadoop-common:3.5.0`, `org.apache.hadoop:hadoop-auth:3.5.0`, `org.apache.hadoop:hadoop-mapreduce-client-core:3.5.0`. `parquet-avro` stays for `IonToParquet` until Hardwood's writer reaches GA.
>
> Hardwood pulls in **no transitive dependencies** beyond optional compression codecs (Snappy, ZSTD, LZ4, Brotli) — verify which codecs the current test fixtures under `src/test/resources` actually exercise and only add those.
>
> Track `dev.hardwood:hardwood-core` releases past `1.1.0.Beta1` for write-path GA before considering an `IonToParquet` migration.
## Plugin Structure
- **Repository**: `plugin-serdes` (existing — no new repository, no new subpackage)
- **Namespace**: `io.kestra.plugin.serdes.parquet` (existing)
- **Affected classes**: `io.kestra.plugin.serdes.parquet.ParquetToIon` (read path — candidate for migration), `io.kestra.plugin.serdes.parquet.ParquetTools` (Hadoop logger/Snappy init workarounds — likely simplified or removed once Hadoop is dropped from the read path)
- **Unaffected for now**: `io.kestra.plugin.serdes.parquet.IonToParquet` (write path — stays on `parquet-avro` until Hardwood's writer, added in `1.1.0.Beta1`, reaches GA)
- **Categories**: `DATA`, `CORE` (unchanged — existing `package-info.java`)
## Suggested Tasks
1. Spike: swap `ParquetToIon`'s `AvroParquetReader`/`HadoopInputFile` usage for Hardwood's `RowReader`, mapping Hardwood's row/column types to the `GenericRecord`-based `AvroDeserializer.recordDeserializer` pipeline (or a new deserializer if Hardwood's native types make the Avro hop unnecessary)
2. Verify Hardwood correctly reads every Parquet file already covered by `src/test/resources` for this plugin, including nested/complex schemas and each compression codec in use
3. Confirm the "documented semantic differences" Hardwood calls out (e.g. SQL three-valued logic for inequality operations) don't silently change output for existing flows — add regression tests if any diff is found
4. Benchmark: compare read throughput and memory footprint against the current `parquet-avro` + Hadoop path on a representative large Parquet file
5. If the spike is a net win, remove `hadoop-common`, `hadoop-auth`, `hadoop-mapreduce-client-core` from `build.gradle` for the read path and simplify/remove `ParquetTools.handleLogger()` if it was solely working around Hadoop's logging
6. Leave `IonToParquet` on `parquet-avro` for now; Hardwood's writer landed in `1.1.0.Beta1` (2026-08-31) but is still Beta — revisit once it reaches GA
7. Track Hardwood's writer path to GA; once stable, open a follow-up issue to evaluate migrating `IonToParquet` off `parquet-avro`
8. Update `src/main/resources/doc/io.kestra.plugin.serdes.md` if the Parquet section references implementation details that change (e.g. Hadoop-related gotchas)
## Acceptance Criteria
### Functional
- [ ] `ParquetToIon` produces byte-identical ION output (or documents and justifies any diff) against the current `parquet-avro`-based implementation, across the existing test fixture set
- [ ] Unit tests pass (`./gradlew test`)
- [ ] Build passes (`./gradlew build`)
- [ ] `IonToParquet` is unmodified and continues to pass its existing tests
- [ ] `IonToParquet` migration stays out of scope for this issue until Hardwood's writer (`1.1.0.Beta1`+) reaches GA
### Kestra Plugin Coding Standards
- [ ] No behavior change to any `Property`-typed inputs on `ParquetToIon` — this is an internal implementation swap, not a property redesign
- [ ] `runContext.logger()` used for any new logging (no direct Hadoop logger config left behind if Hadoop is removed)
- [ ] `@Metric`-declared `records` counter output unchanged
### Documentation & Structure
- [ ] `build.gradle` dependency comment reflects the new Hardwood dependency and the reason `parquet-avro`/Hadoop remain for the write path
- [ ] Plugin how-to doc (`io.kestra.plugin.serdes.md`) updated if any user-facing behavior or limitation changes (e.g. new max column-chunk size constraint of 2 GB that Hardwood documents)
## Notes for the developer
- Hardwood documents a hard limit: individual column chunks must not exceed 2 GB. This is not a concern for `parquet-avro` today — confirm this doesn't regress any existing large-file flow before removing the old path.
- Hardwood 1.1.0.Beta1 (2026-08-31) added write support (Beta). Do not migrate `IonToParquet` until it reaches GA — track https://hardwood.dev/latest/ release notes.
-
---
*[View as Artifact](https://claude.ai/code/artifact/fee29d30-bd85-483e-af8a-1391b8432c5b)*
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with build.gradle, ParquetToIon, ParquetTools, and the existing fixtures under src/test/resources; run the current tests before evaluating Hardwood's RowReader. Compare the read path across nested schemas and used compression codecs, then benchmark output, throughput, and memory against the existing implementation. Done means the acceptance checks pass, or any incompatibility is documented, while IonToParquet remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- data
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100