Spark implementation of the FD tools (mix_maprule, unshard, t2s, t2c).
Reads stdtime.* footprint descriptor files from Linode Object Storage (S3),
runs the desired C++ tool via JNI, and uploads the resulting stdtime.* /
stdspace.* / stdcount.* output file back to S3 — encrypted with SSE-C.
Four Spark jobs are provided:
| Job class | Launcher | Replaces |
|---|---|---|
com.fd.FDPipelineJob |
run_pipeline_job.sh |
mix_maprule |
com.fd.UnshardJob |
run_unshard_job.sh |
unshard |
com.fd.T2sJob |
run_t2s_job.sh |
t2s (stdtime → stdspace) |
com.fd.T2cJob |
run_t2c_job.sh |
t2c (stdtime → stdcount) |
This replaces the standalone binaries for cloud/distributed execution while keeping all computation in the existing C++ codebase.
These stdtime.* input files are produced upstream by fd-compute-spark.
Linode Object Storage
<MIX_MAPRULE_IDIR>/stdtime.mm1.keyN (SSE-C encrypted)
<MIX_MAPRULE_IDIR>/stdtime.w143.keyN
│
│ Download + SSE-C decrypt (driver, then broadcast to executor)
▼
/tmp/fd_mix_<id>/input/stdtime.mm1
/tmp/fd_mix_<id>/input/stdtime.w143
│
│ MIX_MAPRULE_CONFIG downloaded from S3, broadcast as string
▼
C++ FD_Calculus::process() via JNI (FD_JNI.cpp)
Config: "mm1 add 1\nw143 add 1"
│
▼
/tmp/fd_mix_<id>/output/stdtime.<suffix>
│
│ SSE-C encrypt + upload
▼
Linode Object Storage
<MIX_MAPRULE_ODIR>/stdtime.<suffix>.keyN
fd-tools-spark/
├── compile_native.sh ← Step 1: compile C++ on your target platform
├── run_pipeline_job.sh ← Step 3: run the Spark job
├── build.sbt ← Scala 2.13 / Spark 4.1 build file
├── conf/
│ └── encryption_keys.json ← SSE-C AES keys (one entry per key index)
├── cpp_src/ ← All C++ source files
│ ├── FD_JNI.cpp ← JNI bridge (Spark entry point)
│ ├── FD_Calculus.cpp/h ← Core mix computation
│ ├── FD.cpp/h ← FD data structure
│ ├── FD_QuickAdd.cpp/h ← Fast accumulation helpers
│ ├── FD_Quota.h ← Quota definitions
│ ├── FD_stdtime.cpp/h ← stdtime file I/O
│ ├── FD_stdspace.cpp/h ← stdspace file I/O
│ ├── FD_stdcount.cpp/h ← stdcount file I/O
│ ├── now.cpp/h ← Timing utility
│ ├── mix_maprule.cpp ← Standalone binary (reference / local testing)
│ ├── t2s.cpp ← stdtime → stdspace conversion binary
│ ├── t2c.cpp ← stdtime → stdcount conversion binary
│ ├── concat_stdtime.cpp ← concat binary
│ └── unshard.cpp ← unshard binary
├── lib/ ← Native library (output of compile_native.sh)
│ ├── libFD.dylib ← macOS — used for local testing
│ └── libFD.so ← Linux — used on Spark cluster
│ (sbt package bundles these into the jar
│ under native/ for runtime extraction)
└── src/main/scala/com/fd/
├── FDPipelineJob.scala ← Spark job — mix_maprule
├── UnshardJob.scala ← Spark job — unshard
├── T2xJob.scala ← Shared pipeline + T2sJob / T2cJob wrappers
└── FDNative.scala ← JNI interface + jar-resource loader
| File | Portable? | Notes |
|---|---|---|
target/scala-2.13/fd-spark_2.13-0.1.jar |
Pure Scala bytecode + the .so/.dylib produced by compile_native.sh, embedded as resources under native/ |
|
run_pipeline_job.sh / run_unshard_job.sh / run_t2s_job.sh / run_t2c_job.sh |
✅ | Shell scripts, no changes needed |
conf/encryption_keys.json |
✅ | Keep secret — contains AES keys |
The native library is bundled inside the jar. At runtime,
FDNativeextractsnative/libFD.{so,dylib}from the jar to a temp file and callsSystem.load(). NoLD_LIBRARY_PATH/java.library.pathsetup needed on the receiving cluster.
⚠️ The jar is platform-specific because the bundled.so/.dylibis. Re-run./compile_native.sh && sbt packageon the target OS/arch (typically Linux x86_64) before distributing to the cluster. A macOS-built jar will fail withdlopenerrors on Linux.
Run on the target platform (the machine where Spark executors will run):
cd fd-tools-spark
./compile_native.shThis produces:
lib/libFD.dylib(macOS) orlib/libFD.so(Linux) — embedded into the jar by step 2- Standalone binaries in
cpp_src/:mix_maprule,t2s,t2c,concat,unshard(for local CLI testing)
Linux prerequisites:
sudo apt-get install -y g++ openjdk-11-jdk
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64macOS prerequisites:
brew install openjdk@11
export JAVA_HOME=/opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk/Contents/Homecd fd-tools-spark
sbt package
# → target/scala-2.13/fd-spark_2.13-0.1.jarsbt package automatically copies whichever of lib/libFD.{so,dylib} exist
into the jar under native/. You can verify with:
unzip -l target/scala-2.13/fd-spark_2.13-0.1.jar | grep native/
# native/libFD.so ...
# native/libFD.dylib ... (only on macOS dev boxes)Always run
compile_native.shfirst. Iflib/is empty whensbt packageruns, the resulting jar will not contain a native lib and the runtime loader will fall back toSystem.loadLibrary("FD")(which most deploys won't have set up viaLD_LIBRARY_PATH).
Set the three required MIX_MAPRULE_* env vars and run:
cd fd-tools-spark
MIX_MAPRULE_IDIR=Anirudh/fds \
MIX_MAPRULE_ODIR=Anirudh/mix_output \
MIX_MAPRULE_CONFIG=Anirudh/config.key2 \
./run_pipeline_job.shWith all optional parameters:
MIX_MAPRULE_IDIR=Anirudh/fds \
MIX_MAPRULE_ODIR=Anirudh/mix_output \
MIX_MAPRULE_CONFIG=Anirudh/config.key2 \
MIX_MAPRULE_SUFFIX=mymix \
MIX_MAPRULE_THINNING_PCT_JUMP=5.0 \
S3_OUTPUT_KEY_INDEX=2 \
FD_LOCAL_DEBUG_DIR=/tmp/fd_debug_out \
./run_pipeline_job.shThe unshard job divides every value in a stdtime.* file by an integer
UNSHARD_FACTOR (typically the number of map shards that produced it). It
operates on a single input file rather than a directory.
cd fd-tools-spark
UNSHARD_IDIR=Anirudh/mix_output/stdtime.config.key2 \
UNSHARD_ODIR=Anirudh/unshard_output/stdtime.unshard \
UNSHARD_FACTOR=16 \
./run_unshard_job.sh| Variable | Required | Description |
|---|---|---|
UNSHARD_IDIR |
✅ | S3 path to the single input stdtime.*.keyN file |
UNSHARD_ODIR |
✅ | S3 output path (without .keyN — added automatically) |
UNSHARD_FACTOR |
✅ | Float divisor applied to every value in the input |
S3_OUTPUT_KEY_INDEX |
✗ | SSE-C key index for the output file. Defaults to smallest key index |
FD_LOCAL_DEBUG_DIR |
✗ | If set, also writes a local copy of the unsharded output |
Output is uploaded to s3://<S3_BUCKET>/<UNSHARD_ODIR>.key<N>.
The t2s job converts a stdtime.* file into a stdspace.* sizing curve.
It operates on a single input file rather than a directory.
cd fd-tools-spark
T2S_IDIR=Anirudh/unshard_output/stdtime.unshard.key2 \
T2S_ODIR=Anirudh/t2s_output/stdspace.unshard \
./run_t2s_job.sh| Variable | Required | Description |
|---|---|---|
T2S_IDIR |
✅ | S3 path to the single input stdtime.*.keyN file |
T2S_ODIR |
✅ | S3 output path (without .keyN — added automatically) |
S3_OUTPUT_KEY_INDEX |
✗ | SSE-C key index for the output file. Defaults to smallest key index |
FD_LOCAL_DEBUG_DIR |
✗ | If set, also writes a local copy of the stdspace output |
Output is uploaded to s3://<S3_BUCKET>/<T2S_ODIR>.key<N>.
The t2c job converts a stdtime.* file into a stdcount.* sizing curve.
Same shape as t2s — only the output type differs.
cd fd-tools-spark
T2C_IDIR=Anirudh/unshard_output/stdtime.unshard.key2 \
T2C_ODIR=Anirudh/t2c_output/stdcount.unshard \
./run_t2c_job.sh| Variable | Required | Description |
|---|---|---|
T2C_IDIR |
✅ | S3 path to the single input stdtime.*.keyN file |
T2C_ODIR |
✅ | S3 output path (without .keyN — added automatically) |
S3_OUTPUT_KEY_INDEX |
✗ | SSE-C key index for the output file. Defaults to smallest key index |
FD_LOCAL_DEBUG_DIR |
✗ | If set, also writes a local copy of the stdcount output |
Output is uploaded to s3://<S3_BUCKET>/<T2C_ODIR>.key<N>.
| Variable | Description |
|---|---|
S3_ACCESS_KEY |
Linode Object Storage access key |
S3_SECRET_KEY |
Linode Object Storage secret key |
| Variable | Required | Description |
|---|---|---|
MIX_MAPRULE_IDIR |
✅ | S3 prefix containing stdtime.* input files. All stdtime.* files under this prefix are downloaded. E.g. Anirudh/fds |
MIX_MAPRULE_ODIR |
✅ | S3 prefix for output upload. E.g. Anirudh/mix_output |
MIX_MAPRULE_CONFIG |
✅ | S3 path to the config file. E.g. Anirudh/config.key2 |
MIX_MAPRULE_SUFFIX |
✗ | Output filename suffix → stdtime.<suffix>. Defaults to config filename stem (e.g. config from config.key2) |
MIX_MAPRULE_THINNING_PCT_JUMP |
✗ | Thinning percentage jump (same as mix_maprule -j). Default 0.0 = no thinning |
Values can be bare S3 keys (
Anirudh/fds) or full URLs (s3://bucket/Anirudh/fds) — both are accepted.
| Variable | Default | Description |
|---|---|---|
S3_BUCKET |
fds-e3testing-bucket-husingh |
Bucket name |
S3_ENDPOINT |
us-ord-10.linodeobjects.com |
S3-compatible endpoint |
S3_OUTPUT_KEY_INDEX |
smallest key index | Key index for SSE-C encrypting the output. Output file named stdtime.<suffix>.key<N> |
ENCRYPTION_KEYS_FILE |
conf/encryption_keys.json |
JSON file mapping key index → base64 AES-256 key |
| Variable | Default | Description |
|---|---|---|
FD_LOCAL_DEBUG_DIR |
(unset) | If set, copies downloaded inputs and C++ output to <dir>/<suffix>/input/ and <dir>/<suffix>/output/ before temp cleanup |
conf/encryption_keys.json maps each key index (the .keyN suffix on S3 files)
to its base64-encoded 32-byte AES-256 SSE-C key:
{
"1": "<base64-32-byte-key>",
"2": "<base64-32-byte-key>"
}Input files are decrypted with the key matching their .keyN suffix. Output files
are encrypted with S3_OUTPUT_KEY_INDEX.
The Spark job reads the MIX_MAPRULE_* env vars and translates them into
explicit JNI call parameters — the C++ JNI bridge (FD_JNI.cpp) does not
read env vars itself:
| Env var | C++ call |
|---|---|
MIX_MAPRULE_IDIR |
Files downloaded here → fcc.setInputDirectory(localInputDir) |
MIX_MAPRULE_ODIR |
C++ writes locally → Scala uploads to this S3 prefix |
MIX_MAPRULE_CONFIG |
Downloaded → passed as string → fcc.readCommands(tmpFile) |
MIX_MAPRULE_SUFFIX |
fcc.setAnswerSuffix(suffix) → output file = stdtime.<suffix> |
MIX_MAPRULE_THINNING_PCT_JUMP |
fcc.setThinningPctJump(pct) |
After a successful run, the output file appears at:
s3://<S3_BUCKET>/<MIX_MAPRULE_ODIR>/stdtime.<suffix>.key<N>
Example:
s3://fds-e3testing-bucket-husingh/Anirudh/mix_output/stdtime.config.key2
Inspect locally if FD_LOCAL_DEBUG_DIR was set:
ls /tmp/fd_debug_out/config/
# input/ ← downloaded stdtime.* inputs
# output/ ← C++ mix result
head /tmp/fd_debug_out/config/output/stdtime.configBuilt by compile_native.sh for local testing — identical logic to what the JNI
bridge calls:
| Binary | Description |
|---|---|
mix_maprule |
Blend stdtime.* files per a config (same logic as the Spark job) |
t2s |
Convert stdtime.* → stdspace.* sizing curve |
t2c |
Convert stdtime.* → stdcount.* sizing curve |
concat |
Concatenate / combine stdtime.* files |
unshard |
Merge sharded stdtime.* files from multiple reducers |
These binaries also read MIX_MAPRULE_* env vars directly (unlike the JNI path).
Example — run mix_maprule locally to verify the same result as the Spark job:
cd fd-tools-spark/cpp_src
MIX_MAPRULE_IDIR=/tmp/fd_debug_out/config/input \
MIX_MAPRULE_ODIR=/tmp/fd_debug_out/config/standalone_output \
MIX_MAPRULE_CONFIG=/path/to/config \
MIX_MAPRULE_SUFFIX=config \
./mix_maprule| Item | Notes |
|---|---|
| Single Spark task | One operation per job invocation. Run the job multiple times (with different env vars) for multiple operations in parallel. |
concat not yet in Spark |
The C++ source is included for reference; a JNI wrapper would follow the same pattern as runT2s / runT2c in FD_JNI.cpp. |