Skip to main content
Enterprise data teams run their analytics on Spark, Hadoop, and lakehouse stacks (Iceberg, Delta). There are two ways to feed that data into eomer:
  • 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.
Both paths use the same Spark/Hadoop _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 the s3a:// 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

Spark’s Parquet writer emits a _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

If the marker is absent, the run aborts with a clear error — eomer never sees a half-written snapshot. 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:
Point eomer at the pointer:
eomer reads 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 the cloud_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

The _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.