Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
520cc26
Add `DmsModifyTaskOperator`
AlejandroMorgante May 26, 2026
2612396
Refine DmsModifyTaskOperator: add stop/restart params and fix state h…
AlejandroMorgante May 26, 2026
356c287
DmsModifyTaskOperator: safer defaults and fail-fast on terminal states
AlejandroMorgante May 26, 2026
4f7c3ff
Fix providers changelog: replace newsfragment with direct changelog e…
AlejandroMorgante May 27, 2026
fef7fea
Add execute_complete tests for DmsModifyTaskOperator
AlejandroMorgante May 27, 2026
959e788
Make DmsModifyTaskOperator fully non-blocking in deferrable mode
AlejandroMorgante May 27, 2026
ab5e126
Simplify DmsModifyTaskOperator: pure modify, no lifecycle orchestration
AlejandroMorgante May 28, 2026
b4cc351
Remove manual changelog edit — release manager regenerates from git log
AlejandroMorgante May 28, 2026
5404d6f
Complete DmsTaskState enum with all states from boto3 docs
AlejandroMorgante May 28, 2026
fca480f
Update DmsModifyTaskOperator docs: remove stop/restart params, point …
AlejandroMorgante May 28, 2026
bcfe8b1
Remove unused DmsTaskStoppedTrigger
AlejandroMorgante May 28, 2026
0ee3742
Fix DmsModifyTaskOperator docstring: deferrable defaults to False
AlejandroMorgante May 28, 2026
4ce684f
Fix exampleinclude path in DMS docs: remove duplicate providers/ prefix
AlejandroMorgante May 29, 2026
9710279
Address review comments on DmsModifyTaskOperator
AlejandroMorgante May 30, 2026
80fd3ea
Address second round of review comments on DmsModifyTaskOperator
AlejandroMorgante May 30, 2026
25cab96
Add DMS modification waiter flow
AlejandroMorgante Jun 2, 2026
7d43c45
Keep DMS modify return values consistent
AlejandroMorgante Jun 3, 2026
81374f6
Clarify DMS modify task preconditions
AlejandroMorgante Jun 3, 2026
efba8e6
Add DMS modify hook unit tests
AlejandroMorgante Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions providers/amazon/docs/operators/dms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ To create a replication task you can use
:start-after: [START howto_operator_dms_create_task]
:end-before: [END howto_operator_dms_create_task]

.. _howto/operator:DmsModifyTaskOperator:

Modify a replication task
=========================

To modify an existing replication task (e.g. to update table mappings for a backfill) you can use
:class:`~airflow.providers.amazon.aws.operators.dms.DmsModifyTaskOperator`.
The task must be stopped before modification — use :class:`~airflow.providers.amazon.aws.operators.dms.DmsStopTaskOperator`
upstream in the Dag, and :class:`~airflow.providers.amazon.aws.operators.dms.DmsStartTaskOperator`
downstream to restart it afterwards if needed.

.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_dms.py
:language: python
:dedent: 4
:start-after: [START howto_operator_dms_modify_task]
:end-before: [END howto_operator_dms_modify_task]

.. _howto/operator:DmsStartTaskOperator:

Start a replication task
Expand Down
68 changes: 68 additions & 0 deletions providers/amazon/src/airflow/providers/amazon/aws/hooks/dms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ class DmsTaskWaiterStatus(str, Enum):
STOPPED = "stopped"


class DmsTaskState(str, Enum):
"""
AWS DMS replication task states.

Source: https://docs.aws.amazon.com/boto3/latest/reference/services/dms/client/modify_replication_task.html
"""

CREATING = "creating"
READY = "ready"
STARTING = "starting"
RUNNING = "running"
STOPPING = "stopping"
STOPPED = "stopped"
MODIFYING = "modifying"
MOVING = "moving"
TESTING = "testing"
FAILED = "failed"
FAILED_MOVE = "failed-move"
DELETING = "deleting"


DMS_MODIFIABLE_STATES: frozenset[DmsTaskState] = frozenset(
{DmsTaskState.STOPPED, DmsTaskState.READY, DmsTaskState.FAILED}
)


class DmsHook(AwsBaseHook):
"""
Interact with AWS Database Migration Service (DMS).
Expand Down Expand Up @@ -200,6 +226,48 @@ def delete_replication_task(self, replication_task_arn):

self.wait_for_task_status(replication_task_arn, DmsTaskWaiterStatus.DELETED)

def modify_replication_task(
Comment thread
vincbeck marked this conversation as resolved.
self,
replication_task_arn: str,
table_mappings: dict | None = None,
migration_type: str | None = None,
replication_task_settings: dict | None = None,
cdc_start_time: datetime | None = None,
cdc_start_position: str | None = None,
cdc_stop_position: str | None = None,
) -> dict:
"""
Modify an existing replication task.

.. seealso::
- :external+boto3:py:meth:`DatabaseMigrationService.Client.modify_replication_task`

:param replication_task_arn: Replication task ARN
:param table_mappings: JSON table mappings dict
:param migration_type: Migration type ('full-load'|'cdc'|'full-load-and-cdc')
:param replication_task_settings: Task settings dict
:param cdc_start_time: Start time for CDC
:param cdc_start_position: CDC start position (checkpoint or LSN/SCN format)
:param cdc_stop_position: CDC stop position
:return: Modified replication task dict
"""
dms_client = self.get_conn()
kwargs: dict = {"ReplicationTaskArn": replication_task_arn}
if table_mappings is not None:
kwargs["TableMappings"] = json.dumps(table_mappings)
if migration_type is not None:
kwargs["MigrationType"] = migration_type
if replication_task_settings is not None:
kwargs["ReplicationTaskSettings"] = json.dumps(replication_task_settings)
if cdc_start_time is not None:
kwargs["CdcStartTime"] = cdc_start_time
if cdc_start_position is not None:
kwargs["CdcStartPosition"] = cdc_start_position
if cdc_stop_position is not None:
kwargs["CdcStopPosition"] = cdc_stop_position
response = dms_client.modify_replication_task(**kwargs)
return response.get("ReplicationTask", {})

def wait_for_task_status(self, replication_task_arn: str, status: DmsTaskWaiterStatus):
"""
Wait for replication task to reach status; supported statuses: deleted, ready, running, stopped.
Expand Down
169 changes: 168 additions & 1 deletion providers/amazon/src/airflow/providers/amazon/aws/operators/dms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@
from datetime import datetime
from typing import Any, ClassVar

from airflow.providers.amazon.aws.hooks.dms import DmsHook
from airflow.providers.amazon.aws.hooks.dms import DMS_MODIFIABLE_STATES, DmsHook, DmsTaskState
from airflow.providers.amazon.aws.operators.base_aws import AwsBaseOperator
from airflow.providers.amazon.aws.triggers.dms import (
DmsReplicationCompleteTrigger,
DmsReplicationConfigDeletedTrigger,
DmsReplicationDeprovisionedTrigger,
DmsReplicationStoppedTrigger,
DmsReplicationTerminalStatusTrigger,
DmsTaskModifyCompleteTrigger,
)
from airflow.providers.amazon.aws.utils import validate_execute_complete_event
from airflow.providers.amazon.aws.utils.mixins import aws_template_fields
Expand Down Expand Up @@ -118,6 +119,172 @@ def execute(self, context: Context):
return task_arn


class DmsModifyTaskOperator(AwsBaseOperator[DmsHook]):
"""
Modifies an existing AWS DMS replication task.

The task must already be in a modifiable state before modification.
Use :class:`DmsStopTaskOperator` upstream in the Dag to stop it, and
:class:`DmsStartTaskOperator` downstream to restart it afterwards if needed.

Valid modifiable states are ``stopped``, ``ready``, and ``failed``.

.. seealso::
For more information on how to use this operator, take a look at the guide:
:ref:`howto/operator:DmsModifyTaskOperator`

:param replication_task_arn: Replication task ARN
:param table_mappings: New table mappings. If not provided, existing mappings are kept.
:param migration_type: Migration type ('full-load'|'cdc'|'full-load-and-cdc').
If not provided, existing type is kept.
:param replication_task_settings: Task settings dict. If not provided, existing settings are kept.
:param cdc_start_time: Start time for CDC.
:param cdc_start_position: Indicates when to start CDC (checkpoint or LSN/SCN format).
Mutually exclusive with cdc_start_time.
:param cdc_stop_position: Indicates when to stop CDC.
:param wait_for_completion: If True, wait for the modification to finish before returning.
In deferrable mode the operator defers rather than blocking. Defaults to True.
:param deferrable: Run the operator in deferrable mode. Defaults to False.
:param waiter_delay: Seconds between waiter polls (default: 30).
:param waiter_max_attempts: Maximum waiter poll attempts (default: 60).
:param aws_conn_id: The Airflow connection used for AWS credentials.
If this is ``None`` or empty then the default boto3 behaviour is used. If
running Airflow in a distributed manner and aws_conn_id is None or
empty, then default boto3 configuration would be used (and must be
maintained on each worker node).
:param region_name: AWS region_name. If not specified then the default boto3 behaviour is used.
:param verify: Whether or not to verify SSL certificates. See:
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
:param botocore_config: Configuration dictionary (key-values) for botocore client. See:
https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
"""
Comment thread
AlejandroMorgante marked this conversation as resolved.

MODIFIABLE_STATES = DMS_MODIFIABLE_STATES

aws_hook_class = DmsHook
template_fields: Sequence[str] = aws_template_fields(
"replication_task_arn",
"table_mappings",
"migration_type",
"replication_task_settings",
"cdc_start_time",
"cdc_start_position",
"cdc_stop_position",
)
template_fields_renderers: ClassVar[dict] = {
"table_mappings": "json",
"replication_task_settings": "json",
}

def __init__(
self,
*,
replication_task_arn: str,
table_mappings: dict | None = None,
migration_type: str | None = None,
replication_task_settings: dict | None = None,
cdc_start_time: datetime | None = None,
cdc_start_position: str | None = None,
cdc_stop_position: str | None = None,
wait_for_completion: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
waiter_delay: int = 30,
waiter_max_attempts: int = 60,
Comment thread
AlejandroMorgante marked this conversation as resolved.
aws_conn_id: str | None = "aws_default",
**kwargs,
):
super().__init__(aws_conn_id=aws_conn_id, **kwargs)
if cdc_start_time and cdc_start_position:
raise ValueError("Only one of cdc_start_time or cdc_start_position can be provided.")
self.replication_task_arn = replication_task_arn
self.table_mappings = table_mappings
self.migration_type = migration_type
self.replication_task_settings = replication_task_settings
self.cdc_start_time = cdc_start_time
self.cdc_start_position = cdc_start_position
self.cdc_stop_position = cdc_stop_position
self.wait_for_completion = wait_for_completion
self.deferrable = deferrable
self.waiter_delay = waiter_delay
self.waiter_max_attempts = waiter_max_attempts

def _wait_for_modification_completion(self) -> None:
self.hook.get_waiter("replication_task_modified").wait(
Filters=[{"Name": "replication-task-arn", "Values": [self.replication_task_arn]}],
WithoutSettings=True,
WaiterConfig={"Delay": self.waiter_delay, "MaxAttempts": self.waiter_max_attempts},
)

def execute(self, context: Context) -> dict:
tasks = self.hook.find_replication_tasks_by_arn(
replication_task_arn=self.replication_task_arn, without_settings=True
)
if not tasks:
raise ValueError(f"Replication task {self.replication_task_arn} not found.")

current_status = tasks[0].get("Status", "").lower()
self.log.info(
"Current status of replication task(%s) is '%s'.", self.replication_task_arn, current_status
)

if current_status == DmsTaskState.MODIFYING:
self._wait_for_modification_completion()
tasks = self.hook.find_replication_tasks_by_arn(
replication_task_arn=self.replication_task_arn, without_settings=True
)
if not tasks:
raise ValueError(f"Replication task {self.replication_task_arn} not found.")
current_status = tasks[0].get("Status", "").lower()

if current_status not in self.MODIFIABLE_STATES:
raise RuntimeError(
f"Replication task {self.replication_task_arn} is in state '{current_status}' "
f"and must be in a modifiable state (stopped, ready, or failed) before modification. "
f"Use DmsStopTaskOperator to stop it first."
Comment thread
AlejandroMorgante marked this conversation as resolved.
)

result = self.hook.modify_replication_task(
replication_task_arn=self.replication_task_arn,
table_mappings=self.table_mappings,
migration_type=self.migration_type,
replication_task_settings=self.replication_task_settings,
cdc_start_time=self.cdc_start_time,
cdc_start_position=self.cdc_start_position,
cdc_stop_position=self.cdc_stop_position,
)
self.log.info("DMS replication task(%s) has been modified.", self.replication_task_arn)

if self.wait_for_completion:
if self.deferrable:
self.defer(
trigger=DmsTaskModifyCompleteTrigger(
replication_task_arn=self.replication_task_arn,
waiter_delay=self.waiter_delay,
waiter_max_attempts=self.waiter_max_attempts,
aws_conn_id=self.aws_conn_id,
),
method_name="execute_complete",
kwargs={"result": result},
)
Comment thread
AlejandroMorgante marked this conversation as resolved.
else:
self._wait_for_modification_completion()

return result

def execute_complete(
self, context: Context, event: dict | None = None, result: dict | None = None
) -> dict:
validated_event = validate_execute_complete_event(event)
if validated_event["status"] != "success":
raise RuntimeError(f"Error waiting for DMS task modification to complete: {validated_event}")
replication_task_arn = validated_event["replication_task_arn"]
self.log.info(
"DMS replication task(%s) modification complete.",
replication_task_arn,
)
return result or {}


class DmsDeleteTaskOperator(AwsBaseOperator[DmsHook]):
"""
Deletes AWS DMS replication task.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,51 @@ def hook(self) -> AwsGenericHook:
verify=self.verify,
config=self.botocore_config,
)


class DmsTaskModifyCompleteTrigger(AwsBaseWaiterTrigger):
"""
Trigger when a DMS classic replication task modification completes.

:param replication_task_arn: The ARN of the replication task.
:param waiter_delay: The amount of time in seconds to wait between attempts.
:param waiter_max_attempts: The maximum number of attempts to be made.
:param aws_conn_id: The Airflow connection used for AWS credentials.
:param verify: Whether or not to verify SSL certificates.
:param botocore_config: Configuration dictionary (key-values) for botocore client.
"""

def __init__(
self,
replication_task_arn: str,
waiter_delay: int = 30,
waiter_max_attempts: int = 60,
aws_conn_id: str | None = "aws_default",
verify: bool | str | None = None,
botocore_config: dict | None = None,
) -> None:
super().__init__(
serialized_fields={"replication_task_arn": replication_task_arn},
waiter_name="replication_task_modified",
waiter_delay=waiter_delay,
waiter_args={
"Filters": [{"Name": "replication-task-arn", "Values": [replication_task_arn]}],
"WithoutSettings": True,
},
waiter_max_attempts=waiter_max_attempts,
failure_message="Replication task modification failed to complete.",
status_message="Status replication task is",
status_queries=["ReplicationTasks[0].Status"],
return_key="replication_task_arn",
return_value=replication_task_arn,
aws_conn_id=aws_conn_id,
verify=verify,
botocore_config=botocore_config,
)

def hook(self) -> AwsGenericHook:
return DmsHook(
self.aws_conn_id,
verify=self.verify,
config=self.botocore_config,
)
Loading
Loading