- Push to R2 (recommended) — the upstream job writes Parquet
directly to your tenant’s R2 bucket via the
s3a://connector. - Pull via
cloud_fs(fallback) — eomer reads directly from the customer’s Azure/GCS/WebHDFS/S3-compatible store. For regulated or air-gapped environments where a cross-cloud copy is not an option.
_SUCCESS marker contract, so
the consumer never sees a half-written prefix. This guide covers the
producer configuration for push, the eomer-side config for both
directions, and how to decide which one fits.
Why push to R2 instead of pulling
- No VPN peering, no firewall holes. The customer’s cluster writes outbound to an HTTPS endpoint.
- One auth surface. The same R2 token the customer already has covers both direct uploads and Spark pushes.
- Works with any S3-compatible producer. Spark, Flink, Trino,
DuckDB,
hadoop distcp, Databricks, EMR — all support thes3a://scheme out of the box. - The eomer forecasting model uses batch input. A coherent history snapshot per job run is the right semantic for forecasting; streaming event feeds would need to be materialized into the same shape regardless.
Producer configuration
Apache Spark 3.x
_SUCCESS marker automatically when all
part-files are durable. That’s all eomer needs to know the snapshot is
safe to read.
Hadoop distcp
distcp writes _SUCCESS at the destination once all files copy
cleanly.
Other producers
Any tool that speaks S3 works — Flink, Trino, DuckDB (COPY ... TO 's3://...'), pandas + s3fs. Make sure the job writes a _SUCCESS
(or equivalent) marker after the data files are durable. If the tool
doesn’t emit one, add a final step: aws s3 cp /dev/null s3://bucket/prefix/_SUCCESS --endpoint-url https://....
Consumer contract (eomer side)
Minimum: require _SUCCESS before reading
require_success_marker is ignored
when key points at a single object (no slash).
Rolling pointer: watermark_file
Producers that write dated snapshots and keep history should update a
pointer file atomically once each snapshot completes:
latest.txt, trims the contents, and uses them as the
effective prefix. This decouples the eomer config from the snapshot
rotation: the customer’s scheduler owns the pointer; eomer always reads
whatever the pointer says “latest” is.
A runnable example is in
configs/example_r2_spark_push.yaml.
Pull fallback: read directly from the customer’s cloud
Push-to-R2 is the recommended primary path: one auth surface, no VPN peering, producer-side handoff contract. When it’s not viable — regulated environments that disallow cross-cloud copies, customers who already have fresh data in their own Azure/GCS/WebHDFS store, or short pilots where setting up a Spark job is overkill — eomer can pull directly via thecloud_fs connector.
One connector covers every fsspec-supported backend:
For Cloudflare R2 keep using
type: r2 — the dedicated connector
has stricter EOMER_R2_* credential handling and a simpler config.
The cloud_fs validator will reject r2:// URIs and point you back
to the R2 connector.
Azure Data Lake example
GCS example
WebHDFS example
_SUCCESS marker contract is identical across all backends — the
connector uses the same Spark/Hadoop convention as the R2 push path.
Credentials are passed verbatim to fsspec under storage_options;
never embed secrets in configs. A runnable example is in
configs/example_cloud_fs.yaml.
Choosing between push and pull
Troubleshooting
See also Tenant-Isolated Storage (R2) for
bucket naming and credential setup.