aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.47k stars 3.83k forks source link

(aws-stepfunctions): StateMachineFragment returns error at synth when setting start_state and end_states #18316

Closed aws-painec closed 1 year ago

aws-painec commented 2 years ago

What is the problem?

Following these instructions to develop a StateMachine in a python app.

As written, they do not work; setting self.start_state and self.end_states for the fragment results in the following error at synth time:

TypeError: Can't instantiate abstract class MyJob with abstract methods end_states, start_state

Reproduction Steps

  1. Initialize a new CDK app in Python
  2. Add a very basic custom StateMachineFragment subclass to the default stack like so:
from aws_cdk import (
    core as cdk,
    aws_stepfunctions as sfn
)

class CdktestStack(cdk.Stack):

    def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        MyJob(self, "machine")

class MyJob(sfn.StateMachineFragment):
    def __init__(self, scope, id):
        super().__init__(scope, id)

        state = sfn.Wait(self, "Wait", time=sfn.WaitTime.duration(cdk.Duration.seconds(5)))

        self.start_state = state
        self.end_states = [state]
  1. Run cdk synth

What did you expect to happen?

App synths successfully, as per the example

What actually happened?

Error: TypeError: Can't instantiate abstract class MyJob with abstract methods end_states, start_state

CDK CLI Version

1.134.0

Framework Version

No response

Node.js Version

12.22.1

OS

Amazon Linux 2

Language

Python

Language Version

Python 3.9.7

Other information

The easiest workaround (though annoying) is to override the methods when extending the class. Building off the example from the repro steps, above, this is valid and synths correctly:

from aws_cdk import (
    core as cdk,
    aws_stepfunctions as sfn
)

class CdktestStack(cdk.Stack):

    def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        MyJob(self, "machine")

class MyJob(sfn.StateMachineFragment):
    def __init__(self, scope, id):
        super().__init__(scope, id)

        state = sfn.Wait(self, "Wait", time=sfn.WaitTime.duration(cdk.Duration.seconds(5)))

        self._start_state = state
        self._end_states = [state]

    def start_state(self):
        return self._start_state

    def end_states(self):
        return self._end_states
kaizencc commented 2 years ago

HI @aws-painec! Thanks for alerting me of this issue. You are right that this example is currently broken, actually in more ways than one. I am surprised that your workaround works for myriad reasons (did not work for me). Here's what I've got:

The TypeScript example is broken already. Should look like this:

import { Stack } from '@aws-cdk/core';
import { Construct } from 'constructs'; // Change: pull this one so it translates correctly to v2
import * as sfn from '@aws-cdk/aws-stepfunctions';

interface MyJobProps {
  jobFlavor: string;
}

class MyJob extends sfn.StateMachineFragment {
  public readonly startState: sfn.State;
  public readonly endStates: sfn.INextable[];

  constructor(parent: Construct, id: string, props: MyJobProps) {
    super(parent, id);

    const choice = new sfn.Choice(this, 'Choice')
      .when(sfn.Condition.stringEquals('$.branch', 'left'), new sfn.Pass(this, 'Left Branch'))
      .when(sfn.Condition.stringEquals('$.branch', 'right'), new sfn.Pass(this, 'Right Branch'));

    // ...

    this.startState = choice;
    this.endStates = choice.afterwards().endStates;
  }
}

class MyStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    // Do 3 different variants of MyJob in parallel
    const parallel = new sfn.Parallel(this, 'All jobs')
      .branch(new MyJob(this, 'Quick', { jobFlavor: 'quick' }).prefixStates())
      .branch(new MyJob(this, 'Medium', { jobFlavor: 'medium' }).prefixStates())
      .branch(new MyJob(this, 'Slow', { jobFlavor: 'slow' }).prefixStates());

    // Change: a state machine is necessary here for `cdk synth`
    sfn.StateMachine(this, 'SM', {
      definition: parallel,
    };
  }
}

Ideally this translates to this in Python (although I'm not certain how we can get there):

from aws_cdk.core import Stack
from constructs import Construct
import aws_cdk.aws_stepfunctions as sfn

class MyJob(sfn.StateMachineFragment):
    def __init__(self, parent, id, *, job_flavor):
        super().__init__(parent, id)

        choice = sfn.Choice(self, "Choice").when(sfn.Condition.string_equals("$.branch", "left"), sfn.Pass(self, "Left Branch")).when(sfn.Condition.string_equals("$.branch", "right"), sfn.Pass(self, "Right Branch"))

        # ...

        self._start_state = choice
        self._end_states = choice.afterwards().end_states

    @property
    def start_state(self):
      return self._start_state

    @property
    def end_states(self):
      return self._end_states

class PythonCdkStack(Stack):
    def __init__(self, scope, id):
        super().__init__(scope, id)
        # Do 3 different variants of MyJob in parallel
        definition = sfn.Parallel(self, "All jobs").branch(MyJob(self, "Quick", job_flavor="quick").prefix_states()).branch(MyJob(self, "Medium", job_flavor="medium").prefix_states()).branch(MyJob(self, "Slow", job_flavor="slow").prefix_states())
        sfn.StateMachine(self, 'SM', 
          definition=definition
        )

Note that I found the @property necessary -- relevant stack overflow.

@rix0rrr is there anything we can do about translating these Python inheritance examples correctly? I will go ahead and change the TypeScript example to work in TS.

github-actions[bot] commented 1 year ago

This issue has not received any attention in 1 year. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

timeisapear commented 1 year ago

still an issue https://github.com/healthverity/ccp-workflow/pull/180