12.9 C
Canberra
Sunday, April 27, 2025

Handle concurrent write conflicts in Apache Iceberg on the AWS Glue Knowledge Catalog


In fashionable information architectures, Apache Iceberg has emerged as a well-liked desk format for information lakes, providing key options together with ACID transactions and concurrent write assist. Though these capabilities are highly effective, implementing them successfully in manufacturing environments presents distinctive challenges that require cautious consideration.

Take into account a standard state of affairs: A streaming pipeline repeatedly writes information to an Iceberg desk whereas scheduled upkeep jobs carry out compaction operations. Though Iceberg offers built-in mechanisms to deal with concurrent writes, sure battle situations—comparable to between streaming updates and compaction operations—can result in transaction failures that require particular dealing with patterns.

This submit demonstrates learn how to implement dependable concurrent write dealing with mechanisms in Iceberg tables. We are going to discover Iceberg’s concurrency mannequin, look at frequent battle situations, and supply sensible implementation patterns of each automated retry mechanisms and conditions requiring customized battle decision logic for constructing resilient information pipelines. We can even cowl the sample with automated compaction via AWS Glue Knowledge Catalog desk optimization.

Frequent battle situations

Probably the most frequent information conflicts happen in a number of particular operational situations that many organizations encounter of their information pipelines, which we focus on on this part.

Concurrent UPDATE/DELETE on overlapping partitions

When a number of processes try to change the identical partition concurrently, information conflicts can come up. For instance, think about an information high quality course of updating buyer data with corrected addresses whereas one other course of is deleting outdated buyer data. Each operations goal the identical partition based mostly on customer_id, resulting in potential conflicts as a result of they’re modifying an overlapping dataset. These conflicts are notably frequent in large-scale information cleanup operations.

Compaction vs. streaming writes

A traditional battle state of affairs happens throughout desk upkeep operations. Take into account a streaming pipeline ingesting real-time occasion information whereas a scheduled compaction job runs to optimize file sizes. The streaming course of could be writing new data to a partition whereas the compaction job is trying to mix present information in the identical partition. This state of affairs is very frequent with Knowledge Catalog desk optimization, the place automated compaction can run concurrently with steady information ingestion.

Concurrent MERGE operations

MERGE operations are notably vulnerable to conflicts as a result of they contain each studying and writing information. For example, an hourly job could be merging buyer profile updates from a supply system whereas a separate job is merging choice updates from one other system. If each jobs try to change the identical buyer data, they’ll battle as a result of every operation bases its modifications on a distinct view of the present information state.

Basic concurrent desk updates

When a number of transactions happen concurrently, some transactions may fail to decide to the catalog because of interference from different transactions. Iceberg has mechanisms to deal with this state of affairs, so it may well adapt to concurrent transactions in lots of instances. Nonetheless, commits can nonetheless fail if the newest metadata is up to date after the bottom metadata model is established. This state of affairs applies to any kind of updates on an Iceberg desk.

Iceberg’s concurrency mannequin and battle kind

Earlier than diving into particular implementation patterns, it’s important to know how Iceberg manages concurrent writes via its desk structure and transaction mannequin. Iceberg makes use of a layered structure to handle desk state and information:

  • Catalog layer – Maintains a pointer to the present desk metadata file, serving as the one supply of reality for desk state. The Knowledge Catalog offers the performance because the Iceberg catalog.
  • Metadata layer – Incorporates metadata information that observe desk historical past, schema evolution, and snapshot info. These information are saved on Amazon Easy Storage Service (Amazon S3).
  • Knowledge layer – Shops the precise information information and delete information (for Merge-on-Learn operations). These information are additionally saved on Amazon S3.

The next diagram illustrates this structure.

This structure is prime to Iceberg’s optimistic concurrency management, the place a number of writers can proceed with their operations concurrently, and conflicts are detected at commit time.

Write transaction circulate

A typical write transaction in Iceberg follows these key steps:

  1. Learn present state. In lots of operations (like OVERWRITE, MERGE, and DELETE), the question engine must know which information or rows are related, so it reads the present desk snapshot. That is elective for operations like INSERT.
  2. Decide the modifications in transaction, and write new information information.
  3. Load the desk’s newest metadata, and decide which metadata model is used as the bottom for the replace.
  4. Verify if the change ready in Step 2 is suitable with the newest desk information in Step 3. If the examine failed, the transaction should cease.
  5. Generate new metadata information.
  6. Commit the metadata information to the catalog. If the commit failed, retry from Step 3. The variety of retries depends upon the configuration.

The next diagram illustrates this workflow.

Iceberg write transaction flow

Conflicts can happen at two vital factors:

  • Knowledge replace conflicts – Throughout validation when checking for information conflicts (Step 4)
  • Catalog commit conflicts – In the course of the commit when trying to replace the catalog pointer (Step 6)

When working with Iceberg tables, understanding the forms of conflicts that may happen and the way they’re dealt with is essential for constructing dependable information pipelines. Let’s look at the 2 main forms of conflicts and their traits.

Catalog commit conflicts

Catalog commit conflicts happen when a number of writers try and replace the desk metadata concurrently. When a commit battle happens, Iceberg will routinely retry the operation based mostly on the desk’s write properties. The retry course of solely repeats the metadata commit, not all the transaction, making it each secure and environment friendly. When the retries fail, the transaction fails with CommitFailedException.

Within the following diagram, two transactions run concurrently. Transaction 1 efficiently updates the desk’s newest snapshot within the Iceberg catalog from 0 to 1. In the meantime, transaction 2 makes an attempt to replace from Snapshot 0 to 1, however when it tries to commit the modifications to the catalog, it finds that the newest snapshot has already been modified to 1 by transaction 1. Consequently, transaction 2 must retry from Step 3.

Catalog commit conflicts1

These conflicts are usually transient and will be routinely resolved via retries. You’ll be able to optionally configure write properties controlling commit retry habits. For extra detailed configuration, confer with Write properties within the Iceberg documentation.

The metadata used when studying the present state (Step 1) and the snapshot used as base metadata for updates (Step 3) will be completely different. Even when one other transaction updates the newest snapshot between Steps 1 and three, the present transaction can nonetheless commit modifications to the catalog so long as it passes the information battle examine (Step 4). Because of this even when computing modifications and writing information information (Step 1 to 2) take a very long time, and different transactions make modifications throughout this era, the transaction can nonetheless try and decide to the catalog. This demonstrates Iceberg’s clever concurrency management mechanism.

The next diagram illustrates this workflow.

Catalog commit conflicts2

Knowledge replace conflicts

Knowledge replace conflicts are extra advanced and happen when concurrent transactions try to change overlapping information. Throughout a write transaction, the question engine checks consistency between the snapshot being written and the newest snapshot based on transaction isolation guidelines. When incompatibility is detected, the transaction fails with a ValidationException.

Within the following diagram, two transactions run concurrently on an worker desk containing id, title, and wage columns. Transaction 1 makes an attempt to replace a report based mostly on Snapshot 0 and efficiently commits this modification, making the newest snapshot model 1. In the meantime, transaction 2 additionally makes an attempt to replace the identical report based mostly on Snapshot 0. When transaction 2 initially scanned the information, the newest snapshot was 0, however it has since been up to date to 1 by transaction 1. In the course of the information battle examine, transaction 2 discovers that its modifications battle with Snapshot 1, ensuing within the transaction failing.

data conflict

These conflicts can’t be routinely retried by Iceberg’s library as a result of when information conflicts happen, the desk’s state has modified, making it unsure whether or not retrying the transaction would preserve total information consistency. You might want to deal with one of these battle based mostly in your particular use case and necessities.

The next desk summarizes how completely different write patterns have various chance of conflicts.

Write Sample Catalog Commit Battle (Routinely retryable) Knowledge Battle (Non-retryable)
INSERT (AppendFiles) Sure By no means
UPDATE/DELETE with Copy-on-Write or Merge-on-Learn (OverwriteFiles) Sure Sure
Compaction (RewriteFiles) Sure Sure

Iceberg desk’s isolation ranges

Iceberg tables assist two isolation ranges: Serializable and Snapshot isolation. Each present a learn constant view of the desk and guarantee readers see solely dedicated information. Serializable isolation ensures that concurrent operations run as in the event that they had been carried out in some sequential order. Snapshot isolation offers weaker ensures however provides higher efficiency in environments with many concurrent writers. Beneath snapshot isolation, information battle checks can cross even when concurrent transactions add new information with data that probably match its situations.

By default, Iceberg tables use serializable isolation. You’ll be able to configure isolation ranges for particular operations utilizing desk properties:

tbl_properties = {
    'write.delete.isolation-level' = 'serializable',
    'write.replace.isolation-level' = 'serializable',
    'write.merge.isolation-level' = 'serializable'
}

You need to select the suitable isolation stage based mostly in your use case. Observe that for conflicts between streaming ingestion and compaction operations, which is likely one of the commonest situations, snapshot isolation doesn’t present any further advantages to the default serializable isolation. For extra detailed configuration, see IsolationLevel.

Implementation patterns

Implementing sturdy concurrent write dealing with in Iceberg requires completely different methods relying on the battle kind and use case. On this part, we share confirmed patterns for dealing with frequent situations.

Handle catalog commit conflicts

Catalog commit conflicts are comparatively easy to deal with via desk properties. The next configurations function preliminary baseline settings you could modify based mostly in your particular workload patterns and necessities.

For frequent concurrent writes (for instance, streaming ingestion):

tbl_properties = {
    'commit.retry.num-retries': '10',
    'commit.retry.min-wait-ms': '100',
    'commit.retry.max-wait-ms': '10000',
    'commit.retry.total-timeout-ms': '1800000'
}

For upkeep operations (for instance, compaction):

tbl_properties = {
    'commit.retry.num-retries': '4',
    'commit.retry.min-wait-ms': '1000',
    'commit.retry.max-wait-ms': '60000',
    'commit.retry.total-timeout-ms': '1800000'
}

Handle information replace conflicts

For information replace conflicts, which might’t be routinely retried, you’ll want to implement a customized retry mechanism with correct error dealing with. A typical state of affairs is when stream UPSERT ingestion conflicts with concurrent compaction operations. In such instances, the stream ingestion job ought to usually implement retries to deal with incoming information. With out correct error dealing with, the job will fail with a ValidationException.

We present two instance scripts demonstrating a sensible implementation of error dealing with for information conflicts in Iceberg streaming jobs. The code particularly catches ValidationException via Py4JJavaError dealing with, which is crucial for correct Java-Python interplay. It contains exponential backoff and jitter technique by including a random delay of 0–25% to every retry interval. For instance, if the bottom exponential backoff time is 4 seconds, the precise retry delay will likely be between 4–5 seconds, serving to stop quick retry storms whereas sustaining cheap latency.

On this instance, we create a state of affairs with frequent MERGE operations on the identical data by utilizing 'worth' as a singular identifier and artificially limiting its vary. By making use of a modulo operation (worth % 20), we constrain all values to fall inside 0–19, which suggests a number of updates will goal the identical data. For example, if the unique stream accommodates values 0, 20, 40, and 60, they’ll all be mapped to 0, leading to a number of MERGE operations concentrating on the identical report. We then use groupBy and max aggregation to simulate a typical UPSERT sample the place we hold the newest report for every worth. The remodeled information is saved in a short lived view that serves because the supply desk within the MERGE assertion, permitting us to carry out UPDATE operations utilizing 'worth' because the matching situation. This setup helps show how our retry mechanism handles ValidationExceptions that happen when concurrent transactions try to change the identical data.

The primary instance makes use of Spark Structured Streaming utilizing a fee supply with a 20-second set off interval to show the retry mechanism’s habits when concurrent operations trigger information conflicts. Change together with your database title, together with your desk title, amzn-s3-demo-bucket together with your S3 bucket title.

import time
import random
from pyspark.sql import SparkSession
from py4j.protocol import Py4JJavaError
from pyspark.sql.capabilities import max as max_

CATALOG = "glue_catalog"
DATABASE = ""
TABLE = ""
BUCKET = "amzn-s3-demo-bucket"

spark = SparkSession.builder 
    .appName("IcebergUpsertExample") 
    .config(f"spark.sql.catalog.{CATALOG}", "org.apache.iceberg.spark.SparkCatalog") 
    .config("spark.sql.extensions","org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") 
    .config(f"spark.sql.catalog.{CATALOG}.io-impl","org.apache.iceberg.aws.s3.S3FileIO") 
    .config("spark.sql.defaultCatalog", CATALOG) 
    .config(f"spark.sql.catalog.{CATALOG}.kind", "glue") 
    .getOrCreate()
    
spark.sql(f"""
    CREATE TABLE IF NOT EXISTS {DATABASE}.{TABLE} (
        timestamp TIMESTAMP,
        worth LONG
    )
    USING iceberg
    LOCATION 's3://{BUCKET}/warehouse'
""")

def backoff(try):
    """Exponential backoff with jitter"""
    exp_backoff = min(2 ** try, 60)
    jitter = random.uniform(0, 0.25 * exp_backoff)
    return exp_backoff + jitter

def is_validation_exception(java_exception):
    """Verify if exception is ValidationException"""
    trigger = java_exception
    whereas trigger is just not None:
        if "org.apache.iceberg.exceptions.ValidationException" in str(trigger.getClass().getName()):
            return True
        trigger = trigger.getCause()
    return False

def upsert_with_retry(microBatchDF, batchId):
    max_retries = 5
    try = 0
    
    # Use a narrower key vary to deliberately enhance updates for a similar worth in MERGE
    transformedDF = microBatchDF 
        .selectExpr("timestamp", "worth % 20 AS worth") 
        .groupBy("worth") 
        .agg(max_("timestamp").alias("timestamp"))
        
    view_name = f"incoming_data_{batchId}"
    transformedDF.createOrReplaceGlobalTempView(view_name)
    
    whereas try < max_retries:
        attempt:
            spark.sql(f"""
                MERGE INTO {DATABASE}.{TABLE} AS t
                USING global_temp.{view_name} AS i
                ON t.worth = i.worth
                WHEN MATCHED THEN
                  UPDATE SET
                    t.timestamp = i.timestamp,
                    t.worth     = i.worth
                WHEN NOT MATCHED THEN
                  INSERT (timestamp, worth)
                  VALUES (i.timestamp, i.worth)
            """)
            
            print(f"[SUCCESS] Batch {batchId} processed efficiently")
            return
            
        besides Py4JJavaError as e:
            if is_validation_exception(e.java_exception):
                try += 1
                if try < max_retries:
                    delay = backoff(try)
                    print(f"[RETRY] Batch {batchId} failed with ValidationException. "
                          f"Retrying in {delay} seconds. Try {try}/{max_retries}")
                    time.sleep(delay)
                else:
                    print(f"[FAILED] Batch {batchId} failed after {max_retries} makes an attempt")
                    elevate

# Pattern streaming question setup
df = spark.readStream 
    .format("fee") 
    .possibility("rowsPerSecond", 10) 
    .load()

# Begin streaming question
question = df.writeStream 
    .set off(processingTime="20 seconds") 
    .possibility("checkpointLocation", f"s3://{BUCKET}/checkpointLocation") 
    .foreachBatch(upsert_with_retry) 
    .begin()

question.awaitTermination()

The second instance makes use of GlueContext.forEachBatch out there on AWS Glue Streaming jobs. The implementation sample for the retry mechanism stays the identical, however the primary variations are the preliminary setup utilizing GlueContext and learn how to create a streaming DataFrame. Though our instance makes use of spark.readStream with a fee supply for demonstration, in precise AWS Glue Streaming jobs, you’ll usually create your streaming DataFrame utilizing glueContext.create_data_frame.from_catalog to learn from sources like Amazon Kinesis or Kafka. For extra particulars, see AWS Glue Streaming connections. Change together with your database title, together with your desk title, amzn-s3-demo-bucket together with your S3 bucket title.

import time
import random
from py4j.protocol import Py4JJavaError
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from pyspark.sql import SparkSession
from pyspark.sql.capabilities import max as max_

CATALOG = "glue_catalog"
DATABASE = ""
TABLE = ""
BUCKET = "amzn-s3-demo-bucket"

spark = SparkSession.builder 
    .appName("IcebergUpsertExample") 
    .config(f"spark.sql.catalog.{CATALOG}", "org.apache.iceberg.spark.SparkCatalog") 
    .config("spark.sql.extensions","org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") 
    .config(f"spark.sql.catalog.{CATALOG}.io-impl","org.apache.iceberg.aws.s3.S3FileIO") 
    .config("spark.sql.defaultCatalog", CATALOG) 
    .config(f"spark.sql.catalog.{CATALOG}.kind", "glue") 
    .getOrCreate()

sc = spark.sparkContext
glueContext = GlueContext(sc)

spark.sql(f"""
    CREATE TABLE IF NOT EXISTS {DATABASE}.{TABLE} (
        timestamp TIMESTAMP,
        worth LONG
    )
    USING iceberg
    LOCATION 's3://{BUCKET}/warehouse'
""")

def backoff(try):
    exp_backoff = min(2 ** try, 60)
    jitter = random.uniform(0, 0.25 * exp_backoff)
    return exp_backoff + jitter

def is_validation_exception(java_exception):
    trigger = java_exception
    whereas trigger is just not None:
        if "org.apache.iceberg.exceptions.ValidationException" in str(trigger.getClass().getName()):
            return True
        trigger = trigger.getCause()
    return False

def upsert_with_retry(batch_df, batchId):
    max_retries = 5
    try = 0
    transformedDF = batch_df.selectExpr("timestamp", "worth % 20 AS worth") 
                           .groupBy("worth") 
                           .agg(max_("timestamp").alias("timestamp"))
                           
    view_name = f"incoming_data_{batchId}"
    transformedDF.createOrReplaceGlobalTempView(view_name)
    
    whereas try < max_retries:
        attempt:
            spark.sql(f"""
                MERGE INTO {DATABASE}.{TABLE} AS t
                USING global_temp.{view_name} AS i
                ON t.worth = i.worth
                WHEN MATCHED THEN
                  UPDATE SET
                    t.timestamp = i.timestamp,
                    t.worth     = i.worth
                WHEN NOT MATCHED THEN
                  INSERT (timestamp, worth)
                  VALUES (i.timestamp, i.worth)
            """)
            print(f"[SUCCESS] Batch {batchId} processed efficiently")
            return
        besides Py4JJavaError as e:
            if is_validation_exception(e.java_exception):
                try += 1
                if try < max_retries:
                    delay = backoff(try)
                    print(f"[RETRY] Batch {batchId} failed with ValidationException. "
                          f"Retrying in {delay} seconds. Try {try}/{max_retries}")
                    time.sleep(delay)
                else:
                    print(f"[FAILED] Batch {batchId} failed after {max_retries} makes an attempt")
                    elevate

# Pattern streaming question setup
streaming_df = spark.readStream 
    .format("fee") 
    .possibility("rowsPerSecond", 10) 
    .load()

# In precise Glue Streaming jobs, you'll usually create a streaming DataFrame like this:
"""
streaming_df = glueContext.create_data_frame.from_catalog(
    database = "database",
    table_name = "table_name",
    transformation_ctx = "streaming_df",
    additional_options = {
        "startingPosition": "TRIM_HORIZON",
        "inferSchema": "false"
    }
)
"""

glueContext.forEachBatch(
    body=streaming_df,
    batch_function=upsert_with_retry,
    choices={
        "windowSize": "20 seconds",
        "checkpointLocation": f"s3://{BUCKET}/checkpointLocation"
    }
)

Reduce battle risk by scoping your operations

When performing upkeep operations like compaction or updates, it’s beneficial to slim down the scope to attenuate overlap with different operations. For instance, think about a desk partitioned by date the place a streaming job repeatedly upserts information for the newest date. The next is the instance script to run the rewrite_data_files process to compact all the desk:

# Instance of broad scope compaction
spark.sql("""
   CALL catalog_name.system.rewrite_data_files(
       desk => 'db.table_name'
   )
""")

By narrowing the compaction scope with a date partition filter within the the place clause, you’ll be able to keep away from conflicts between streaming ingestion and compaction operations. The streaming job can proceed to work with the newest partition whereas compaction processes historic information.

# Slender down the scope by partition
spark.sql("""
    CALL catalog_name.system.rewrite_data_files(
        desk => 'db.table_name',
        the place => 'date_partition < current_date'
    )
""")

Conclusion

Efficiently managing concurrent writes in Iceberg requires understanding each the desk structure and varied battle situations. On this submit, we explored learn how to implement dependable battle dealing with mechanisms in manufacturing environments.

Probably the most vital idea to recollect is the excellence between catalog commit conflicts and information conflicts. Though catalog commit conflicts will be dealt with via automated retries and desk properties configuration, information conflicts require cautious implementation of customized dealing with logic. This turns into notably essential when implementing upkeep operations like compaction, the place utilizing the the place clause in rewrite_data_files can considerably decrease battle potential by lowering the scope of operations.

For streaming pipelines, the important thing to success lies in implementing correct error dealing with that may differentiate between battle varieties and reply appropriately. This contains configuring appropriate retry settings via desk properties and implementing backoff methods that align together with your workload traits. When mixed with well-timed upkeep operations, these patterns assist construct resilient information pipelines that may deal with concurrent writes reliably.

By making use of these patterns and understanding the underlying mechanisms of Iceberg’s concurrency mannequin, you’ll be able to construct sturdy information pipelines that successfully deal with concurrent write situations whereas sustaining information consistency and reliability.


Concerning the Authors

Sotaro Hikita is an Analytics Options Architect. He helps clients throughout a variety of industries in constructing and working analytics platforms extra successfully. He’s notably keen about large information applied sciences and open supply software program.

Noritaka Sekiyama is a Principal Huge Knowledge Architect on the AWS Glue workforce. He works based mostly in Tokyo, Japan. He’s answerable for constructing software program artifacts to assist clients. In his spare time, he enjoys biking together with his highway bike.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles