Unverified Commit cfb97edb authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

Implement waiters (#3595)

This PR implements waiters according to the [Smithy waiters
spec](https://smithy.io/2.0/additional-specs/waiters.html).

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent c7178749
Loading
Loading
Loading
Loading
+43 −1
Original line number Diff line number Diff line
@@ -10,3 +10,45 @@
# references = ["smithy-rs#920"]
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
# author = "rcoh"

[[aws-sdk-rust]]
message = """
Added support for waiters. Services that model waiters now have a `Waiters` trait that adds
some methods prefixed with `wait_until` to the existing clients. These can be used to, for example
in S3, wait for a newly created bucket to be ready, or in EC2, to wait for a started instance to
have the status OK.

Using a waiter looks like the following example for EC2:
```rust
use aws_sdk_ec2::client::Waiters;

let result = ec2_client.wait_until_instance_status_ok()
    .instance_ids("some-instance-id")
    .wait(Duration::from_secs(300))
    .await;
```
"""
references = ["aws-sdk-rust#400", "smithy-rs#3595", "smithy-rs#3593", "smithy-rs#3585", "smithy-rs#3571", "smithy-rs#3569"]
meta = { "breaking" = false, "tada" = true, "bug" = false }
author = "jdisanti"

[[smithy-rs]]
message = """
Added support for waiters. Services that model waiters now have a `Waiters` trait that adds
some methods prefixed with `wait_until` to the existing clients.

For example, if there was a waiter modeled for "thing" that takes a "thing ID", using
that waiter would look as follows:

```rust
use my_generated_client::client::Waiters;

let result = client.wait_until_thing()
    .thing_id("someId")
    .wait(Duration::from_secs(120))
    .await;
```
"""
references = ["smithy-rs#119", "smithy-rs#3595", "smithy-rs#3593", "smithy-rs#3585", "smithy-rs#3571", "smithy-rs#3569"]
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "client" }
author = "jdisanti"
+2 −1
Original line number Diff line number Diff line
@@ -159,6 +159,7 @@ private class AwsFluentClientDocs(private val codegenContext: ClientCodegenConte
                    )
                    AwsDocs.clientConstructionDocs(codegenContext)(this)
                    FluentClientDocs.clientUsageDocs(codegenContext)(this)
                    FluentClientDocs.waiterDocs(codegenContext)(this)
                }

            else -> emptySection
+2 −2
Original line number Diff line number Diff line
@@ -8,11 +8,11 @@ publish = false

[dev-dependencies]
aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" }
aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] }
aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] }
aws-smithy-runtime-api = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime-api", features = ["client", "http-02x"] }
aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" }
aws-sdk-ec2 = { path = "../../build/aws-sdk/sdk/ec2", features = ["behavior-version-latest"] }
aws-sdk-ec2 = { path = "../../build/aws-sdk/sdk/ec2", features = ["test-util"] }
tokio = { version = "1.23.1", features = ["full"]}
http = "0.2.0"
tokio-stream = "0.1.5"
+588 −0
Original line number Diff line number Diff line
{
  "events": [
    {
      "connection_id": 0,
      "action": {
        "Request": {
          "request": {
            "uri": "https://ec2.us-west-2.amazonaws.com/",
            "headers": {
              "content-type": [
                "application/x-www-form-urlencoded"
              ],
              "content-length": [
                "73"
              ],
              "amz-sdk-request": [
                "attempt=1; max=3"
              ]
            },
            "method": "POST"
          }
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Data": {
          "data": {
            "Utf8": "Action=StartInstances&Version=2016-11-15&InstanceId.1=i-09fb4224219ac6902"
          },
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "headers": {
                "x-amzn-requestid": [
                  "bec35864-7ea6-430e-864e-229454f8b80d"
                ],
                "cache-control": [
                  "no-cache, no-store"
                ],
                "strict-transport-security": [
                  "max-age=31536000; includeSubDomains"
                ],
                "content-type": [
                  "text/xml;charset=UTF-8"
                ],
                "content-length": [
                  "579"
                ],
                "date": [
                  "Fri, 19 Apr 2024 00:26:55 GMT"
                ],
                "server": [
                  "AmazonEC2"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<StartInstancesResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">\n    <requestId>bec35864-7ea6-430e-864e-229454f8b80d</requestId>\n    <instancesSet>\n        <item>\n            <instanceId>i-09fb4224219ac6902</instanceId>\n            <currentState>\n                <code>0</code>\n                <name>pending</name>\n            </currentState>\n            <previousState>\n                <code>80</code>\n                <name>stopped</name>\n            </previousState>\n        </item>\n    </instancesSet>\n</StartInstancesResponse>\n"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 0,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 1,
      "action": {
        "Request": {
          "request": {
            "uri": "https://ec2.us-west-2.amazonaws.com/",
            "headers": {
              "content-type": [
                "application/x-www-form-urlencoded"
              ],
              "content-length": [
                "81"
              ],
              "amz-sdk-request": [
                "attempt=1; max=3"
              ]
            },
            "method": "POST"
          }
        }
      }
    },
    {
      "connection_id": 1,
      "action": {
        "Data": {
          "data": {
            "Utf8": "Action=DescribeInstanceStatus&Version=2016-11-15&InstanceId.1=i-09fb4224219ac6902"
          },
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 1,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 1,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "headers": {
                "x-amzn-requestid": [
                  "02147aa0-b2d0-41a3-afd7-570bb2d75151"
                ],
                "cache-control": [
                  "no-cache, no-store"
                ],
                "strict-transport-security": [
                  "max-age=31536000; includeSubDomains"
                ],
                "content-type": [
                  "text/xml;charset=UTF-8"
                ],
                "content-length": [
                  "243"
                ],
                "date": [
                  "Fri, 19 Apr 2024 00:26:56 GMT"
                ],
                "server": [
                  "AmazonEC2"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 1,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<DescribeInstanceStatusResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">\n    <requestId>02147aa0-b2d0-41a3-afd7-570bb2d75151</requestId>\n    <instanceStatusSet/>\n</DescribeInstanceStatusResponse>"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 1,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 2,
      "action": {
        "Request": {
          "request": {
            "uri": "https://ec2.us-west-2.amazonaws.com/",
            "headers": {
              "content-type": [
                "application/x-www-form-urlencoded"
              ],
              "content-length": [
                "81"
              ],
              "amz-sdk-request": [
                "attempt=1; max=3"
              ]
            },
            "method": "POST"
          }
        }
      }
    },
    {
      "connection_id": 2,
      "action": {
        "Data": {
          "data": {
            "Utf8": "Action=DescribeInstanceStatus&Version=2016-11-15&InstanceId.1=i-09fb4224219ac6902"
          },
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 2,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 2,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "headers": {
                "x-amzn-requestid": [
                  "4ddc4e65-50f4-42f9-a327-ad1c1fa717c8"
                ],
                "cache-control": [
                  "no-cache, no-store"
                ],
                "strict-transport-security": [
                  "max-age=31536000; includeSubDomains"
                ],
                "content-type": [
                  "text/xml;charset=UTF-8"
                ],
                "content-length": [
                  "1171"
                ],
                "date": [
                  "Fri, 19 Apr 2024 00:27:11 GMT"
                ],
                "server": [
                  "AmazonEC2"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 2,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<DescribeInstanceStatusResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">\n    <requestId>4ddc4e65-50f4-42f9-a327-ad1c1fa717c8</requestId>\n    <instanceStatusSet>\n        <item>\n            <instanceId>i-09fb4224219ac6902</instanceId>\n            <availabilityZone>us-west-2d</availabilityZone>\n            <instanceState>\n                <code>16</code>\n                <name>running</name>\n            </instanceState>\n            <systemStatus>\n                <status>initializing</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>initializing</status>\n                    </item>\n                </details>\n            </systemStatus>\n            <instanceStatus>\n                <status>initializing</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>initializing</status>\n                    </item>\n                </details>\n            </instanceStatus>\n        </item>\n    </instanceStatusSet>\n</DescribeInstanceStatusResponse>"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 2,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 3,
      "action": {
        "Request": {
          "request": {
            "uri": "https://ec2.us-west-2.amazonaws.com/",
            "headers": {
              "content-type": [
                "application/x-www-form-urlencoded"
              ],
              "content-length": [
                "81"
              ],
              "amz-sdk-request": [
                "attempt=1; max=3"
              ]
            },
            "method": "POST"
          }
        }
      }
    },
    {
      "connection_id": 3,
      "action": {
        "Data": {
          "data": {
            "Utf8": "Action=DescribeInstanceStatus&Version=2016-11-15&InstanceId.1=i-09fb4224219ac6902"
          },
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 3,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 3,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "headers": {
                "x-amzn-requestid": [
                  "78adf8db-74c8-462e-98c5-17db37544534"
                ],
                "cache-control": [
                  "no-cache, no-store"
                ],
                "strict-transport-security": [
                  "max-age=31536000; includeSubDomains"
                ],
                "content-type": [
                  "text/xml;charset=UTF-8"
                ],
                "content-length": [
                  "1171"
                ],
                "date": [
                  "Fri, 19 Apr 2024 00:27:39 GMT"
                ],
                "server": [
                  "AmazonEC2"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 3,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<DescribeInstanceStatusResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">\n    <requestId>78adf8db-74c8-462e-98c5-17db37544534</requestId>\n    <instanceStatusSet>\n        <item>\n            <instanceId>i-09fb4224219ac6902</instanceId>\n            <availabilityZone>us-west-2d</availabilityZone>\n            <instanceState>\n                <code>16</code>\n                <name>running</name>\n            </instanceState>\n            <systemStatus>\n                <status>initializing</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>initializing</status>\n                    </item>\n                </details>\n            </systemStatus>\n            <instanceStatus>\n                <status>initializing</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>initializing</status>\n                    </item>\n                </details>\n            </instanceStatus>\n        </item>\n    </instanceStatusSet>\n</DescribeInstanceStatusResponse>"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 3,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 4,
      "action": {
        "Request": {
          "request": {
            "uri": "https://ec2.us-west-2.amazonaws.com/",
            "headers": {
              "content-type": [
                "application/x-www-form-urlencoded"
              ],
              "content-length": [
                "81"
              ],
              "amz-sdk-request": [
                "attempt=1; max=3"
              ]
            },
            "method": "POST"
          }
        }
      }
    },
    {
      "connection_id": 4,
      "action": {
        "Data": {
          "data": {
            "Utf8": "Action=DescribeInstanceStatus&Version=2016-11-15&InstanceId.1=i-09fb4224219ac6902"
          },
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 4,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 4,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "headers": {
                "x-amzn-requestid": [
                  "48fb29e8-5b25-4854-a127-586f1b0ff62c"
                ],
                "cache-control": [
                  "no-cache, no-store"
                ],
                "strict-transport-security": [
                  "max-age=31536000; includeSubDomains"
                ],
                "content-type": [
                  "text/xml;charset=UTF-8"
                ],
                "content-length": [
                  "1171"
                ],
                "date": [
                  "Fri, 19 Apr 2024 00:28:34 GMT"
                ],
                "server": [
                  "AmazonEC2"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 4,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<DescribeInstanceStatusResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">\n    <requestId>48fb29e8-5b25-4854-a127-586f1b0ff62c</requestId>\n    <instanceStatusSet>\n        <item>\n            <instanceId>i-09fb4224219ac6902</instanceId>\n            <availabilityZone>us-west-2d</availabilityZone>\n            <instanceState>\n                <code>16</code>\n                <name>running</name>\n            </instanceState>\n            <systemStatus>\n                <status>initializing</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>initializing</status>\n                    </item>\n                </details>\n            </systemStatus>\n            <instanceStatus>\n                <status>initializing</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>initializing</status>\n                    </item>\n                </details>\n            </instanceStatus>\n        </item>\n    </instanceStatusSet>\n</DescribeInstanceStatusResponse>"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 4,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 5,
      "action": {
        "Request": {
          "request": {
            "uri": "https://ec2.us-west-2.amazonaws.com/",
            "headers": {
              "content-type": [
                "application/x-www-form-urlencoded"
              ],
              "content-length": [
                "81"
              ],
              "amz-sdk-request": [
                "attempt=1; max=3"
              ]
            },
            "method": "POST"
          }
        }
      }
    },
    {
      "connection_id": 5,
      "action": {
        "Data": {
          "data": {
            "Utf8": "Action=DescribeInstanceStatus&Version=2016-11-15&InstanceId.1=i-09fb4224219ac6902"
          },
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 5,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Request"
        }
      }
    },
    {
      "connection_id": 5,
      "action": {
        "Response": {
          "response": {
            "Ok": {
              "status": 200,
              "headers": {
                "x-amzn-requestid": [
                  "c9b9b2a0-ddbc-4ea8-a76a-4339a9a99de4"
                ],
                "cache-control": [
                  "no-cache, no-store"
                ],
                "strict-transport-security": [
                  "max-age=31536000; includeSubDomains"
                ],
                "content-type": [
                  "text/xml;charset=UTF-8"
                ],
                "content-length": [
                  "1139"
                ],
                "date": [
                  "Fri, 19 Apr 2024 00:29:53 GMT"
                ],
                "server": [
                  "AmazonEC2"
                ]
              }
            }
          }
        }
      }
    },
    {
      "connection_id": 5,
      "action": {
        "Data": {
          "data": {
            "Utf8": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<DescribeInstanceStatusResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">\n    <requestId>c9b9b2a0-ddbc-4ea8-a76a-4339a9a99de4</requestId>\n    <instanceStatusSet>\n        <item>\n            <instanceId>i-09fb4224219ac6902</instanceId>\n            <availabilityZone>us-west-2d</availabilityZone>\n            <instanceState>\n                <code>16</code>\n                <name>running</name>\n            </instanceState>\n            <systemStatus>\n                <status>ok</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>passed</status>\n                    </item>\n                </details>\n            </systemStatus>\n            <instanceStatus>\n                <status>ok</status>\n                <details>\n                    <item>\n                        <name>reachability</name>\n                        <status>passed</status>\n                    </item>\n                </details>\n            </instanceStatus>\n        </item>\n    </instanceStatusSet>\n</DescribeInstanceStatusResponse>"
          },
          "direction": "Response"
        }
      }
    },
    {
      "connection_id": 5,
      "action": {
        "Eof": {
          "ok": true,
          "direction": "Response"
        }
      }
    }
  ],
  "docs": "traffic recording of EC2 start_instance followed by EC2 wait_until_instance_status_ok being successful",
  "version": "V0"
}
+87 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

use aws_sdk_ec2::{client::Waiters, config::Region, error::DisplayErrorContext, Client};
use aws_smithy_async::test_util::tick_advance_sleep::{
    tick_advance_time_and_sleep, TickAdvanceTime,
};
use aws_smithy_runtime::{
    client::http::test_util::dvr::ReplayingClient, test_util::capture_test_logs::show_test_logs,
};
use aws_smithy_runtime_api::client::waiters::error::WaiterError;
use aws_smithy_types::retry::RetryConfig;
use std::time::Duration;

async fn prerequisites() -> (Client, ReplayingClient, TickAdvanceTime) {
    let (time_source, sleep_impl) = tick_advance_time_and_sleep();
    let client =
        ReplayingClient::from_file("tests/instance-status-ok-waiter-success.json").unwrap();
    let config = aws_sdk_ec2::Config::builder()
        .with_test_defaults()
        .http_client(client.clone())
        .time_source(time_source.clone())
        .sleep_impl(sleep_impl)
        .region(Region::new("us-west-2"))
        .retry_config(RetryConfig::standard())
        .build();
    (aws_sdk_ec2::Client::from_conf(config), client, time_source)
}

#[tokio::test]
async fn waiters_success() {
    let _logs = show_test_logs();

    let (ec2, http_client, time_source) = prerequisites().await;

    ec2.start_instances()
        .instance_ids("i-09fb4224219ac6902")
        .send()
        .await
        .unwrap();

    let waiter_task = tokio::spawn(
        ec2.wait_until_instance_status_ok()
            .instance_ids("i-09fb4224219ac6902")
            .wait(Duration::from_secs(300)),
    );

    // The responses in the test data will make the waiter poll a few times, so it will take some time
    // to complete. If successful, it shouldn't take a full 300 seconds. However, in the event it isn't successful,
    // waiting the full 300 seconds will result in a max time exceeded error instead of a never ending test.
    time_source.tick(Duration::from_secs(305)).await;
    waiter_task.await.unwrap().unwrap();

    http_client.full_validate("application/xml").await.unwrap();
}

#[tokio::test]
async fn waiters_exceed_max_wait_time() {
    let _logs = show_test_logs();

    let (ec2, _, time_source) = prerequisites().await;

    ec2.start_instances()
        .instance_ids("i-09fb4224219ac6902")
        .send()
        .await
        .unwrap();

    let waiter_task = tokio::spawn(
        ec2.wait_until_instance_status_ok()
            .instance_ids("i-09fb4224219ac6902")
            .wait(Duration::from_secs(30)),
    );

    time_source.tick(Duration::from_secs(35)).await;
    let err = waiter_task.await.unwrap().err().expect("should fail");
    match err {
        WaiterError::ExceededMaxWait(context) => {
            assert_eq!(30, context.max_wait().as_secs());
            assert_eq!(30, context.elapsed().as_secs());
            assert_eq!(3, context.poll_count());
        }
        err => panic!("unexpected error: {}", DisplayErrorContext(&err)),
    }
}
Loading