20
0

4 Commits

Author SHA1 Message Date
1525a48561 datasets/README: remove untested notices after successful end-to-end run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 10:05:18 -07:00
172e66865c datasets: explicit partition counts and cache df between sorts
sort_and_write() now takes num_partitions (default from config or 200)
and passes it explicitly to repartition() instead of relying on
spark.sql.shuffle.partitions. comments_part2 defaults to 800 partitions
(~1GB/file matching the existing dataset density) and submissions_part2
to 300. Also cache the preprocessed DataFrame so the input is not
re-read and reshuffled twice when writing the by_subreddit and
by_author outputs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:48:05 -07:00
9bc871285b datasets/add_months.sh: auto-detect cores/memory for spark-submit
Compute executor cores and memory from the values spark-env.sh detects
at startup (SPARK_WORKER_CORES, SPARK_WORKER_MEMORY) and pass them as
--conf flags to spark-submit. This overrides the static defaults in
spark-defaults.conf so the job scales automatically to whatever machine
it runs on. Uses a bash array to avoid word-splitting issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:07:58 -07:00
7c9db519ac datasets/add_months.sh: set PYSPARK_PYTHON so spark workers use the venv
spark-submit defaults to the container Python, which doesn't have the
project's dependencies. Export PYSPARK_PYTHON=$PYTHON before starting
the cluster so workers use the same interpreter as Part 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:16:50 -07:00
5 changed files with 40 additions and 13 deletions

View File

@@ -60,9 +60,6 @@ runs the Part 2 Spark sort.
### Add new months — `add_months.sh YYYY-MM [YYYY-MM ...]` ### Add new months — `add_months.sh YYYY-MM [YYYY-MM ...]`
> **NOTE: written but not yet tested. Remove this notice after a
> successful end-to-end run.**
Use this for routine incremental updates. Runs Part 1 on only the Use this for routine incremental updates. Runs Part 1 on only the
specified months, then appends the sorted output as a new layer of specified months, then appends the sorted output as a new layer of
partition files alongside the existing ones. No existing data is partition files alongside the existing ones. No existing data is
@@ -130,9 +127,6 @@ from a login node — it takes the number of nodes as its first argument.
### Merge layers — `merge_layers.sh` ### Merge layers — `merge_layers.sh`
> **NOTE: written but not yet tested. Remove this notice after a
> successful end-to-end run.**
Use this to collapse accumulated layers from incremental adds into a Use this to collapse accumulated layers from incremental adds into a
single clean layer. Reads the existing final datasets, re-sorts single clean layer. Reads the existing final datasets, re-sorts
everything, writes to `.merging` temp paths, then atomically replaces the everything, writes to `.merging` temp paths, then atomically replaces the

View File

@@ -108,15 +108,32 @@ parallel --joblog add_months_joblog.txt --results add_months_logs \
# --- Part 2: sort new months into staging (Spark, single fat node) ---------- # --- Part 2: sort new months into staging (Spark, single fat node) ----------
source "$SPARK_CONF_DIR/spark-env.sh" source "$SPARK_CONF_DIR/spark-env.sh"
export PYSPARK_PYTHON="$PYTHON"
# Compute executor resources from what spark-env.sh detected.
# One executor gets all worker cores and ~85% of worker memory (leaving
# headroom for the driver and overhead).
_EXEC_CORES=${SPARK_WORKER_CORES:-$(nproc)}
_WORKER_MEM_GB=${SPARK_WORKER_MEMORY%g}
_EXEC_MEM_GB=$(( _WORKER_MEM_GB * 85 / 100 ))
_DRIVER_MEM_GB=$(( _WORKER_MEM_GB * 10 / 100 ))
SPARK_SUBMIT_CONF=(
--conf "spark.executor.cores=${_EXEC_CORES}"
--conf "spark.executor.memory=${_EXEC_MEM_GB}g"
--conf "spark.driver.memory=${_DRIVER_MEM_GB}g"
)
start_spark_cluster.sh start_spark_cluster.sh
spark-submit --master "spark://$(hostname):$SPARK_MASTER_PORT" \ spark-submit --master "spark://$(hostname):$SPARK_MASTER_PORT" \
"${SPARK_SUBMIT_CONF[@]}" \
comments_part2.py \ comments_part2.py \
--indir="$TEMP_COMMENTS" \ --indir="$TEMP_COMMENTS" \
--out_by_subreddit="$STAGING_COMMENTS_SUB" \ --out_by_subreddit="$STAGING_COMMENTS_SUB" \
--out_by_author="$STAGING_COMMENTS_AUTH" --out_by_author="$STAGING_COMMENTS_AUTH"
spark-submit --master "spark://$(hostname):$SPARK_MASTER_PORT" \ spark-submit --master "spark://$(hostname):$SPARK_MASTER_PORT" \
"${SPARK_SUBMIT_CONF[@]}" \
submissions_part2.py \ submissions_part2.py \
--indir="$TEMP_SUBMISSIONS" \ --indir="$TEMP_SUBMISSIONS" \
--out_by_subreddit="$STAGING_SUBMISSIONS_SUB" \ --out_by_subreddit="$STAGING_SUBMISSIONS_SUB" \

View File

@@ -15,7 +15,9 @@ from dumps_helper import COMMENTS, sort_and_write
if __name__ == "__main__": if __name__ == "__main__":
fire.Fire(lambda indir=None, out_by_subreddit=None, out_by_author=None: fire.Fire(lambda indir=None, out_by_subreddit=None, out_by_author=None,
num_partitions=800:
sort_and_write(COMMENTS, indir=indir, sort_and_write(COMMENTS, indir=indir,
out_by_subreddit=out_by_subreddit, out_by_subreddit=out_by_subreddit,
out_by_author=out_by_author)) out_by_author=out_by_author,
num_partitions=num_partitions))

View File

@@ -256,7 +256,8 @@ def gen_task_list(config, script_name, dumpdir=None, tasklist=None):
# --- Part 2: spark sort + repartition -------------------------------------- # --- Part 2: spark sort + repartition --------------------------------------
def sort_and_write(config, indir=None, out_by_subreddit=None, out_by_author=None): def sort_and_write(config, indir=None, out_by_subreddit=None, out_by_author=None,
num_partitions=None):
"""Read a directory of per-source parquets, sort and repartition twice """Read a directory of per-source parquets, sort and repartition twice
(once by subreddit, once by author), and write the two output datasets. (once by subreddit, once by author), and write the two output datasets.
@@ -265,6 +266,11 @@ def sort_and_write(config, indir=None, out_by_subreddit=None, out_by_author=None
and config['output_by_author']. Override them to write to staging directories and config['output_by_author']. Override them to write to staging directories
instead of the live datasets (see add_months.sh). instead of the live datasets (see add_months.sh).
num_partitions controls the number of output files per dataset. Target
~1GB per output file: use ~800 for a full 16-month comments batch,
~300 for submissions. Defaults to config['num_partitions'] if set,
otherwise 200.
Pyspark is imported lazily so Part 1 callers don't pay the Spark startup Pyspark is imported lazily so Part 1 callers don't pay the Spark startup
cost. cost.
""" """
@@ -273,6 +279,7 @@ def sort_and_write(config, indir=None, out_by_subreddit=None, out_by_author=None
indir = indir or config['outdir'] indir = indir or config['outdir']
out_by_subreddit = out_by_subreddit or config['output_by_subreddit'] out_by_subreddit = out_by_subreddit or config['output_by_subreddit']
out_by_author = out_by_author or config['output_by_author'] out_by_author = out_by_author or config['output_by_author']
num_partitions = num_partitions or config.get('num_partitions', 200)
spark = SparkSession.builder.appName(config['app_name']).getOrCreate() spark = SparkSession.builder.appName(config['app_name']).getOrCreate()
@@ -287,16 +294,21 @@ def sort_and_write(config, indir=None, out_by_subreddit=None, out_by_author=None
df = df.withColumn("Year", f.year(f.col("CreatedAt"))) df = df.withColumn("Year", f.year(f.col("CreatedAt")))
df = df.withColumn("Day", f.dayofmonth(f.col("CreatedAt"))) df = df.withColumn("Day", f.dayofmonth(f.col("CreatedAt")))
# Cache after preprocessing so the input is not re-read for each sort.
df = df.cache()
sub_keys = config['subreddit_sort_keys'] sub_keys = config['subreddit_sort_keys']
df_sub = df.repartition('subreddit').sort(sub_keys, ascending=True) df_sub = df.repartition(num_partitions, 'subreddit').sort(sub_keys, ascending=True)
df_sub = df_sub.sortWithinPartitions(sub_keys, ascending=True) df_sub = df_sub.sortWithinPartitions(sub_keys, ascending=True)
df_sub.write.parquet(out_by_subreddit, mode='overwrite', compression='snappy') df_sub.write.parquet(out_by_subreddit, mode='overwrite', compression='snappy')
auth_keys = config['author_sort_keys'] auth_keys = config['author_sort_keys']
df_auth = df.repartition('author').sort(auth_keys, ascending=True) df_auth = df.repartition(num_partitions, 'author').sort(auth_keys, ascending=True)
df_auth = df_auth.sortWithinPartitions(auth_keys, ascending=True) df_auth = df_auth.sortWithinPartitions(auth_keys, ascending=True)
df_auth.write.parquet(out_by_author, mode='overwrite', compression='snappy') df_auth.write.parquet(out_by_author, mode='overwrite', compression='snappy')
df.unpersist()
def merge_layers(config): def merge_layers(config):
"""Collapse all accumulated layers in the final datasets into a single """Collapse all accumulated layers in the final datasets into a single

View File

@@ -15,7 +15,9 @@ from dumps_helper import SUBMISSIONS, sort_and_write
if __name__ == "__main__": if __name__ == "__main__":
fire.Fire(lambda indir=None, out_by_subreddit=None, out_by_author=None: fire.Fire(lambda indir=None, out_by_subreddit=None, out_by_author=None,
num_partitions=300:
sort_and_write(SUBMISSIONS, indir=indir, sort_and_write(SUBMISSIONS, indir=indir,
out_by_subreddit=out_by_subreddit, out_by_subreddit=out_by_subreddit,
out_by_author=out_by_author)) out_by_author=out_by_author,
num_partitions=num_partitions))