5454
5555# Iceberg ships inside the EMR image; this is a local path in the container, not
5656# a download. See the EMR Serverless "Using Apache Iceberg" documentation.
57- ICEBERG_JAR = "/usr/share/aws/iceberg/lib/iceberg-spark3-runtime.jar"
57+ #
58+ # The documented path is the default below. It is overridable because the layout
59+ # is release-dependent: this exact value does not exist on emr-spark-8.0.0 (the
60+ # jar name carries "spark3", and EMR 8 runs Spark 4), where the job fails with
61+ # NoSuchFileException. Set ICEBERG_JAR_PATH to the real path for your release,
62+ # or to an empty string to omit spark.jars entirely when Iceberg is already on
63+ # the default classpath. Run with PROBE=1 to have the image report its layout.
64+ ICEBERG_JAR = os .environ .get (
65+ "ICEBERG_JAR_PATH" , "/usr/share/aws/iceberg/lib/iceberg-spark3-runtime.jar"
66+ )
67+ PROBE = os .environ .get ("PROBE" , "" ).lower () in ("1" , "true" , "yes" )
5868
5969# Catalog implementations per storage mode.
6070GLUE_CATALOG_IMPL = "org.apache.iceberg.aws.glue.GlueCatalog"
@@ -141,10 +151,12 @@ def _wait_for(get_state, want: set, bad: set, what: str, timeout_s: int = 600) -
141151def spark_submit_params (mode : str , catalog_impl : str , warehouse : str ) -> str :
142152 jars = ICEBERG_JAR
143153 if mode == "s3tables" and S3TABLES_EXTRA_JARS :
144- jars = f" { jars } , { S3TABLES_EXTRA_JARS } "
154+ jars = "," . join ( j for j in ( jars , S3TABLES_EXTRA_JARS ) if j )
145155
146- params = [
147- f"--conf spark.jars={ jars } " ,
156+ params = []
157+ if jars :
158+ params .append (f"--conf spark.jars={ jars } " )
159+ params += [
148160 "--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions" ,
149161 "--conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog" ,
150162 f"--conf spark.sql.catalog.local.catalog-impl={ catalog_impl } " ,
@@ -159,6 +171,43 @@ def spark_submit_params(mode: str, catalog_impl: str, warehouse: str) -> str:
159171 return " " .join (params )
160172
161173
174+ def run_probe (app_id : str , probe_uri : str ) -> int :
175+ """Submit the diagnostic job and print where its output landed.
176+
177+ Cheap way to settle release-dependent questions (jar path, whether Iceberg is
178+ already on the classpath, whether the S3 Tables catalog exists) instead of
179+ guessing across job runs. Output goes to the driver stdout log in S3.
180+ """
181+ print ("\n [driver] === probe: reporting the image layout ===" )
182+ resp = emr .start_job_run (
183+ applicationId = app_id ,
184+ executionRoleArn = JOB_ROLE_ARN ,
185+ name = f"{ RESOURCE_PREFIX } -probe" [:64 ],
186+ executionTimeoutMinutes = 15 ,
187+ jobDriver = {"sparkSubmit" : {"entryPoint" : probe_uri , "entryPointArguments" : [],
188+ "sparkSubmitParameters" : "" }},
189+ configurationOverrides = {
190+ "monitoringConfiguration" : {
191+ "s3MonitoringConfiguration" : {"logUri" : s3_uri (ENGINE , "logs" , RUN_TAG ) + "/" }
192+ }
193+ },
194+ tags = {"project" : "iceberg-matrix" , "run" : RUN_TAG , "mode" : "probe" },
195+ )
196+ job_id = resp ["jobRunId" ]
197+ state = _wait_for (
198+ lambda : emr .get_job_run (applicationId = app_id , jobRunId = job_id )["jobRun" ]["state" ],
199+ want = {"SUCCESS" , "FAILED" , "CANCELLED" }, bad = set (),
200+ what = f"probe job { job_id } " , timeout_s = 1200 ,
201+ )
202+ log_prefix = f"{ ENGINE } /logs/{ RUN_TAG } /applications/{ app_id } /jobs/{ job_id } /"
203+ print (f"[driver] probe { state } " )
204+ print (f"[driver] read the PROBE lines from the driver stdout under:" )
205+ print (f"[driver] s3://{ DATA_BUCKET } /{ log_prefix } SPARK_DRIVER/stdout.gz" )
206+ print (f"[driver] e.g. aws s3 cp s3://{ DATA_BUCKET } /{ log_prefix } SPARK_DRIVER/stdout.gz - "
207+ "| gunzip | grep '^PROBE'" )
208+ return 0 if state == "SUCCESS" else 1
209+
210+
162211def run_mode (app_id : str , mode : str , bundle_uri : str , entry_uri : str ) -> dict :
163212 if mode == "s3buckets" :
164213 catalog_impl = GLUE_CATALOG_IMPL
@@ -294,6 +343,16 @@ def main() -> int:
294343 modes = ["s3buckets" , "s3tables" ] if MODES == "both" else [MODES ]
295344 print (f"[driver] region={ REGION } bucket={ DATA_BUCKET } modes={ modes } " )
296345
346+ if PROBE :
347+ probe_uri = upload (Path (__file__ ).with_name ("emr_probe.py" ),
348+ f"{ ENGINE } /scripts/{ RUN_TAG } /emr_probe.py" )
349+ app_id = create_application ()
350+ Path ("/tmp/emr-application-id" ).write_text (app_id )
351+ if os .environ .get ("GITHUB_ENV" ):
352+ with open (os .environ ["GITHUB_ENV" ], "a" ) as f :
353+ f .write (f"EMR_APPLICATION_ID={ app_id } \n " )
354+ return run_probe (app_id , probe_uri )
355+
297356 bundle = build_bundle (Path ("/tmp" ) / f"{ RUN_TAG } -bundle.zip" )
298357 bundle_uri = upload (bundle , f"{ ENGINE } /scripts/{ RUN_TAG } /bundle.zip" )
299358 entry_uri = upload (Path (__file__ ).with_name ("emr_entrypoint.py" ),
0 commit comments