Skip to content

Commit 621089d

Browse files
authored
AWS Step Functions Integration (#211)
Integrates Metaflow with [AWS Step Functions](https://aws.amazon.com/step-functions/). Introduces a new command `step-functions`: - `python my_flow.py step-functions create` will compile and export the user-defined Metaflow flow into an AWS Step Functions state machine. This will allow users of Metaflow to move their flows into production seamlessly with AWS. - An additional flow level decorator, `@schedule`, allows users to optionally schedule the execution of their flows by integrating with AWS Event Bridge. - All current functionality of Metaflow - containerized job execution on top of AWS Batch through `@batch`, dependency management via `@conda`, retrying mechanisms through `@retry`, `@catch` and `@timeout`, parameters, branches, and for-eaches are now available within AWS Step Functions through this integration. - Additionally, introduces a notion of `production token` to ensure flow deployments to AWS Step Functions have proper safeguards against unintended production deployments - `python my_flow.py step-functions trigger` will trigger a deployed workflow on AWS Step Functions For more details see - [User docs](https://docs.metaflow.org/going-to-production-with-metaflow/scheduling-metaflow-flows) and [Admin docs](https://admin-docs.metaflow.org/metaflow-on-aws/operations-guide/metaflow-service-migration-guide#metaflow-2-1)
1 parent 32679a2 commit 621089d

32 files changed

+2712
-308
lines changed

metaflow/cli.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import click
99

10+
from . import current
1011
from . import lint
1112
from . import plugins
1213
from . import parameters
@@ -375,6 +376,12 @@ def logs(obj, input_path, stdout=None, stderr=None, both=None):
375376
default=None,
376377
help="Run id of the origin flow, if this task is part of a flow "
377378
"being resumed.")
379+
@click.option('--with',
380+
'decospecs',
381+
multiple=True,
382+
help="Add a decorator to this task. You can specify this "
383+
"option multiple times to attach multiple decorators "
384+
"to this task.")
378385
@click.pass_obj
379386
def step(obj,
380387
step_name,
@@ -387,7 +394,8 @@ def step(obj,
387394
retry_count=None,
388395
max_user_code_retries=None,
389396
clone_only=None,
390-
clone_run_id=None):
397+
clone_run_id=None,
398+
decospecs=None):
391399
if user_namespace is not None:
392400
namespace(user_namespace or None)
393401

@@ -402,9 +410,13 @@ def step(obj,
402410
fg='magenta',
403411
bold=False)
404412

413+
if decospecs:
414+
decorators._attach_decorators_to_step(func, decospecs)
415+
405416
obj.datastore.datastore_root = obj.datastore_root
406417
if obj.datastore.datastore_root is None:
407-
obj.datastore.datastore_root = obj.datastore.get_datastore_root_from_config(obj.echo)
418+
obj.datastore.datastore_root = \
419+
obj.datastore.get_datastore_root_from_config(obj.echo)
408420

409421
obj.metadata.add_sticky_tags(tags=tags)
410422
paths = decompress_list(input_paths) if input_paths else []
@@ -433,7 +445,7 @@ def step(obj,
433445

434446
echo('Success', fg='green', bold=True, indent=True)
435447

436-
@parameters.add_custom_parameters
448+
@parameters.add_custom_parameters(deploy_mode=False)
437449
@cli.command(help="Internal command to initialize a run.")
438450
@click.option('--run-id',
439451
default=None,
@@ -454,7 +466,8 @@ def init(obj, run_id=None, task_id=None, **kwargs):
454466
# variables.
455467

456468
if obj.datastore.datastore_root is None:
457-
obj.datastore.datastore_root = obj.datastore.get_datastore_root_from_config(obj.echo)
469+
obj.datastore.datastore_root = \
470+
obj.datastore.get_datastore_root_from_config(obj.echo)
458471

459472
runtime = NativeRuntime(obj.flow,
460473
obj.graph,
@@ -563,7 +576,7 @@ def resume(obj,
563576
write_run_id(run_id_file, runtime.run_id)
564577

565578

566-
@parameters.add_custom_parameters
579+
@parameters.add_custom_parameters(deploy_mode=True)
567580
@cli.command(help='Run the workflow locally.')
568581
@common_run_options
569582
@click.option('--namespace',
@@ -634,7 +647,8 @@ def before_run(obj, tags, decospecs):
634647
#obj.environment.init_environment(obj.logger)
635648

636649
if obj.datastore.datastore_root is None:
637-
obj.datastore.datastore_root = obj.datastore.get_datastore_root_from_config(obj.echo)
650+
obj.datastore.datastore_root = \
651+
obj.datastore.get_datastore_root_from_config(obj.echo)
638652

639653
decorators._init_decorators(
640654
obj.flow, obj.graph, obj.environment, obj.datastore, obj.logger)
@@ -643,7 +657,6 @@ def before_run(obj, tags, decospecs):
643657
# Package working directory only once per run.
644658
# We explicitly avoid doing this in `start` since it is invoked for every
645659
# step in the run.
646-
# TODO(crk): Capture time taken to package and log to keystone.
647660
obj.package = MetaflowPackage(obj.flow, obj.environment, obj.logger, obj.package_suffixes)
648661

649662

@@ -767,9 +780,21 @@ def start(ctx,
767780
ctx.obj.event_logger,
768781
ctx.obj.monitor)
769782
ctx.obj.datastore = DATASTORES[datastore]
770-
ctx.obj.datastore_root = datastore_root
771783

784+
if datastore_root is None:
785+
datastore_root = \
786+
ctx.obj.datastore.get_datastore_root_from_config(ctx.obj.echo)
787+
ctx.obj.datastore_root = ctx.obj.datastore.datastore_root = datastore_root
788+
789+
if decospecs:
790+
decorators._attach_decorators(ctx.obj.flow, decospecs)
791+
792+
# initialize current and parameter context for deploy-time parameters
772793
current._set_env(flow_name=ctx.obj.flow.name, is_running=False)
794+
parameters.set_parameter_context(ctx.obj.flow.name,
795+
ctx.obj.logger,
796+
ctx.obj.datastore)
797+
773798
if ctx.invoked_subcommand not in ('run', 'resume'):
774799
# run/resume are special cases because they can add more decorators with --with,
775800
# so they have to take care of themselves.
@@ -850,8 +875,6 @@ def main(flow, args=None, handle_exceptions=True, entrypoint=None):
850875
state = CliState(flow)
851876
state.entrypoint = entrypoint
852877

853-
parameters.set_parameter_context(flow.name)
854-
855878
try:
856879
if args is None:
857880
start(auto_envvar_prefix='METAFLOW', obj=state)
@@ -873,4 +896,4 @@ def main(flow, args=None, handle_exceptions=True, entrypoint=None):
873896
print_unknown_exception(x)
874897
sys.exit(1)
875898
else:
876-
raise
899+
raise

metaflow/datastore/datastore.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import pickle
1414

1515
from types import MethodType, FunctionType
16-
from ..includefile import InternalFile
1716
from ..parameters import Parameter
1817
from ..exception import MetaflowException, MetaflowInternalError
1918
from ..metadata import DataArtifact
@@ -377,11 +376,11 @@ def __init__(self,
377376

378377
# We have to make MAX_ATTEMPTS HEAD requests, which is
379378
# very unfortunate performance-wise (TODO: parallelize this).
380-
# On Meson it is possible that some attempts are missing, so
381-
# we have to check all possible attempt files to find the
382-
# latest one. Compared to doing a LIST operation, these checks
383-
# are guaranteed to be consistent as long as the task to be
384-
# looked up has already finished.
379+
# On AWS Step Functions it is possible that some attempts are
380+
# missing, so we have to check all possible attempt files to
381+
# find the latest one. Compared to doing a LIST operation,
382+
# these checks are guaranteed to be consistent as long as the
383+
# task to be looked up has already finished.
385384
self.attempt = None # backwards-compatibility for pre-attempts.
386385
for i in range(0, metaflow_config.MAX_ATTEMPTS):
387386
if self.has_metadata('%d.attempt' % i, with_attempt=False):
@@ -486,11 +485,6 @@ def serializable_attributes():
486485
isinstance(getattr(flow.__class__, var), property):
487486
continue
488487
val = getattr(flow, var)
489-
if isinstance(val, InternalFile):
490-
# We will force protocol 4 for serialization for anything
491-
# bigger than 1 GB
492-
yield var, TransformableObject(val()), val.size() > 1024 * 1024 * 1024
493-
continue
494488
if not (isinstance(val, MethodType) or
495489
isinstance(val, FunctionType) or
496490
isinstance(val, Parameter)):

metaflow/datatools/s3.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ def __repr__(self):
166166

167167
class S3(object):
168168

169+
@classmethod
170+
def get_root_from_config(cls, echo, create_on_absent=True):
171+
return DATATOOLS_S3ROOT
172+
169173
def __init__(self,
170174
tmproot='.',
171175
bucket=None,

metaflow/decorators.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -348,24 +348,33 @@ def _attach_decorators(flow, decospecs):
348348
effect as if you defined the decorators statically in the source for
349349
every step. Used by --with command line parameter.
350350
"""
351+
# Attach the decorator to all steps that don't have this decorator
352+
# already. This means that statically defined decorators are always
353+
# preferred over runtime decorators.
354+
#
355+
# Note that each step gets its own instance of the decorator class,
356+
# so decorator can maintain step-specific state.
357+
for step in flow:
358+
_attach_decorators_to_step(step, decospecs)
359+
360+
def _attach_decorators_to_step(step, decospecs):
361+
"""
362+
Attach decorators to a step during runtime. This has the same
363+
effect as if you defined the decorators statically in the source for
364+
the step.
365+
"""
351366
from .plugins import STEP_DECORATORS
352367
decos = {decotype.name: decotype for decotype in STEP_DECORATORS}
353368
for decospec in decospecs:
354369
deconame = decospec.split(':')[0]
355370
if deconame not in decos:
356371
raise UnknownStepDecoratorException(deconame)
357-
358-
# Attach the decorator to all steps that don't have this decorator
372+
# Attach the decorator to step if it doesn't have the decorator
359373
# already. This means that statically defined decorators are always
360374
# preferred over runtime decorators.
361-
#
362-
# Note that each step gets its own instance of the decorator class,
363-
# so decorator can maintain step-specific state.
364-
for step in flow:
365-
if deconame not in [deco.name for deco in step.decorators]:
366-
deco = decos[deconame]._parse_decorator_spec(decospec)
367-
step.decorators.append(deco)
368-
375+
if deconame not in [deco.name for deco in step.decorators]:
376+
deco = decos[deconame]._parse_decorator_spec(decospec)
377+
step.decorators.append(deco)
369378

370379
def _init_decorators(flow, graph, environment, datastore, logger):
371380
for deco in flow._flow_decorators.values():

metaflow/environment.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ def get_client_info(cls, flow_name, metadata):
7979
def get_package_commands(self, code_package_url):
8080
cmds = ["set -e",
8181
"echo \'Setting up task environment.\'",
82-
"%s -m pip install awscli click requests boto3 \
83-
--user -qqq" % self._python(),
82+
"%s -m pip install awscli click requests boto3 --user -qqq"
83+
% self._python(),
8484
"mkdir metaflow",
8585
"cd metaflow",
8686
"mkdir .metaflow", # mute local datastore creation log

metaflow/graph.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def __str__(self):
126126
return\
127127
"""*[{0.name} {0.type} (line {0.func_lineno})]*
128128
in_funcs={in_funcs}
129+
out_funcs={out_funcs}
129130
split_parents={parents}
130131
matching_join={matching_join}
131132
is_inside_foreach={is_inside_foreach}
@@ -139,6 +140,7 @@ def __str__(self):
139140
.format(self,
140141
matching_join=self.matching_join and '[%s]' % self.matching_join,
141142
is_inside_foreach=self.is_inside_foreach,
143+
out_funcs=', '.join('[%s]' % x for x in self.out_funcs),
142144
in_funcs=', '.join('[%s]' % x for x in self.in_funcs),
143145
parents=', '.join('[%s]' % x for x in self.split_parents),
144146
decos=' | '.join(map(str, self.decorators)),

0 commit comments

Comments
 (0)