├── .envrc ├── python ├── layers │ └── commons │ │ ├── requirements.txt │ │ ├── utils │ │ ├── __init__.py │ │ └── loggingutils.py │ │ ├── apigatewayutils │ │ ├── __init__.py │ │ └── apigatewayutils.py │ │ └── dynamodbsheepshed │ │ ├── __init__.py │ │ ├── errors.py │ │ ├── sheep.py │ │ └── sheepshed.py └── lambdas │ ├── delete-wolf-ocd │ ├── requirements.txt │ └── index.py │ ├── get-dog-count │ ├── requirements.txt │ └── index.py │ ├── get-cat-ackermann │ ├── requirements.txt │ └── index.py │ └── post-sheep-random │ ├── requirements.txt │ └── index.py ├── .gitignore ├── nix ├── rust-toolchain.toml ├── flake.nix └── flake.lock ├── images ├── logo.png ├── architecture.png ├── get-started-0.png ├── get-started-1.png ├── get-started-2.png ├── get-started-3.png ├── get-started-4.png ├── get-started-5.png ├── get-started-6.png ├── get-started-7.png ├── get-started-8.png └── log-insights.png ├── utils ├── dynamodb.local ├── clear_dynamodb_table.py ├── invoke_dog.sh ├── invoke_wolf.sh ├── invoke_cat.sh ├── insert_sheeps.sh └── execute_default_benches.sh ├── rust ├── libs │ ├── lambda_event_utils │ │ ├── src │ │ │ ├── lib.rs │ │ │ ├── errors.rs │ │ │ └── sendevents.rs │ │ └── Cargo.toml │ ├── sheep_shed │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── errors.rs │ │ │ ├── lib.rs │ │ │ └── sheep.rs │ ├── lambda_apigw_utils │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── errors.rs │ │ │ └── lib.rs │ ├── lambda_commons_utils │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── dynamodb_sheep_shed │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs └── lambdas │ ├── get-cat-ackermann │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── get-dog-count │ ├── Cargo.toml │ └── src │ │ └── main.rs │ ├── delete-wolf-ocd │ ├── Cargo.toml │ └── src │ │ └── main.rs │ └── post-sheep-random │ ├── Cargo.toml │ └── src │ └── main.rs ├── Cargo.toml ├── LICENSE.txt ├── ci-config ├── buildspec-python.yml └── buildspec-rust.yml ├── ci-template.yml ├── README.md └── demo-template.yml /.envrc: -------------------------------------------------------------------------------- 1 | use flake path:./nix 2 | -------------------------------------------------------------------------------- /python/layers/commons/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/lambdas/delete-wolf-ocd/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/lambdas/get-dog-count/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/lambdas/get-cat-ackermann/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python/lambdas/post-sheep-random/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | dynamodb-local-metadata.json 4 | -------------------------------------------------------------------------------- /nix/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.79" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/logo.png -------------------------------------------------------------------------------- /python/layers/commons/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ Initialize package """ 2 | from .loggingutils import logger 3 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/architecture.png -------------------------------------------------------------------------------- /images/get-started-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-0.png -------------------------------------------------------------------------------- /images/get-started-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-1.png -------------------------------------------------------------------------------- /images/get-started-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-2.png -------------------------------------------------------------------------------- /images/get-started-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-3.png -------------------------------------------------------------------------------- /images/get-started-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-4.png -------------------------------------------------------------------------------- /images/get-started-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-5.png -------------------------------------------------------------------------------- /images/get-started-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-6.png -------------------------------------------------------------------------------- /images/get-started-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-7.png -------------------------------------------------------------------------------- /images/get-started-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/get-started-8.png -------------------------------------------------------------------------------- /images/log-insights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeremieRodon/demo-rust-lambda/HEAD/images/log-insights.png -------------------------------------------------------------------------------- /python/layers/commons/apigatewayutils/__init__.py: -------------------------------------------------------------------------------- 1 | """ Initialize package """ 2 | from .apigatewayutils import * 3 | -------------------------------------------------------------------------------- /utils/dynamodb.local: -------------------------------------------------------------------------------- 1 | java -Djava.library.path=~/Downloads/dynamodb_local/DynamoDBLocal_lib -jar ~/Downloads/dynamodb_local/DynamoDBLocal.jar -sharedDb -inMemory 2 | -------------------------------------------------------------------------------- /python/layers/commons/dynamodbsheepshed/__init__.py: -------------------------------------------------------------------------------- 1 | """ Initialize package """ 2 | from .sheep import Weight, Sheep 3 | from .sheepshed import DynamoDBSheepShed 4 | from .errors import SheepDuplicationError, GenericError, SheepNotPresentError 5 | -------------------------------------------------------------------------------- /rust/libs/lambda_event_utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | mod sendevents; 3 | pub use crate::sendevents::send_custom_event; 4 | 5 | pub mod prelude { 6 | pub use lambda_commons_utils::prelude::*; 7 | pub use lambda_commons_utils::serde_json; 8 | } 9 | -------------------------------------------------------------------------------- /rust/lambdas/get-cat-ackermann/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "get-cat-ackermann" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | [dependencies] 9 | lambda_apigw_utils = { path = "../../libs/lambda_apigw_utils" } 10 | -------------------------------------------------------------------------------- /rust/lambdas/get-dog-count/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "get-dog-count" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | [dependencies] 9 | lambda_apigw_utils = { path = "../../libs/lambda_apigw_utils" } 10 | dynamodb_sheep_shed = { path = "../../libs/dynamodb_sheep_shed" } 11 | sheep_shed = { path = "../../libs/sheep_shed" } 12 | aws-sdk-dynamodb = { workspace = true } 13 | -------------------------------------------------------------------------------- /rust/lambdas/delete-wolf-ocd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "delete-wolf-ocd" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | [dependencies] 9 | lambda_apigw_utils = { path = "../../libs/lambda_apigw_utils" } 10 | dynamodb_sheep_shed = { path = "../../libs/dynamodb_sheep_shed" } 11 | sheep_shed = { path = "../../libs/sheep_shed" } 12 | aws-sdk-dynamodb = { workspace = true } 13 | -------------------------------------------------------------------------------- /python/layers/commons/dynamodbsheepshed/errors.py: -------------------------------------------------------------------------------- 1 | class SheepNotPresentError(Exception): 2 | def __init__(self, tattoo): 3 | super().__init__(f"Sheep is not in the shed: {tattoo}") 4 | 5 | class SheepDuplicationError(Exception): 6 | def __init__(self, tattoo): 7 | super().__init__(f"Sheep already in the shed: {tattoo}") 8 | 9 | class GenericError(Exception): 10 | def __init__(self, message): 11 | super().__init__(f"Generic error: {message}") 12 | -------------------------------------------------------------------------------- /rust/lambdas/post-sheep-random/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "post-sheep-random" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | [dependencies] 9 | lambda_apigw_utils = { path = "../../libs/lambda_apigw_utils" } 10 | dynamodb_sheep_shed = { path = "../../libs/dynamodb_sheep_shed" } 11 | sheep_shed = { path = "../../libs/sheep_shed" } 12 | aws-sdk-dynamodb = { workspace = true } 13 | rand = { workspace = true } 14 | -------------------------------------------------------------------------------- /rust/libs/sheep_shed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sheep_shed" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | serde = { workspace = true } 12 | thiserror = { workspace = true } 13 | lambda_apigw_utils = { path = "../lambda_apigw_utils" } 14 | 15 | [features] 16 | sheepshed_tests = [] 17 | -------------------------------------------------------------------------------- /python/layers/commons/utils/loggingutils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | log_level = logging.INFO 4 | # create logger 5 | logger = logging.getLogger('common') 6 | logger.setLevel(log_level) 7 | logger.propagate = False 8 | # create console handler 9 | ch = logging.StreamHandler() 10 | ch.setLevel(log_level) 11 | # create formatter 12 | formatter = logging.Formatter('[%(asctime)s][%(threadName)s]%(levelname)s - %(message)s') 13 | # add formatter to ch 14 | ch.setFormatter(formatter) 15 | # add ch to logger 16 | logger.addHandler(ch) 17 | -------------------------------------------------------------------------------- /rust/libs/lambda_apigw_utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lambda_apigw_utils" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | lambda_commons_utils = { path = "../lambda_commons_utils" } 12 | lambda_http = { workspace = true } 13 | thiserror = { workspace = true } 14 | 15 | [dev-dependencies] 16 | aws-sdk-dynamodb = { workspace = true } 17 | -------------------------------------------------------------------------------- /rust/libs/lambda_event_utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lambda_event_utils" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | lambda_commons_utils = { path = "../lambda_commons_utils" } 12 | serde = { workspace = true } 13 | aws-sdk-eventbridge = { workspace = true} 14 | serde_type_name = { workspace = true } 15 | thiserror = { workspace = true } 16 | -------------------------------------------------------------------------------- /rust/libs/lambda_event_utils/src/errors.rs: -------------------------------------------------------------------------------- 1 | use aws_sdk_eventbridge::{error::SdkError, operation::put_events::PutEventsError}; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum Error { 6 | #[error("EventBridgeError: {code}/{message}")] 7 | EventBridgeEntryError { code: String, message: String }, 8 | #[error("Failed to infere the type of the event from the event type")] 9 | TypeInferenceFailed, 10 | #[error("EventBridgeError: {source:#}")] 11 | EventBridgeError { 12 | #[from] 13 | source: SdkError, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /rust/libs/lambda_commons_utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lambda_commons_utils" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | log = { workspace = true } 12 | env_logger = { workspace = true } 13 | tokio = { workspace = true} 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | aws-config = { workspace = true} 17 | lambda_runtime = { workspace = true} 18 | aws_lambda_events = { workspace = true} 19 | -------------------------------------------------------------------------------- /rust/libs/lambda_apigw_utils/src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum SimpleError { 5 | #[error("Invalid input: {0}")] 6 | InvalidInput(String), 7 | #[error("Invalid body schema")] 8 | InvalidBody, 9 | #[error("Invalid application state: {0}")] 10 | InvalidState(String), 11 | #[error("{object_type} not found with ID: {id}")] 12 | NotFound { 13 | object_type: &'static str, 14 | id: String, 15 | }, 16 | #[error("Unauthorized")] 17 | Unauthorized, 18 | #[error("Server error: {0}")] 19 | ServerError(&'static str), 20 | #[error("Custom error: {code} {message}")] 21 | Custom { code: u16, message: String }, 22 | } 23 | -------------------------------------------------------------------------------- /rust/lambdas/get-dog-count/src/main.rs: -------------------------------------------------------------------------------- 1 | use dynamodb_sheep_shed::DynamoDBSheepShed; 2 | use sheep_shed::SheepShed; 3 | 4 | use lambda_apigw_utils::prelude::*; 5 | 6 | async fn bark_answer(_req: SimpleRequest<'_>) -> SimpleResult { 7 | log::info!("create a shed instance"); 8 | let dynamodb_sheep_shed = DynamoDBSheepShed::new(dynamo()); 9 | 10 | log::info!("counting sheeps..."); 11 | let count = tokio::runtime::Handle::current() 12 | .spawn_blocking(move || dynamodb_sheep_shed.sheep_count()) 13 | .await 14 | .unwrap()?; 15 | 16 | log::info!("success - count={count}"); 17 | simple_response!(200, json!({"count": count})) 18 | } 19 | 20 | lambda_main!(async bark_answer, dynamo = aws_sdk_dynamodb::Client); 21 | -------------------------------------------------------------------------------- /rust/libs/dynamodb_sheep_shed/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dynamodb_sheep_shed" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | sheep_shed = { path = "../../libs/sheep_shed" } 12 | aws-sdk-dynamodb = { workspace = true } 13 | tokio = { workspace = true } 14 | serde_dynamo = { version = "4", features = ["aws-sdk-dynamodb+1"] } 15 | log = { workspace = true } 16 | 17 | [dev-dependencies] 18 | tokio = { workspace = true, features = ["full"] } 19 | rand = { workspace = true } 20 | sheep_shed = { path = "../../libs/sheep_shed", features = ["sheepshed_tests"] } 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["rust/lambdas/*", "rust/libs/*"] 3 | exclude = ["rust/libs/lambda_event_utils"] 4 | resolver = "2" 5 | 6 | [workspace.package] 7 | rust-version = "1.79.0" 8 | edition = "2021" 9 | authors = ["Jérémie RODON "] 10 | license = "MIT" 11 | 12 | [workspace.dependencies] 13 | aws_lambda_events = "0.15" 14 | lambda_runtime = "0.12" 15 | lambda_http = "0.12" 16 | aws-config = { version = "1.5", features = ["behavior-version-latest"] } 17 | aws-sdk-dynamodb = { version = "1.36" } 18 | serde = { version = "1.0", features = ["derive"] } 19 | tokio = { version = "1", features = ["macros"] } 20 | serde_json = "1.0" 21 | serde_dynamo = { version = "4", features = ["aws-sdk-dynamodb+1"] } 22 | thiserror = "1.0" 23 | rand = "0.8" 24 | log = "0.4" 25 | env_logger = "0.11" 26 | -------------------------------------------------------------------------------- /rust/libs/sheep_shed/src/errors.rs: -------------------------------------------------------------------------------- 1 | use lambda_apigw_utils::SimpleError; 2 | use thiserror::Error; 3 | 4 | use crate::Tattoo; 5 | 6 | #[derive(Debug, Error)] 7 | pub enum Error { 8 | #[error("Sheep is not in the shed: {0}")] 9 | SheepNotPresent(Tattoo), 10 | #[error("Sheep already in the shed: {0}")] 11 | SheepDuplicationError(Tattoo), 12 | #[error("Generic error: {0}")] 13 | GenericError(String), 14 | } 15 | 16 | impl From for SimpleError { 17 | fn from(value: Error) -> Self { 18 | lambda_apigw_utils::lambda_commons_utils::log::error!("sheep_shed::Error: {value}"); 19 | match value { 20 | Error::SheepDuplicationError(_) => SimpleError::InvalidInput(value.to_string()), 21 | Error::SheepNotPresent(_) => SimpleError::Custom { 22 | code: 404, 23 | message: value.to_string(), 24 | }, 25 | Error::GenericError(_) => Self::ServerError("Please try again later"), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /python/lambdas/get-dog-count/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from utils import logger 4 | from apigatewayutils import basic_error, basic_response, HTTPError 5 | from dynamodbsheepshed import DynamoDBSheepShed 6 | 7 | def bark_answer(): 8 | logger.info("create a shed instance") 9 | dynamodb_sheep_shed = DynamoDBSheepShed() 10 | 11 | logger.info("counting sheeps...") 12 | count = dynamodb_sheep_shed.sheep_count() 13 | 14 | logger.info(f"success - count={count}") 15 | return basic_response(200, {'count': count}) 16 | 17 | def lambda_handler(event, context): 18 | print(json.dumps(event, default=str)) 19 | try: 20 | return bark_answer() 21 | except HTTPError as e: 22 | logger.exception(e.message) 23 | return basic_error(e.code, e.message) 24 | except: 25 | logger.exception('Server error') 26 | return basic_error(500, 'Server error') 27 | 28 | logger.error('Should never get in this part of the code...') 29 | return basic_error(500, 'Server error') 30 | -------------------------------------------------------------------------------- /nix/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 4 | 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs = { 9 | nixpkgs.follows = "nixpkgs"; 10 | flake-utils.follows = "flake-utils"; 11 | }; 12 | }; 13 | }; 14 | outputs = { 15 | self, 16 | nixpkgs, 17 | flake-utils, 18 | rust-overlay, 19 | }: 20 | flake-utils.lib.eachDefaultSystem 21 | ( 22 | system: let 23 | overlays = [(import rust-overlay)]; 24 | pkgs = import nixpkgs { 25 | inherit system overlays; 26 | }; 27 | rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 28 | in 29 | with pkgs; { 30 | devShells.default = mkShell { 31 | buildInputs = [rustToolchain jdk21 python312Packages.boto3]; 32 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; 33 | }; 34 | } 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jérémie RODON 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /python/lambdas/get-cat-ackermann/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from utils import logger 4 | from apigatewayutils import basic_error, basic_response, HTTPError, extract_parameters 5 | 6 | def ackermann_2ary_iter(m, n): 7 | stack = [m, n] 8 | while len(stack) > 1: 9 | n, m = stack.pop(), stack.pop() 10 | if m == 0: 11 | stack.append(n + 1) 12 | elif n == 0: 13 | stack.append(m - 1) 14 | stack.append(1) 15 | else: 16 | stack.append(m - 1) 17 | stack.append(m) 18 | stack.append(n - 1) 19 | return stack[0] 20 | 21 | 22 | def lambda_handler(event, context): 23 | print(json.dumps(event, default=str)) 24 | try: 25 | params = extract_parameters(event) 26 | m = int(params['m']) 27 | n = int(params['n']) 28 | logger.info(f"Running A({m}, {n})...") 29 | result = ackermann_2ary_iter(m, n) 30 | logger.info(f"A({m}, {n}) = {result}") 31 | return basic_response(200, {'result': result}) 32 | except HTTPError as e: 33 | logger.exception(e.message) 34 | return basic_error(e.code, e.message) 35 | except: 36 | logger.exception('Server error') 37 | return basic_error(500, 'Server error') 38 | 39 | logger.error('Should never get in this part of the code...') 40 | return basic_error(500, 'Server error') 41 | -------------------------------------------------------------------------------- /python/layers/commons/apigatewayutils/apigatewayutils.py: -------------------------------------------------------------------------------- 1 | from utils import logger 2 | 3 | import json 4 | import os 5 | 6 | class HTTPError(Exception): 7 | def __init__(self, code, message): 8 | self.code = code 9 | self.message = message 10 | 11 | def get_user_name(event): 12 | try: 13 | return event['requestContext']['authorizer']['claims']['sub'] 14 | except: 15 | raise HTTPError(401, 'Unauthorized') 16 | 17 | def get_body_json(event): 18 | try: 19 | return json.loads(event['body']) 20 | except: 21 | logger.exception('Could not load the body') 22 | raise HTTPError(400, 'Invalid JSON body') 23 | 24 | def get_query_parameters(event): 25 | qp = event.get('queryStringParameters') 26 | if qp is None: 27 | return {} 28 | return qp 29 | 30 | def get_path_parameters(event): 31 | pp = event.get('pathParameters') 32 | if pp is None: 33 | return {} 34 | return pp 35 | 36 | def extract_parameters(event): 37 | params = {} 38 | params.update(get_query_parameters(event)) 39 | params.update(get_path_parameters(event)) 40 | return params 41 | 42 | def basic_error(code, message): 43 | return basic_response(code, {'message':message}) 44 | 45 | def basic_response(code, obj={}): 46 | resp = { 47 | 'statusCode': code, 48 | 'headers': { 49 | 'Access-Control-Allow-Origin': os.environ['ALLOW_ORIGIN'] 50 | }, 51 | 'body': json.dumps(obj, separators=(',', ':')) 52 | } 53 | logger.info(f'Sending resp: {resp}') 54 | return resp 55 | -------------------------------------------------------------------------------- /utils/clear_dynamodb_table.py: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # 1. Make sure the terminal from which you invoke is correclty configured 3 | # a. AWS credentials for the AWS account are present in the environment 4 | # b. The associated permissions allow dynamodb:Scan and dynamodb:BatchWriteItem 5 | # c. AWS_DEFAULT_REGION is set on the table's region 6 | # 2. Just invoke: 7 | # clear_dynamodb_table.py 8 | 9 | 10 | import boto3 11 | import sys 12 | 13 | def clear_table(table_name): 14 | print(f"Clearing all the content of DynamoDB table: {table_name}") 15 | # Create table resource 16 | table = boto3.resource("dynamodb").Table(table_name) 17 | 18 | # Items counter 19 | count = 0 20 | 21 | #Batch delete 22 | with table.batch_writer() as batch: 23 | # Scan the table and batch delete 24 | scan_params = { 25 | 'ProjectionExpression': 'tattoo', 26 | 'ReturnConsumedCapacity': 'NONE', 27 | } 28 | lek = None 29 | while True: 30 | if lek is not None: 31 | scan_params['ExclusiveStartKey'] = lek 32 | resp = table.scan(**scan_params) 33 | keys = resp['Items'] 34 | count += len(keys) 35 | print(f"Clearing {len(keys)} keys") 36 | for key in keys: 37 | batch.delete_item(Key=key) 38 | lek = resp.get('LastEvaluatedKey') 39 | if lek is None: 40 | break 41 | 42 | 43 | print(f"Table {table_name} emptied") 44 | print(f"{count} items cleared") 45 | 46 | if __name__ == "__main__": 47 | clear_table(sys.argv[1]) 48 | -------------------------------------------------------------------------------- /utils/invoke_dog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | echo "Usage: $0 [] " 5 | echo "" 6 | echo "Repeatedly call GET /dog" 7 | echo "" 8 | echo -e "-p|--parallel \tThe number of concurrent task to use. (Default: 100)" 9 | echo -e "-c|--call-count \tThe number of call to make. (Default: 1000)" 10 | echo "" 11 | echo "OPTIONS:" 12 | echo -e "-h|--help\t\t\tShow this help" 13 | } 14 | 15 | PARALLEL_TASKS=100 16 | COUNT=1000 17 | 18 | POSITIONAL=() 19 | while [[ $# -gt 0 ]] 20 | do 21 | key="$1" 22 | case $key in 23 | -h|--help) 24 | usage 25 | exit 0 26 | ;; 27 | -p|--parallel) 28 | PARALLEL_TASKS="$2" 29 | shift # past argument 30 | shift # past value 31 | ;; 32 | -c|--call-count) 33 | COUNT="$2" 34 | shift # past argument 35 | shift # past value 36 | ;; 37 | *) # unknown option 38 | POSITIONAL+=("$1") # save it in an array for later 39 | shift # past argument 40 | ;; 41 | esac 42 | done 43 | 44 | if [ ${#POSITIONAL[@]} -ne 1 ] ; then 45 | echo "Exactly one is expected as argument" 46 | usage 47 | exit 1 48 | fi 49 | API_URL="${POSITIONAL[0]}" 50 | if [ ${API_URL:${#API_URL}-1} == / ]; then 51 | API_URL=${API_URL::-1} 52 | fi 53 | 54 | timestamp_nano() { 55 | date +%s%N 56 | } 57 | 58 | start_ts=$(timestamp_nano) 59 | seq 1 $COUNT | xargs -Iunused -P$PARALLEL_TASKS curl -s --retry 5 --retry-connrefused "$API_URL/dog" > /dev/null 60 | end_ts=$(timestamp_nano) 61 | echo Calls took $(( ( $end_ts - $start_ts ) / 1000000 ))ms 62 | -------------------------------------------------------------------------------- /utils/invoke_wolf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | echo "Usage: $0 [] " 5 | echo "" 6 | echo "Repeatedly call DELETE /wolf" 7 | echo "" 8 | echo -e "-p|--parallel \tThe number of concurrent task to use. (Default: 100)" 9 | echo -e "-c|--call-count \tThe number of call to make. (Default: 1000)" 10 | echo "" 11 | echo "OPTIONS:" 12 | echo -e "-h|--help\t\t\tShow this help" 13 | } 14 | 15 | PARALLEL_TASKS=100 16 | COUNT=1000 17 | 18 | POSITIONAL=() 19 | while [[ $# -gt 0 ]] 20 | do 21 | key="$1" 22 | case $key in 23 | -h|--help) 24 | usage 25 | exit 0 26 | ;; 27 | -p|--parallel) 28 | PARALLEL_TASKS="$2" 29 | shift # past argument 30 | shift # past value 31 | ;; 32 | -c|--call-count) 33 | COUNT="$2" 34 | shift # past argument 35 | shift # past value 36 | ;; 37 | *) # unknown option 38 | POSITIONAL+=("$1") # save it in an array for later 39 | shift # past argument 40 | ;; 41 | esac 42 | done 43 | 44 | if [ ${#POSITIONAL[@]} -ne 1 ] ; then 45 | echo "Exactly one is expected as argument" 46 | usage 47 | exit 1 48 | fi 49 | API_URL="${POSITIONAL[0]}" 50 | if [ ${API_URL:${#API_URL}-1} == / ]; then 51 | API_URL=${API_URL::-1} 52 | fi 53 | 54 | timestamp_nano() { 55 | date +%s%N 56 | } 57 | 58 | start_ts=$(timestamp_nano) 59 | seq 1 $COUNT | xargs -Iunused -P$PARALLEL_TASKS curl -s --retry 5 --retry-connrefused -XDELETE "$API_URL/wolf" > /dev/null 60 | end_ts=$(timestamp_nano) 61 | echo Calls took $(( ( $end_ts - $start_ts ) / 1000000 ))ms 62 | -------------------------------------------------------------------------------- /rust/lambdas/post-sheep-random/src/main.rs: -------------------------------------------------------------------------------- 1 | use dynamodb_sheep_shed::DynamoDBSheepShed; 2 | use rand::Rng; 3 | use sheep_shed::{Sheep, SheepShed, Tattoo, Weight}; 4 | 5 | use lambda_apigw_utils::prelude::*; 6 | 7 | /// Create a random weight for a Sheep between 80 and 160 kg 8 | async fn generate_random_weight() -> Weight { 9 | let min = Weight::MIN.as_ug(); 10 | let max = Weight::MAX.as_ug(); 11 | Weight::from_ug(rand::thread_rng().gen_range(min..max)) 12 | } 13 | 14 | async fn insert_sheep(req: SimpleRequest<'_>) -> SimpleResult { 15 | let parameters = req.parameters; 16 | let tattoo_parameter = *parameters 17 | .get("Tattoo") 18 | .expect("API Gateway ensures it's here"); 19 | let tattoo = Tattoo(tattoo_parameter.parse().map_err(|e| { 20 | SimpleError::InvalidInput(format!( 21 | "Tattoo parameter {tattoo_parameter} could not be parsed: {e}" 22 | )) 23 | })?); 24 | 25 | log::info!("tattoo={tattoo:?}"); 26 | 27 | let handle = tokio::runtime::Handle::current(); 28 | 29 | log::info!("spawning sheep generation..."); 30 | // Random number generation in a separate task 31 | let new_sheep = handle.spawn(async { 32 | Sheep { 33 | tattoo, 34 | weight: generate_random_weight().await, 35 | } 36 | }); 37 | 38 | log::info!("create a shed instance"); 39 | let mut dynamodb_sheep_shed = DynamoDBSheepShed::new(dynamo()); 40 | 41 | log::info!("waiting sheep generation..."); 42 | let new_sheep = new_sheep.await.unwrap(); 43 | let response = json!(new_sheep); 44 | 45 | log::info!("inserting sheep"); 46 | handle 47 | .spawn_blocking(move || dynamodb_sheep_shed.add_sheep(new_sheep)) 48 | .await 49 | .unwrap()?; 50 | 51 | log::info!("success"); 52 | simple_response!(201, response) 53 | } 54 | 55 | lambda_main!(async insert_sheep, dynamo = aws_sdk_dynamodb::Client); 56 | -------------------------------------------------------------------------------- /python/lambdas/post-sheep-random/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import threading 4 | 5 | from utils import logger 6 | from apigatewayutils import basic_error, basic_response, HTTPError, extract_parameters 7 | from dynamodbsheepshed import DynamoDBSheepShed, Sheep, Weight, SheepDuplicationError 8 | 9 | def generate_random_weight(): 10 | wmin = Weight.MIN.as_ug() 11 | wmax = Weight.MAX.as_ug() 12 | return Weight.from_ug(random.randint(wmin, wmax)) 13 | 14 | def insert_sheep(event): 15 | parameters = extract_parameters(event) 16 | tattoo = int(parameters['Tattoo']) 17 | 18 | logger.info(f"tattoo={tattoo}") 19 | 20 | # Here I do not implement a separate generating thread like 21 | # in the Rust version because I feel it is too penalizing for 22 | # Python, as thread are POSIX thread, quite costly to launch 23 | # whereas Rust use green-threads and pre-launched OS threads 24 | logger.info("generating sheep") 25 | sheep = Sheep(tattoo, generate_random_weight()) 26 | 27 | logger.info("create a shed instance") 28 | dynamodb_sheep_shed = DynamoDBSheepShed() 29 | 30 | logger.info("inserting sheep") 31 | try: 32 | dynamodb_sheep_shed.add_sheep(sheep) 33 | except SheepDuplicationError as sde: 34 | raise HTTPError(400, str(sde)) 35 | 36 | logger.info("success") 37 | 38 | return basic_response(201, { 39 | 'tattoo': sheep.tattoo, 40 | 'weight': sheep.weight.as_ug() 41 | }) 42 | 43 | def lambda_handler(event, context): 44 | print(json.dumps(event, default=str)) 45 | try: 46 | return insert_sheep(event) 47 | except HTTPError as e: 48 | logger.exception(e.message) 49 | return basic_error(e.code, e.message) 50 | except: 51 | logger.exception('Server error') 52 | return basic_error(500, 'Server error') 53 | 54 | logger.error('Should never get in this part of the code...') 55 | return basic_error(500, 'Server error') 56 | -------------------------------------------------------------------------------- /rust/libs/lambda_event_utils/src/sendevents.rs: -------------------------------------------------------------------------------- 1 | use aws_sdk_eventbridge::{types::builders::PutEventsRequestEntryBuilder, Client}; 2 | 3 | use lambda_commons_utils::log; 4 | use lambda_commons_utils::serde_json::json; 5 | 6 | use serde::Serialize; 7 | use serde_type_name::type_name; 8 | 9 | use crate::errors::Error; 10 | 11 | /// Send an event in the Custom EventBus of the project and return the EventId 12 | /// 13 | /// ## Errors 14 | /// 15 | /// The function returns error if it fails to send the event to EventBridge 16 | /// 17 | /// ## Panics 18 | /// 19 | /// The function panics if the EVENT_BUS_NAME env variable is not set 20 | pub async fn send_custom_event(event_bridge_client: Client, event: T) -> Result 21 | where 22 | T: std::fmt::Debug + Serialize, 23 | { 24 | log::debug!("send_custom_event - event={event:?}"); 25 | let event_bus_name = std::env::var("EVENT_BUS_NAME") 26 | .expect("Mandatory environment variable `EVENT_BUS_NAME` is not set"); 27 | let event = PutEventsRequestEntryBuilder::default() 28 | .source("lambda-event-utils") 29 | .event_bus_name(event_bus_name) 30 | .detail_type(type_name(&event).map_err(|_| Error::TypeInferenceFailed)?) 31 | .detail(json!(event).to_string()) 32 | .build(); 33 | let put_event = event_bridge_client.put_events().entries(event); 34 | let result = put_event.send().await?; 35 | let entry_result = result 36 | .entries() 37 | .first() 38 | .expect("vec should always have one entry"); 39 | if result.failed_entry_count > 0 { 40 | Err(Error::EventBridgeEntryError { 41 | code: entry_result 42 | .error_code() 43 | .expect("should always an error code") 44 | .to_owned(), 45 | message: entry_result 46 | .error_message() 47 | .expect("should always an error message") 48 | .to_owned(), 49 | }) 50 | } else { 51 | Ok(entry_result 52 | .event_id() 53 | .expect("should always an event_id") 54 | .to_owned()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utils/invoke_cat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | echo "Usage: $0 [] " 5 | echo "" 6 | echo "Repeatedly call GET /cat?m=&n= with m=3 and n=8 unless overritten" 7 | echo "" 8 | echo -e "-p|--parallel \tThe number of concurrent task to use. (Default: 100)" 9 | echo -e "-c|--call-count \tThe number of call to make. (Default: 1000)" 10 | echo -e "-m \tThe 'm' number for the Ackermann algorithm. (Default: 3)" 11 | echo -e "-n \tThe 'n' number for the Ackermann algorithm. (Default: 8)" 12 | echo "" 13 | echo "OPTIONS:" 14 | echo -e "-h|--help\t\t\tShow this help" 15 | } 16 | 17 | PARALLEL_TASKS=100 18 | COUNT=1000 19 | ARG_M=3 20 | ARG_N=8 21 | 22 | POSITIONAL=() 23 | while [[ $# -gt 0 ]] 24 | do 25 | key="$1" 26 | case $key in 27 | -h|--help) 28 | usage 29 | exit 0 30 | ;; 31 | -p|--parallel) 32 | PARALLEL_TASKS="$2" 33 | shift # past argument 34 | shift # past value 35 | ;; 36 | -c|--call-count) 37 | COUNT="$2" 38 | shift # past argument 39 | shift # past value 40 | ;; 41 | -m) 42 | ARG_M="$2" 43 | shift # past argument 44 | shift # past value 45 | ;; 46 | -n) 47 | ARG_N="$2" 48 | shift # past argument 49 | shift # past value 50 | ;; 51 | *) # unknown option 52 | POSITIONAL+=("$1") # save it in an array for later 53 | shift # past argument 54 | ;; 55 | esac 56 | done 57 | 58 | if [ ${#POSITIONAL[@]} -ne 1 ] ; then 59 | echo "Exactly one is expected as argument" 60 | usage 61 | exit 1 62 | fi 63 | API_URL="${POSITIONAL[0]}" 64 | if [ ${API_URL:${#API_URL}-1} == / ]; then 65 | API_URL=${API_URL::-1} 66 | fi 67 | 68 | timestamp_nano() { 69 | date +%s%N 70 | } 71 | 72 | start_ts=$(timestamp_nano) 73 | seq 1 $COUNT | xargs -Iunused -P$PARALLEL_TASKS curl -s --retry 5 --retry-connrefused "$API_URL/cat?m=$ARG_M&n=$ARG_N" > /dev/null 74 | end_ts=$(timestamp_nano) 75 | echo Calls took $(( ( $end_ts - $start_ts ) / 1000000 ))ms 76 | -------------------------------------------------------------------------------- /python/layers/commons/dynamodbsheepshed/sheep.py: -------------------------------------------------------------------------------- 1 | class Weight: 2 | def __init__(self, weight): 3 | self.__weight = weight 4 | 5 | @classmethod 6 | def from_kg(cls, weight_kg): 7 | return cls(int(weight_kg * 1_000_000_000)) 8 | 9 | @classmethod 10 | def from_g(cls, weight_g): 11 | return cls(int(weight_g * 1_000_000)) 12 | 13 | @classmethod 14 | def from_mg(cls, weight_mg): 15 | return cls(int(weight_mg * 1_000)) 16 | 17 | @classmethod 18 | def from_ug(cls, weight_ug): 19 | return cls(int(weight_ug)) 20 | 21 | def as_kg(self): 22 | return self.__weight / 1_000_000_000 23 | 24 | def as_g(self): 25 | return self.__weight / 1_000_000 26 | 27 | def as_mg(self): 28 | return self.__weight / 1_000 29 | 30 | def as_ug(self): 31 | return self.__weight 32 | 33 | def __str__(self): 34 | if (self.__weight > 1_000_000_000): 35 | return "{:.3f}kg".format(self.as_kg()) 36 | elif self.__weight > 1_000_000: 37 | return "{:.3f}g".format(self.as_g()) 38 | elif self.__weight > 1_000: 39 | return "{:.3f}mg".format(self.as_mg()) 40 | else: 41 | return "{}ug".format(self.__weight) 42 | 43 | def __add__(self, rhs): 44 | return Weight(self.__weight + rhs.__weight) 45 | 46 | def __eq__(self, rhs): 47 | return self.__weight == rhs.__weight 48 | 49 | def __le__(self, rhs): 50 | return self.__weight <= rhs.__weight 51 | 52 | def __lt__(self, rhs): 53 | return self.__weight < rhs.__weight 54 | 55 | def __ge__(self, rhs): 56 | return self.__weight >= rhs.__weight 57 | 58 | def __gt__(self, rhs): 59 | return self.__weight > rhs.__weight 60 | 61 | Weight.MIN=Weight(80_000_000_000) 62 | Weight.MAX=Weight(160_000_000_000) 63 | 64 | class Sheep: 65 | def __init__(self, tattoo, weight): 66 | self.tattoo = tattoo 67 | self.weight = weight 68 | 69 | def __str__(self): 70 | return f"Sheep({self.tattoo}) weighting {self.weight}" 71 | 72 | def __eq__(self, rhs): 73 | return self.tattoo == rhs.tattoo 74 | -------------------------------------------------------------------------------- /utils/insert_sheeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | echo "Usage: $0 [] []" 5 | echo "" 6 | echo "Inserts the given amount of sheeps by repeatedly calling POST /sheep/" 7 | echo "Tattoos will be all the numbers from 1 to included, unless the -s option is used" 8 | echo " defaults to 1000" 9 | echo "" 10 | echo -e "-p|--parallel \tThe number of concurrent task to use. (Default: 100)" 11 | echo -e "-s|--start-at \tThe first sheep tattoo to use. (Default: 1)" 12 | echo "" 13 | echo "OPTIONS:" 14 | echo -e "-h|--help\t\t\tShow this help" 15 | } 16 | 17 | PARALLEL_TASKS=100 18 | SHEEP_COUNT=1000 19 | START_AT=1 20 | 21 | POSITIONAL=() 22 | while [[ $# -gt 0 ]] 23 | do 24 | key="$1" 25 | case $key in 26 | -h|--help) 27 | usage 28 | exit 0 29 | ;; 30 | -p|--parallel) 31 | PARALLEL_TASKS="$2" 32 | shift # past argument 33 | shift # past value 34 | ;; 35 | -s|--start-at) 36 | START_AT="$2" 37 | shift # past argument 38 | shift # past value 39 | ;; 40 | *) # unknown option 41 | POSITIONAL+=("$1") # save it in an array for later 42 | shift # past argument 43 | ;; 44 | esac 45 | done 46 | 47 | if [ ${#POSITIONAL[@]} -eq 0 ] ; then 48 | echo "At least is expected as argument" 49 | usage 50 | exit 1 51 | fi 52 | 53 | if [ ${#POSITIONAL[@]} -gt 2 ] ; then 54 | echo "Too much arguments. Arguments unknown" 55 | usage 56 | exit 1 57 | fi 58 | 59 | API_URL="${POSITIONAL[0]}" 60 | if [ ${API_URL:${#API_URL}-1} == / ]; then 61 | API_URL=${API_URL::-1} 62 | fi 63 | if [ ${#POSITIONAL[@]} -eq 2 ] ; then 64 | SHEEP_COUNT="${POSITIONAL[1]}" 65 | fi 66 | 67 | timestamp_nano() { 68 | date +%s%N 69 | } 70 | SEQ_END=$(( $START_AT + $SHEEP_COUNT - 1 )) 71 | start_ts=$(timestamp_nano) 72 | seq $START_AT $SEQ_END | xargs -Itattoo -P$PARALLEL_TASKS curl -s --retry 5 --retry-connrefused -XPOST "$API_URL/sheep/tattoo" > /dev/null 73 | end_ts=$(timestamp_nano) 74 | echo Insertion took $(( ( $end_ts - $start_ts ) / 1000000 ))ms 75 | -------------------------------------------------------------------------------- /nix/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1726560853, 9 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1730785428, 24 | "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1730946479, 52 | "narHash": "sha256-AxGJ3BRc44o3RBcfXxZqjVYftVtJ2sl+/WEjiLUmXRY=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "7fba269fe89ffad47206e0afba233d337c04cf08", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /utils/execute_default_benches.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | usage () { 4 | echo "Usage: $0 --rust-api --python-api " 5 | echo "" 6 | echo "Execute the 4 loadtest bench in the order: cat, sheeps, dog, wolf for the two APIs, one after the other" 7 | echo "" 8 | echo -e "--rust-api \tThe API URL of the Rust API" 9 | echo -e "--python-api \tThe API URL of the Python API" 10 | echo "" 11 | echo "OPTIONS:" 12 | echo -e "-h|--help\t\t\tShow this help" 13 | } 14 | 15 | PYTHON_API_URL="" 16 | RUST_API_URL="" 17 | 18 | POSITIONAL=() 19 | while [[ $# -gt 0 ]] 20 | do 21 | key="$1" 22 | case $key in 23 | -h|--help) 24 | usage 25 | exit 0 26 | ;; 27 | --rust-api) 28 | RUST_API_URL="$2" 29 | shift # past argument 30 | shift # past value 31 | ;; 32 | --python-api) 33 | PYTHON_API_URL="$2" 34 | shift # past argument 35 | shift # past value 36 | ;; 37 | *) # unknown option 38 | POSITIONAL+=("$1") # save it in an array for later 39 | shift # past argument 40 | ;; 41 | esac 42 | done 43 | 44 | if [ ${#POSITIONAL[@]} -ne 0 ] ; then 45 | echo "Unknown arguments" 46 | usage 47 | exit 1 48 | fi 49 | 50 | if [ -z $RUST_API_URL ] ; then 51 | echo "--rust-api is mandatory" 52 | usage 53 | exit 1 54 | fi 55 | 56 | if [ -z $PYTHON_API_URL ] ; then 57 | echo "--python-api is mandatory" 58 | usage 59 | exit 1 60 | fi 61 | 62 | echo "Launching test..." 63 | echo "###############################################################" 64 | echo "# This will take a while and appear to hang, but don't worry! #" 65 | echo "###############################################################" 66 | 67 | 68 | echo PYTHON CAT: ./invoke_cat.sh $PYTHON_API_URL 69 | ./invoke_cat.sh $PYTHON_API_URL 70 | 71 | echo RUST CAT: ./invoke_cat.sh $RUST_API_URL 72 | ./invoke_cat.sh $RUST_API_URL 73 | 74 | echo PYTHON SHEEPS: ./insert_sheeps.sh $PYTHON_API_URL 75 | ./insert_sheeps.sh $PYTHON_API_URL 76 | 77 | echo RUST SHEEPS: ./insert_sheeps.sh $RUST_API_URL 78 | ./insert_sheeps.sh $RUST_API_URL 79 | 80 | echo PYTHON DOG: ./invoke_dog.sh $PYTHON_API_URL 81 | ./invoke_dog.sh $PYTHON_API_URL 82 | 83 | echo RUST DOG: ./invoke_dog.sh $RUST_API_URL 84 | ./invoke_dog.sh $RUST_API_URL 85 | 86 | echo PYTHON WOLF: ./invoke_wolf.sh $PYTHON_API_URL 87 | ./invoke_wolf.sh $PYTHON_API_URL 88 | 89 | echo RUST WOLF: ./invoke_wolf.sh $RUST_API_URL 90 | ./invoke_wolf.sh $RUST_API_URL 91 | 92 | echo Done. 93 | -------------------------------------------------------------------------------- /python/lambdas/delete-wolf-ocd/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import threading 4 | 5 | from utils import logger 6 | from apigatewayutils import basic_error, basic_response, HTTPError 7 | from dynamodbsheepshed import DynamoDBSheepShed, Sheep, Weight, SheepNotPresentError 8 | 9 | 10 | def sieve_of_eratosthenes(n): 11 | tmp = [True for i in range(n+1)] 12 | tmp[0] = tmp[1] = False 13 | sqrt_n = int(math.sqrt(n)) + 1 14 | for i in range(2, sqrt_n): 15 | if tmp[i]: 16 | j = i * i 17 | while j <= n: 18 | tmp[j] = False 19 | j += i 20 | return [p for (p, b) in enumerate(tmp) if b] 21 | 22 | def wolf_ocd(): 23 | sieve = [None] 24 | sieve_max = int(math.sqrt(Weight.MAX.as_ug())) 25 | def create_sieve(n): 26 | sieve[0] = sieve_of_eratosthenes(n) 27 | logger.info(f"spawning primes sieve generation (2 to {sieve_max})...") 28 | t_sieve = threading.Thread(target=create_sieve, args=(sieve_max,)) 29 | t_sieve.start() 30 | 31 | logger.info(f"retrieving all the sheeps...") 32 | sheeps = [None] 33 | def get_sheeps(): 34 | logger.info(f"create a shed instance") 35 | sheeps[0] = list(DynamoDBSheepShed().sheep_iter()) 36 | t_sheeps = threading.Thread(target=get_sheeps) 37 | t_sheeps.start() 38 | 39 | t_sieve.join() 40 | sieve = sieve[0] 41 | logger.info(f"sieve contains {len(sieve)} primes") 42 | 43 | t_sheeps.join() 44 | sheeps = sheeps[0] 45 | logger.info(f"sheep list contains {len(sheeps)} sheep") 46 | 47 | def edible_sheep(sheep): 48 | sheep_weight_ug = sheep.weight.as_ug() 49 | for prime in sieve: 50 | if sheep_weight_ug % prime == 0: 51 | return False 52 | return True 53 | edible_sheeps = [sheep for sheep in sheeps if edible_sheep(sheep)] 54 | if len(edible_sheeps) > 0: 55 | sheep_to_eat = max(edible_sheeps, key=lambda o:o.weight) 56 | logger.info(f"wolf will eat {sheep_to_eat}") 57 | logger.info(f"create a shed instance") 58 | try: 59 | DynamoDBSheepShed().kill_sheep(sheep_to_eat.tattoo) 60 | except SheepNotPresentError as snpe: 61 | raise HTTPError(500, str(snpe)) 62 | return basic_response(204) 63 | else: 64 | logger.info(f"it seems the wolf will continue to starve...") 65 | return basic_response(404, {'message': "No fitting sheep"}) 66 | 67 | 68 | def lambda_handler(event, context): 69 | print(json.dumps(event, default=str)) 70 | try: 71 | return wolf_ocd() 72 | except HTTPError as e: 73 | logger.exception(e.message) 74 | return basic_error(e.code, e.message) 75 | except: 76 | logger.exception('Server error') 77 | return basic_error(500, 'Server error') 78 | 79 | logger.error('Should never get in this part of the code...') 80 | return basic_error(500, 'Server error') 81 | -------------------------------------------------------------------------------- /ci-config/buildspec-python.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | python: 3.12 7 | commands: 8 | - echo Entered the install phase on `date` 9 | finally: 10 | - echo Exiting the install phase on `date` 11 | pre_build: 12 | on-failure: ABORT 13 | commands: 14 | - echo Entered the pre_build phase on `date` 15 | finally: 16 | - | 17 | echo Changing all file times to 2010-01-01 00:00:00 \(arbitrary\) to avoid inducing changes in ZIP packages 18 | find . -not -path '*/.git/*' -exec touch -a -m -t"201001010000.00" {} \; 19 | - echo Exiting the pre_build phase on `date` 20 | build: 21 | on-failure: ABORT 22 | commands: 23 | - echo Entered the build phase on `date` 24 | - | 25 | echo Installing dependencies for the Python layers 26 | PYTHON_LAYERS_PATH=./python/layers 27 | for LAYER in $(ls $PYTHON_LAYERS_PATH) ; do 28 | echo Creating a supported path for Python layer: $LAYER 29 | mv $PYTHON_LAYERS_PATH/$LAYER $PYTHON_LAYERS_PATH/$LAYER.bak 30 | mkdir -p $PYTHON_LAYERS_PATH/$LAYER/python 31 | mv $PYTHON_LAYERS_PATH/$LAYER.bak/* $PYTHON_LAYERS_PATH/$LAYER/python 32 | rmdir $PYTHON_LAYERS_PATH/$LAYER.bak 33 | echo Installing dependencies for Python layer: $LAYER 34 | pip install -r $PYTHON_LAYERS_PATH/$LAYER/python/requirements.txt -t $PYTHON_LAYERS_PATH/$LAYER/python 35 | rm $PYTHON_LAYERS_PATH/$LAYER/python/requirements.txt 36 | done 37 | - | 38 | echo Installing dependencies for the Python lambdas 39 | PYTHON_LAMBDA_PATH=./python/lambdas 40 | for LAMBDA in $(ls $PYTHON_LAMBDA_PATH) ; do 41 | echo Installing dependencies for Python lambda: $LAMBDA 42 | pip install -r $PYTHON_LAMBDA_PATH/$LAMBDA/requirements.txt -t $PYTHON_LAMBDA_PATH/$LAMBDA 43 | rm $PYTHON_LAMBDA_PATH/$LAMBDA/requirements.txt 44 | done 45 | finally: 46 | - | 47 | echo Changing all file times to 2010-01-01 00:00:00 \(arbitrary\) to avoid inducing changes in ZIP packages 48 | find . -not -path '*/.git/*' -exec touch -a -m -t"201001010000.00" {} \; 49 | - echo Exiting the build phase on `date` 50 | post_build: 51 | commands: 52 | - echo Entered the post_build phase on `date` 53 | - | 54 | echo Soft-link lambda folders 55 | mkdir lambdas 56 | ORIG_LAMBDA_FOLDER=$CODEBUILD_SRC_DIR/python/lambdas 57 | cd lambdas 58 | for LAMBDA_FOLDER in $(ls $ORIG_LAMBDA_FOLDER) ; do 59 | echo Soft-link ./lambdas/$LAMBDA_FOLDER to $ORIG_LAMBDA_FOLDER/$LAMBDA_FOLDER 60 | ln -s $ORIG_LAMBDA_FOLDER/$LAMBDA_FOLDER $LAMBDA_FOLDER 61 | done 62 | cd $CODEBUILD_SRC_DIR 63 | - | 64 | echo Packaging the demo-template file references... 65 | mv demo-template.yml demo-template.yml.bak 66 | aws cloudformation package --template-file demo-template.yml.bak --s3-bucket $ARTIFACT_BUCKET --s3-prefix templates --output-template-file demo-template.yml 67 | finally: 68 | - echo Exiting the post_build phase on `date` 69 | artifacts: 70 | base-directory: . 71 | files: 72 | - demo-template.yml 73 | -------------------------------------------------------------------------------- /rust/libs/lambda_commons_utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use aws_config; 2 | pub use aws_lambda_events; 3 | pub use env_logger; 4 | pub use lambda_runtime; 5 | pub use log; 6 | pub use serde_json; 7 | pub use tokio; 8 | 9 | #[macro_export] 10 | macro_rules! sync_or_async { 11 | (sync $rc:ident($arg:ident)) => { 12 | tokio::task::spawn_blocking(move || $rc($arg)) 13 | .await 14 | .unwrap() 15 | }; 16 | (async $rc:ident($arg:ident)) => { 17 | $rc($arg).await 18 | }; 19 | } 20 | 21 | #[macro_export] 22 | macro_rules! lambda_main { 23 | (common $sync_or_async:ident $rc:ident($event_type:ty)->$return_type:ty $(, $code:stmt)?) => { 24 | use $crate::tokio; 25 | 26 | async fn function_handler( 27 | event: $crate::lambda_runtime::LambdaEvent<$event_type>, 28 | ) -> Result<$return_type, $crate::lambda_runtime::Error> { 29 | $crate::log::debug!("{event:?}"); 30 | let payload = event.payload; 31 | $crate::log::info!("{}", $crate::serde_json::json!(payload)); 32 | Ok($crate::sync_or_async!($sync_or_async $rc(payload))?) 33 | } 34 | 35 | 36 | #[tokio::main] 37 | async fn main() -> Result<(), $crate::lambda_runtime::Error> { 38 | $crate::env_logger::Builder::from_env( 39 | $crate::env_logger::Env::default() 40 | .default_filter_or("info,tracing::span=warn") 41 | .default_write_style_or("never"), 42 | ) 43 | .format_timestamp_micros() 44 | .init(); 45 | 46 | $($code;)? 47 | 48 | $crate::lambda_runtime::run($crate::lambda_runtime::service_fn(function_handler)).await 49 | } 50 | }; 51 | ($sync_or_async:ident $rc:ident($event_type:ty)->$return_type:ty $(,$fn_name:ident = $sdk:ty)+) => { 52 | static AWS_SDK_CONFIG : std::sync::OnceLock<$crate::aws_config::SdkConfig> = std::sync::OnceLock::new(); 53 | pub fn aws_sdk_config() -> &'static $crate::aws_config::SdkConfig { 54 | AWS_SDK_CONFIG.get().unwrap() 55 | } 56 | 57 | // AWS SDK clients globals 58 | $( 59 | pub fn $fn_name() -> $sdk { 60 | static CLIENT : std::sync::OnceLock<$sdk> = std::sync::OnceLock::new(); 61 | CLIENT.get_or_init(||<$sdk>::new(aws_sdk_config())).clone() 62 | } 63 | )+ 64 | 65 | $crate::lambda_main!(common $sync_or_async $rc($event_type)->$return_type, 66 | // AWS SDK clients globals instantiation 67 | AWS_SDK_CONFIG.set($crate::aws_config::load_from_env().await).unwrap() 68 | ); 69 | 70 | }; 71 | ($sync_or_async:ident $rc:ident($event_type:ty)->$return_type:ty) => { 72 | $crate::lambda_main!(common $sync_or_async $rc($event_type)->$return_type); 73 | }; 74 | ($sync_or_async:ident $rc:ident($event_type:ty) $(,$fn_name:ident = $sdk:ty)*) => { 75 | $crate::lambda_main!($sync_or_async $rc($event_type)->() $(, $fn_name = $sdk)*); 76 | }; 77 | ($rc:ident($event_type:ty)->$return_type:ty $(,$fn_name:ident = $sdk:ty)*) => { 78 | $crate::lambda_main!(sync $rc($event_type)->$return_type $(, $fn_name = $sdk)*); 79 | }; 80 | ($rc:ident($event_type:ty) $(,$fn_name:ident = $sdk:ty)*) => { 81 | $crate::lambda_main!($rc($event_type)->() $(, $fn_name = $sdk)*); 82 | }; 83 | } 84 | 85 | pub mod prelude { 86 | pub use super::lambda_main; 87 | pub use super::log; 88 | } 89 | -------------------------------------------------------------------------------- /rust/lambdas/get-cat-ackermann/src/main.rs: -------------------------------------------------------------------------------- 1 | use lambda_apigw_utils::prelude::*; 2 | 3 | fn ackermann_2ary_iter(m: u128, n: u128) -> u128 { 4 | let mut stack = vec![m, n]; 5 | loop { 6 | // match (n, m) 7 | match (/* Pop n */ stack.pop(), /* Pop m */ stack.pop()) { 8 | // If the second pop is a None, then the first one 9 | // was the last element of the stack, we are finished 10 | (Some(result), None) => return result, 11 | // If m == 0 12 | // r1: A(0, n) => n + 1 13 | (Some(n), Some(m)) if m == 0 => stack.push(n + 1), 14 | // If n == 0 15 | // r2: A(m + 1, 0) => A(m, 1) 16 | // r2: A(m, 0) => A(m - 1, 1) 17 | (Some(n), Some(m)) if n == 0 => { 18 | // Push m first 19 | stack.push(m - 1); 20 | // Push n 21 | stack.push(1); 22 | } 23 | // Else 24 | // r3: A(m + 1, n + 1) => A(m, A(m + 1, n)) 25 | // r3: A(m, n) => A(m - 1, A(m, n - 1)) 26 | (Some(n), Some(m)) => { 27 | // Push m - 1 28 | stack.push(m - 1); 29 | stack.push(m); 30 | stack.push(n - 1); 31 | } 32 | (None, None) | (None, Some(_)) => { 33 | unreachable!("we always return a result before those situations") 34 | } 35 | } 36 | } 37 | } 38 | 39 | async fn run_ackermann(req: SimpleRequest<'_>) -> SimpleResult { 40 | let parameters = req.parameters; 41 | let m = parameters.get("m").unwrap().parse().unwrap(); 42 | let n = parameters.get("n").unwrap().parse().unwrap(); 43 | log::info!("Running A({m}, {n})..."); 44 | let result = tokio::task::spawn_blocking(move || ackermann_2ary_iter(m, n)) 45 | .await 46 | .unwrap(); 47 | log::info!("A({m}, {n}) = {result}"); 48 | 49 | simple_response!(200, json!({"result": result})) 50 | } 51 | 52 | lambda_main!(async run_ackermann); 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | 57 | use super::*; 58 | 59 | #[test] 60 | fn ackermann_2ary_iter_r1() { 61 | let ns = vec![0, 1, 13, 23897]; 62 | // A(0, n) => n + 1 63 | for n in ns { 64 | assert_eq!(ackermann_2ary_iter(0, n), n + 1); 65 | } 66 | } 67 | 68 | #[test] 69 | fn ackermann_2ary_iter_r2() { 70 | let ms = vec![0, 1, 2, 3]; 71 | // A(m + 1, 0) => A(m, 1) 72 | for m in ms { 73 | assert_eq!(ackermann_2ary_iter(m + 1, 0), ackermann_2ary_iter(m, 1)); 74 | } 75 | } 76 | 77 | #[test] 78 | fn ackermann_2ary_iter_r3() { 79 | let (m, n) = (1, 1); 80 | // A(m + 1, n + 1) => A(m, A(m + 1, n)) 81 | assert_eq!( 82 | ackermann_2ary_iter(m + 1, n + 1), 83 | ackermann_2ary_iter(m, ackermann_2ary_iter(m + 1, n)) 84 | ); 85 | } 86 | 87 | #[test] 88 | fn ackermann_2ary_iter_known_res() { 89 | // A(1, 2) => 4 90 | assert_eq!(ackermann_2ary_iter(1, 2), 4); 91 | } 92 | 93 | #[test] 94 | #[ignore = "this is more a benchmark and it takes ages"] 95 | fn fake_test() { 96 | use std::time; 97 | 98 | // A(2, 10000) = 20003 99 | // 3 seconds 100 | let start = time::Instant::now(); 101 | let res = ackermann_2ary_iter(2, 10_000); 102 | let elapsed = time::Instant::now() - start; 103 | println!("ackermann_2ary_iter(2, 10_000) = {res}"); 104 | println!("Elapsed: {elapsed:?}"); 105 | println!(); 106 | 107 | // A(2, 50000) = 100003 108 | // iterations=5000350006 109 | // max_stack_size=100003 110 | // 97 seconds 111 | let start = time::Instant::now(); 112 | let res = ackermann_2ary_iter(2, 50_000); 113 | let elapsed = time::Instant::now() - start; 114 | println!("ackermann_2ary_iter(2, 50_000) = {res}"); 115 | println!("Elapsed: {elapsed:?}"); 116 | println!(); 117 | 118 | // MAX A(3, 14) 119 | // A(3, 14) = 131069 120 | // iterations=11452590818 121 | // max_stack_size=131069 122 | let start = time::Instant::now(); 123 | let res = ackermann_2ary_iter(3, 14); 124 | let elapsed = time::Instant::now() - start; 125 | println!("ackermann_2ary_iter(3, 14) = {res}"); 126 | println!("Elapsed: {elapsed:?}"); 127 | println!(); 128 | 129 | // MAX A(4, 1) 130 | // A(4, 1) = 65533 131 | // iterations=2862984011 132 | // max_stack_size=65533 133 | let start = time::Instant::now(); 134 | let res = ackermann_2ary_iter(4, 1); 135 | let elapsed = time::Instant::now() - start; 136 | println!("ackermann_2ary_iter(4, 1) = {res}"); 137 | println!("Elapsed: {elapsed:?}"); 138 | println!(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /python/layers/commons/dynamodbsheepshed/sheepshed.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import concurrent.futures 4 | from utils import logger 5 | 6 | from .sheep import Sheep, Weight 7 | from .errors import SheepDuplicationError, GenericError, SheepNotPresentError 8 | 9 | 10 | class DynamoDBSheepShed: 11 | def __init__(self): 12 | self.__table_name = os.environ['BACKEND_TABLE_NAME'] 13 | self.__table = boto3.resource('dynamodb').Table(self.__table_name) 14 | self.__client = self.__table.meta.client 15 | 16 | def _full_table_scan(self, count_only): 17 | logger.info(f"_full_table_scan(count_only={count_only})") 18 | # Request the approximate item count that DynamoDB updates sometimes 19 | approx_table_size = self.__client.describe_table(TableName=self.__table_name)['Table']['ItemCount'] 20 | logger.info(f"approx_table_size={approx_table_size}") 21 | 22 | # The database representation of a Sheep is ~50 bytes 23 | # Therefore, a scan page of 1MB will contain ~20 000 sheeps 24 | # We will be very conservative and devide the table into 100k sheeps segments because 25 | # it would be very counter-productive to have more parallel scans than we would have had pages. 26 | # Bottom line, with 100k items per segment we expect each segment to be fully scanned in 5 requests. 27 | parallel_scan_threads = min(int(1 + approx_table_size / 100_000), 1_000_000) 28 | def scan_segment(seg_i, total_segments): 29 | items = [] if not count_only else None 30 | count = 0 31 | exclusive_start_key = None 32 | scan_args = { 33 | 'Segment': seg_i, 34 | 'TotalSegments': total_segments, 35 | 'Select': "COUNT" if count_only else "ALL_ATTRIBUTES", 36 | } 37 | while True: 38 | if exclusive_start_key is not None: 39 | scan_args['ExclusiveStartKey'] = exclusive_start_key 40 | try: 41 | results = self.__table.scan(**scan_args) 42 | exclusive_start_key = results.get('LastEvaluatedKey') 43 | count += results['Count'] 44 | if not count_only: 45 | items.extend([ 46 | Sheep(tattoo=ditem['tattoo'], weight=Weight(ditem['weight'])) 47 | for ditem in results['Items'] 48 | ]) 49 | if exclusive_start_key is None: 50 | break 51 | except Exception as e: 52 | raise GenericError(str(e)) 53 | return count, items 54 | 55 | with concurrent.futures.ThreadPoolExecutor(max_workers=parallel_scan_threads) as executor: 56 | tasks = [executor.submit(scan_segment, i, parallel_scan_threads) for i in range(parallel_scan_threads)] 57 | logger.info(f"Launched {len(tasks)} python-threads") 58 | items = [] if not count_only else None 59 | count = 0 60 | for task in concurrent.futures.as_completed(tasks): 61 | t_count, t_items = task.result() 62 | count += t_count 63 | if not count_only: 64 | items.extend(t_items) 65 | 66 | logger.info(f"_full_table_scan => ({count}, {'Some' if not count_only else 'None'})") 67 | return count, items 68 | 69 | def add_sheep(self, sheep): 70 | """ 71 | Add a new [Sheep] in the [SheepShed] 72 | # Errors 73 | It is not allowed to add a duplicated [Sheep], will raise a 74 | [SheepDuplicationError] if the user tries to add 75 | a [Sheep] with an already known [Tattoo] 76 | """ 77 | logger.info(f"add_sheep(sheep={sheep})") 78 | try: 79 | self.__table.put_item( 80 | Item={ 81 | 'tattoo': sheep.tattoo, 82 | 'weight': sheep.weight.as_ug(), 83 | }, 84 | ConditionExpression='attribute_not_exists(tattoo)', 85 | ReturnValues='NONE' 86 | ) 87 | except self.__client.exceptions.ConditionalCheckFailedException: 88 | raise SheepDuplicationError(sheep.tattoo) 89 | except Exception as e: 90 | raise GenericError(str(e)) 91 | logger.info(f"add_sheep => Ok") 92 | 93 | def sheep_count(self): 94 | """Return the number of [Sheep] in the [SheepShed]""" 95 | return self._full_table_scan(True)[0] 96 | 97 | def sheep_iter(self): 98 | """Return an [Iterator] over all the [Sheep]s in the [SheepShed]""" 99 | return iter(self._full_table_scan(False)[1]) 100 | 101 | def kill_sheep(self, tattoo): 102 | """ 103 | Kill an unlucky Sheep. 104 | Remove it from the [SheepShed] and return it's body. 105 | # Errors 106 | It is not allowed to kill an inexistant [Sheep], will raise 107 | a [SheepNotPresent] if the user tries to kill a [Sheep] that 108 | is not in the [SheepShed] 109 | """ 110 | logger.info(f"kill_sheep(tattoo={tattoo})") 111 | try: 112 | result = self.__table.delete_item( 113 | Key={'tattoo': tattoo}, 114 | ConditionExpression='attribute_exists(tattoo)', 115 | ReturnValues='ALL_OLD' 116 | ) 117 | sheep = Sheep(tattoo=result['Attributes']['tattoo'], weight=Weight(result['Attributes']['weight'])) 118 | logger.info(f"kill_sheep => {sheep}") 119 | return sheep 120 | except self.__client.exceptions.ConditionalCheckFailedException: 121 | raise SheepNotPresentError(tattoo) 122 | except Exception as e: 123 | raise GenericError(str(e)) 124 | -------------------------------------------------------------------------------- /rust/libs/sheep_shed/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | mod sheep; 3 | 4 | pub use sheep::{Sheep, Tattoo, Weight, WeightUnit}; 5 | use std::collections::HashMap; 6 | 7 | /// The trait for [SheepShed] that can hold [Sheep]s and provides basic methods 8 | /// for interacting with it. 9 | pub trait SheepShed { 10 | /// Add a new [Sheep] in the [SheepShed] 11 | /// # Errors 12 | /// It is not allowed to add a duplicated [Sheep], will return an 13 | /// [errors::Error::SheepDuplicationError] if the user tries to add 14 | /// a [Sheep] with an already known [Tattoo] 15 | fn add_sheep(&mut self, sheep: Sheep) -> Result<(), errors::Error>; 16 | /// Return the number of [Sheep] in the [SheepShed] 17 | fn sheep_count(&self) -> Result; 18 | /// Return an [Iterator] over all the [Sheep]s in the [SheepShed] 19 | fn sheep_iter(&self) -> Result, errors::Error>; 20 | /// Kill an unlucky Sheep. 21 | /// Remove it from the [SheepShed] and return it's body. 22 | /// # Errors 23 | /// It is not allowed to kill an inexistant [Sheep], will return an 24 | /// [errors::Error::SheepNotPresent] if the user tries to kill 25 | /// a [Sheep] that is not in the [SheepShed] 26 | fn kill_sheep(&mut self, tattoo: &Tattoo) -> Result; 27 | } 28 | 29 | #[derive(Debug, Clone, Default)] 30 | pub struct MemorySheepShed(HashMap); 31 | 32 | impl SheepShed for MemorySheepShed { 33 | fn add_sheep(&mut self, sheep: Sheep) -> Result<(), errors::Error> { 34 | if self.0.contains_key(&sheep.tattoo) { 35 | Err(errors::Error::SheepDuplicationError(sheep.tattoo)) 36 | } else { 37 | self.0.insert(sheep.tattoo.clone(), sheep); 38 | Ok(()) 39 | } 40 | } 41 | 42 | /// Return the number of [Sheep] in the [SheepShed] 43 | /// Never returns an [Err] variant. 44 | fn sheep_count(&self) -> Result { 45 | Ok(self.0.len()) 46 | } 47 | 48 | fn sheep_iter(&self) -> Result, errors::Error> { 49 | Ok(self.0.values().cloned()) 50 | } 51 | 52 | fn kill_sheep(&mut self, tattoo: &Tattoo) -> Result { 53 | if self.0.contains_key(tattoo) { 54 | Ok(self.0.remove(tattoo).unwrap()) 55 | } else { 56 | Err(errors::Error::SheepNotPresent(tattoo.to_owned())) 57 | } 58 | } 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | 64 | use super::*; 65 | 66 | macro_rules! impl_test_template { 67 | ($tn: tt) => { 68 | #[test] 69 | fn $tn() { 70 | let sheep_shed = MemorySheepShed::default(); 71 | crate::test_templates::$tn(sheep_shed) 72 | } 73 | }; 74 | } 75 | 76 | impl_test_template!(cannot_duplicate_sheep); 77 | impl_test_template!(sheep_shed_sheep_count); 78 | impl_test_template!(sheep_shed_iterator); 79 | impl_test_template!(cannot_kill_inexistent_sheep); 80 | } 81 | 82 | #[cfg(any(feature = "sheepshed_tests", test))] 83 | pub mod test_templates { 84 | use crate::{errors::Error, sheep::WeightUnit, Sheep, SheepShed, Tattoo, Weight}; 85 | 86 | fn prep_base_sheep_shed(mut sheep_shed: T) -> T { 87 | let sheep1 = Sheep { 88 | tattoo: Tattoo(1), 89 | weight: Weight::from_unit(100.0, WeightUnit::Kilograms), 90 | }; 91 | let sheep2 = Sheep { 92 | tattoo: Tattoo(2), 93 | weight: Weight::from_unit(120.0, WeightUnit::Kilograms), 94 | }; 95 | sheep_shed.add_sheep(sheep1).unwrap(); 96 | sheep_shed.add_sheep(sheep2).unwrap(); 97 | sheep_shed 98 | } 99 | 100 | pub fn cannot_duplicate_sheep(sheep_shed: T) { 101 | let mut sheep_shed = prep_base_sheep_shed(sheep_shed); 102 | let sheep3 = Sheep { 103 | tattoo: Tattoo(1), 104 | weight: Weight::from_unit(120.0, WeightUnit::Kilograms), 105 | }; 106 | // Sheep3 has the same Tattoo as Sheep1 so it should fail 107 | assert!(sheep_shed.add_sheep(sheep3).is_err_and(|e| match e { 108 | Error::SheepDuplicationError(_) => true, 109 | _ => false, 110 | })); 111 | } 112 | 113 | pub fn sheep_shed_sheep_count(sheep_shed: T) { 114 | let mut sheep_shed = prep_base_sheep_shed(sheep_shed); 115 | let sheep3 = Sheep { 116 | tattoo: Tattoo(4), 117 | weight: Weight::from_unit(120.0, WeightUnit::Kilograms), 118 | }; 119 | assert_eq!(sheep_shed.sheep_count().unwrap(), 2); 120 | sheep_shed.add_sheep(sheep3).unwrap(); 121 | assert_eq!(sheep_shed.sheep_count().unwrap(), 3); 122 | } 123 | 124 | pub fn sheep_shed_iterator(sheep_shed: T) { 125 | let sheep_shed = prep_base_sheep_shed(sheep_shed); 126 | let weight = sheep_shed 127 | .sheep_iter() 128 | .unwrap() 129 | .fold(Weight::ZERO, |acc, sheep| acc + sheep.weight); 130 | assert_eq!(weight, Weight::from_unit(220.0, WeightUnit::Kilograms)); 131 | } 132 | 133 | pub fn cannot_kill_inexistent_sheep(sheep_shed: T) { 134 | let mut sheep_shed = prep_base_sheep_shed(sheep_shed); 135 | // Inexistant tattoo 136 | assert!(sheep_shed.kill_sheep(&Tattoo(4)).is_err_and(|e| match e { 137 | Error::SheepNotPresent(_) => true, 138 | _ => false, 139 | })); 140 | // Existing tattoo 141 | assert!(sheep_shed.kill_sheep(&Tattoo(2)).is_ok()); 142 | // Not anymore 143 | assert!(sheep_shed.kill_sheep(&Tattoo(2)).is_err_and(|e| match e { 144 | Error::SheepNotPresent(_) => true, 145 | _ => false, 146 | })); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /rust/libs/sheep_shed/src/sheep.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The various units of [Weight] that we can display 6 | #[derive(Debug, Clone, Copy)] 7 | pub enum WeightUnit { 8 | Micrograms, 9 | Milligrams, 10 | Grams, 11 | Kilograms, 12 | } 13 | impl WeightUnit { 14 | fn best_unit(weight: &Weight) -> Self { 15 | match weight.0 { 16 | w if w > 1_000_000_000 => WeightUnit::Kilograms, 17 | w if w > 1_000_000 => WeightUnit::Grams, 18 | w if w > 1_000 => WeightUnit::Milligrams, 19 | _ => WeightUnit::Micrograms, 20 | } 21 | } 22 | 23 | fn unit_ratio(&self) -> f64 { 24 | match self { 25 | WeightUnit::Micrograms => 1.0, 26 | WeightUnit::Milligrams => 1_000.0, 27 | WeightUnit::Grams => 1_000_000.0, 28 | WeightUnit::Kilograms => 1_000_000_000.0, 29 | } 30 | } 31 | } 32 | impl Display for WeightUnit { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | match self { 35 | WeightUnit::Micrograms => write!(f, "ug"), 36 | WeightUnit::Milligrams => write!(f, "mg"), 37 | WeightUnit::Grams => write!(f, "g"), 38 | WeightUnit::Kilograms => write!(f, "kg"), 39 | } 40 | } 41 | } 42 | 43 | /// Represent a weight. Internally stored as a [u64] representing micrograms. 44 | /// This struct provide a [Display] impl that always print the [Weight] with 45 | /// the correct unit. 46 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] 47 | #[serde(transparent)] 48 | pub struct Weight(u64); 49 | impl Weight { 50 | pub const ZERO: Weight = Weight(0); 51 | pub const MIN: Weight = Weight(80_000_000_000); 52 | pub const MAX: Weight = Weight(160_000_000_000); 53 | 54 | /// Instantiate a [Weight] from a [f64] in the specified [WeightUnit]. 55 | pub fn from_unit(weight: f64, wu: WeightUnit) -> Self { 56 | Self((weight * wu.unit_ratio()) as u64) 57 | } 58 | 59 | /// Instantiate a [Weight] from a [u64] in micrograms. 60 | pub fn from_ug(weight: u64) -> Self { 61 | Self(weight) 62 | } 63 | 64 | /// Return the [Weight] as an [f64] in the specified [WeightUnit]. 65 | pub fn as_unit(&self, wu: WeightUnit) -> f64 { 66 | self.0 as f64 / wu.unit_ratio() 67 | } 68 | 69 | /// Return the [Weight] as an [u64] in micrograms. 70 | pub fn as_ug(&self) -> u64 { 71 | self.0 72 | } 73 | } 74 | 75 | impl Display for Weight { 76 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 77 | let best_unit = WeightUnit::best_unit(self); 78 | write!(f, "{:.3}{best_unit}", self.as_unit(best_unit)) 79 | } 80 | } 81 | impl std::ops::Add for Weight { 82 | type Output = Self; 83 | 84 | fn add(self, rhs: Self) -> Self::Output { 85 | Weight(self.0 + rhs.0) 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)] 90 | #[serde(transparent)] 91 | pub struct Tattoo(pub u64); 92 | impl Display for Tattoo { 93 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 94 | self.0.fmt(f) 95 | } 96 | } 97 | 98 | #[derive(Debug, Clone, Serialize, Deserialize)] 99 | pub struct Sheep { 100 | pub tattoo: Tattoo, 101 | pub weight: Weight, 102 | } 103 | impl Display for Sheep { 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | write!(f, "Sheep({}) weighting {}", self.tattoo, self.weight) 106 | } 107 | } 108 | /// Sheeps equality is only driven by their [Tattoo] equality 109 | impl PartialEq for Sheep { 110 | fn eq(&self, other: &Self) -> bool { 111 | self.tattoo == other.tattoo 112 | } 113 | } 114 | impl Eq for Sheep {} 115 | 116 | /// [std::hash::Hash] implementation must be equivalent to [Eq] 117 | /// so we ignore the weight field of the [Sheep] 118 | impl std::hash::Hash for Sheep { 119 | fn hash(&self, state: &mut H) { 120 | self.tattoo.hash(state); 121 | } 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | use super::*; 127 | 128 | #[test] 129 | fn weight_display() { 130 | assert_eq!( 131 | Weight::from_unit(1.430983, WeightUnit::Kilograms).to_string(), 132 | "1.431kg" 133 | ); 134 | assert_eq!( 135 | Weight::from_unit(3489.43982, WeightUnit::Grams).to_string(), 136 | "3.489kg" 137 | ); 138 | assert_eq!( 139 | Weight::from_unit(489.43982, WeightUnit::Grams).to_string(), 140 | "489.440g" 141 | ); 142 | assert_eq!( 143 | Weight::from_unit(432.87, WeightUnit::Milligrams).to_string(), 144 | "432.870mg" 145 | ); 146 | assert_eq!(Weight::from_ug(976).to_string(), "976.000ug"); 147 | assert_eq!( 148 | Weight::from_unit(0.457, WeightUnit::Milligrams).to_string(), 149 | "457.000ug" 150 | ); 151 | } 152 | 153 | #[test] 154 | fn sheeps_equal_equiv_tattoos_equal() { 155 | let t1 = Tattoo(1); 156 | let t2 = Tattoo(2); 157 | let w1 = Weight::from_unit(100.0, WeightUnit::Kilograms); 158 | let w2 = Weight::from_unit(150.0, WeightUnit::Kilograms); 159 | 160 | let sheep1 = Sheep { 161 | tattoo: t1.clone(), 162 | weight: w1, 163 | }; 164 | let sheep2 = Sheep { 165 | tattoo: t2.clone(), 166 | weight: w1, 167 | }; 168 | let sheep3 = Sheep { 169 | tattoo: t1, 170 | weight: w2, 171 | }; 172 | let sheep4 = Sheep { 173 | tattoo: t2, 174 | weight: w2, 175 | }; 176 | assert_eq!(sheep1, sheep3); 177 | assert_eq!(sheep2, sheep4); 178 | assert_ne!(sheep1, sheep2); 179 | assert_ne!(sheep3, sheep4); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /rust/lambdas/delete-wolf-ocd/src/main.rs: -------------------------------------------------------------------------------- 1 | use dynamodb_sheep_shed::DynamoDBSheepShed; 2 | use sheep_shed::{Sheep, SheepShed, Weight}; 3 | 4 | use lambda_apigw_utils::prelude::*; 5 | 6 | /// Create a Sieve of Eratostenes containing all the primes between 0 and n 7 | fn sieve_of_eratosthenes(n: u64) -> Vec { 8 | assert!(n < usize::MAX as u64); 9 | if n < 2 { 10 | return vec![]; 11 | } 12 | // Boolean array with a value for every number from 0 to n 13 | // Initially every number from 0 to n is considered prime 14 | // So the array is initialized at "true" for every index 15 | let mut tmp = vec![true; n as usize + 1]; 16 | // 0 and 1 are not primes 17 | tmp[0] = false; 18 | tmp[1] = false; 19 | // Compute the square root of n, rounding up 20 | let sqrt_n = (n as f64).sqrt() as usize + 1; 21 | // Cast n to an usize instead of a u64 22 | let n_usize = n as usize; 23 | 24 | // For every candidate i from 2 to SquareRoot(n) rounded up excluded 25 | for i in 2..sqrt_n { 26 | // If the candidate i is prime 27 | // Exemple1: i = 2 28 | // Exemple2: i = 3 29 | if tmp[i] { 30 | // Then initialize j = i^2, this optimization work because of maths: 31 | // any multiple of our prime "i" that is inferior to i^2 MUST BE 32 | // a multiple of a previously processed prime, so already marked false. 33 | // When we process multiples of 3, we start at 9, skipping 6, 34 | // but 6 is 2*3 and was already taken care of when processing multiples of 2. 35 | // Exemple1: j = 4 36 | // Exemple2: j = 9 37 | let mut j = i * i; 38 | // As long as j is <= n 39 | while j <= n_usize { 40 | // Mark every j as "not prime" 41 | // Exemple1: 4, 6, 8, etc... 42 | // Exemple2: 9, 12, 15, 18, etc... 43 | tmp[j] = false; 44 | // Increment j by i 45 | // Exemple1: j += 2 46 | // Exemple2: j += 3 47 | j += i; 48 | } 49 | } 50 | } 51 | // At this point: 52 | // tmp[i] = true if i is prime 53 | // tmp[i] = false if i is NOT prime 54 | // Iterate over tmp to extract our sieve 55 | tmp.into_iter() 56 | // Enumerate provide the index alongside 57 | // the corresponding boolean value 58 | .enumerate() 59 | // We "filter" to keep only the prime indexes 60 | .filter(|(_index, is_prime)| *is_prime) 61 | // Indexes are of type usize but we want u64 62 | // So we "map" the values 63 | .map(|(index, _is_prime)| index as u64) 64 | // We collect 65 | .collect() 66 | } 67 | 68 | /// This wolf suffer from Obsessive-Compulsive disorder: it is hungry, but it cannot kill just any sheep !! 69 | /// 70 | /// It is very important for the wolf that the [Weight] of the [Sheep] expressed in micro-grams is a 71 | /// prime number!!! And of course, the bigest possible. 72 | async fn wolf_ocd(_req: SimpleRequest<'_>) -> SimpleResult { 73 | let handle = tokio::runtime::Handle::current(); 74 | 75 | // The wolf is multi-tasking: he knows retrieving infos on all the sheep 76 | // will take time, and computing primes too, so he is spawning a thread to 77 | // compute the primes. 78 | let sieve_max = (Weight::MAX.as_ug() as f64).sqrt() as u64; 79 | log::info!("spawning primes sieve generation (2 to {sieve_max})..."); 80 | let f_sieve = handle.spawn_blocking(move || sieve_of_eratosthenes(sieve_max)); 81 | 82 | // Then another thread to retrieve the sheeps 83 | log::info!("retrieving all the sheeps..."); 84 | let f_sheeps = handle.spawn_blocking(move || { 85 | log::info!("create a shed instance"); 86 | DynamoDBSheepShed::new(dynamo()) 87 | .sheep_iter() 88 | .map(|iter| iter.collect::>()) 89 | }); 90 | 91 | // Wait both thread finishes 92 | let sieve = f_sieve.await.unwrap(); 93 | log::info!("sieve contains {} primes", sieve.len()); 94 | 95 | let sheeps = f_sheeps.await.unwrap()?; 96 | log::info!("sheep list contains {} sheep", sheeps.len()); 97 | 98 | // Find a suitable sheep 99 | let sheep_to_eat = sheeps 100 | .into_iter() 101 | .filter(|sheep| { 102 | let sheep_weight_ug = sheep.weight.as_ug(); 103 | for &prime in &sieve { 104 | if sheep_weight_ug % prime == 0 { 105 | return false; 106 | } 107 | } 108 | true 109 | }) 110 | .fold(None, |heaviest_sheep, current_sheep| { 111 | // If there is no heaviest_sheep or heaviest_sheep is lighter than the current sheep 112 | if !heaviest_sheep 113 | .as_ref() 114 | .is_some_and(|hs: &Sheep| hs.weight > current_sheep.weight) 115 | { 116 | Some(current_sheep) 117 | } else { 118 | heaviest_sheep 119 | } 120 | }); 121 | 122 | // If we found a suitable sheep, eat it and return 204 123 | if let Some(sheep) = &sheep_to_eat { 124 | let sheep_tattoo = sheep.tattoo.clone(); 125 | log::info!("wolf will eat {sheep}"); 126 | let _ = handle 127 | .spawn_blocking(move || { 128 | log::info!("create a shed instance"); 129 | DynamoDBSheepShed::new(dynamo()).kill_sheep(&sheep_tattoo) 130 | }) 131 | .await 132 | .unwrap() 133 | .map_err(|e| { 134 | // In this specific case, we consider SheepNotPresent to be a 500 135 | if let sheep_shed::errors::Error::SheepNotPresent(_) = e { 136 | SimpleError::Custom { 137 | code: 500, 138 | message: e.to_string(), 139 | } 140 | } else { 141 | // Any other error will follow the standard conversion 142 | e.into() 143 | } 144 | })?; 145 | simple_response!(204) 146 | // Else do nothing and return 404 147 | } else { 148 | log::info!("it seems the wolf will continue to starve..."); 149 | simple_response!(404, json!({"message": "No fitting sheep"})) 150 | } 151 | } 152 | 153 | lambda_main!(async wolf_ocd, dynamo = aws_sdk_dynamodb::Client); 154 | 155 | #[cfg(test)] 156 | mod tests { 157 | 158 | use super::*; 159 | 160 | #[test] 161 | fn small_sieve_of_eratosthenes() { 162 | let soe = sieve_of_eratosthenes(1000); 163 | let primes_to_1000 = vec![ 164 | 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 165 | 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 166 | 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 167 | 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 168 | 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 169 | 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 170 | 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 171 | 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 172 | 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 173 | 947, 953, 967, 971, 977, 983, 991, 997, 174 | ]; 175 | assert_eq!(soe, primes_to_1000); 176 | } 177 | 178 | #[test] 179 | fn is_prime_101() { 180 | let soe = sieve_of_eratosthenes(101); 181 | assert_eq!(soe.last().cloned().unwrap(), 101); 182 | } 183 | 184 | #[test] 185 | fn sieve_0() { 186 | let soe = sieve_of_eratosthenes(0); 187 | assert_eq!(soe, Vec::::new()); 188 | } 189 | 190 | #[test] 191 | fn sieve_1() { 192 | let soe = sieve_of_eratosthenes(1); 193 | assert_eq!(soe, Vec::::new()); 194 | } 195 | 196 | #[test] 197 | fn sieve_2() { 198 | let soe = sieve_of_eratosthenes(2); 199 | assert_eq!(soe, vec![2]); 200 | } 201 | 202 | #[test] 203 | fn is_not_prime_102() { 204 | let soe = sieve_of_eratosthenes(102); 205 | assert_eq!(soe.last().cloned().unwrap(), 101); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /ci-config/buildspec-rust.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | RUST_VERSION: "1.79.0" 6 | RUST_TARGET_ARCH: "aarch64-unknown-linux-gnu" 7 | 8 | phases: 9 | install: 10 | runtime-versions: 11 | python: 3.12 12 | commands: 13 | - echo Entered the install phase on `date` 14 | - | 15 | echo Installing Rust $RUST_VERSION 16 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none --profile minimal 17 | export PATH="/root/.cargo/bin:$PATH" 18 | rustup toolchain install $RUST_VERSION 19 | rustup default $RUST_VERSION 20 | pip3 install cargo-lambda 21 | finally: 22 | - | 23 | echo Changing all file times to 2010-01-01 00:00:00 \(arbitrary\) to avoid inducing changes in ZIP packages 24 | find . -not -path '*/.git/*' -exec touch -a -m -t"201001010000.00" {} \; 25 | - echo Exiting the install phase on `date` 26 | pre_build: 27 | on-failure: ABORT 28 | commands: 29 | - echo Entered the pre_build phase on `date` 30 | ################## 31 | # RUST PRE-BUILD # 32 | ################## 33 | # Declare the paths where we store our lambdas and libs 34 | - | 35 | RUST_LIB_PATH=./rust/libs 36 | RUST_LAMBDA_PATH=./rust/lambdas 37 | # We verify if we changed Rust version since the last build by comparing the current version 38 | # with the one stored in cache (if any). If we have changed Rust version, we clean every 39 | # build artifacts. As we don't want to change the Cargo.lock that fixes packages versions, 40 | # we save it before cleaning and restore it after. 41 | # In the following section, we compute MD5sums for each folder under the lib and lambda path 42 | # For each lib/lambda, if it has changed compared to the previous build, we add it to the "to clean" list 43 | # Then we compare the Cargo.lock of the previous build we the current one, looking for packages 44 | # that disappeared and adding them to the "to clean" list if needed 45 | # In the end, we clean every build artifacts for the packages in the "to clean" list. 46 | # As previously, we protect the Cargo.lock, because we want to use exactly the commited one in the 47 | # build stage. 48 | - | 49 | echo Cleaning targets cache if necessary 50 | declare -A PACKAGES_TO_CLEAN 51 | echo Creating the rust_target_md5sum if needed 52 | if ! [ -d rust_target_md5sum ] ; then mkdir rust_target_md5sum ; fi 53 | echo Selectively clean-up LIB artifacts if code has changed 54 | for LIB in $(ls $RUST_LIB_PATH) ; do 55 | echo Control MD5 for $RUST_LIB_PATH/$LIB 56 | cd $RUST_LIB_PATH/$LIB 57 | current_md5=$(find . | sort | zip -@ - | md5sum | cut -f1 -d" ") 58 | cd $CODEBUILD_SRC_DIR 59 | if ! previous_md5=$(cat rust_target_md5sum/$LIB 2>/dev/null) || [ "$previous_md5" != "$current_md5" ] ; then 60 | echo previous_md5=$previous_md5 current_md5=$current_md5 =\> Clean lib $LIB and updating md5sum 61 | PACKAGES_TO_CLEAN+=([$LIB]=1) 62 | echo $current_md5 > rust_target_md5sum/$LIB 63 | fi 64 | done 65 | echo Selectively clean-up LAMBDA artifacts if code has changed 66 | for LAMBDA in $(ls $RUST_LAMBDA_PATH) ; do 67 | echo Control MD5 for $RUST_LAMBDA_PATH/$LAMBDA 68 | cd $RUST_LAMBDA_PATH/$LAMBDA 69 | current_md5=$(find . | sort | zip -@ - | md5sum | cut -f1 -d" ") 70 | cd $CODEBUILD_SRC_DIR 71 | if ! previous_md5=$(cat rust_target_md5sum/$LAMBDA 2>/dev/null) || [ "$previous_md5" != "$current_md5" ] ; then 72 | echo previous_md5=$previous_md5 current_md5=$current_md5 => Clean lambda $LAMBDA and updating md5sum 73 | PACKAGES_TO_CLEAN+=([$LAMBDA]=1) 74 | echo $current_md5 > rust_target_md5sum/$LAMBDA 75 | fi 76 | done 77 | # Only execute this section if there is a previous Cargo.lock 78 | if [ -f Cargo.lock.old ] ; then 79 | echo "Cargo.lock from the previous build found, restoring it" 80 | mv Cargo.lock Cargo.lock.new 81 | cp Cargo.lock.old Cargo.lock 82 | echo Building the current packages set from the current Cargo.lock... 83 | declare -A CURRENT_PACKAGES 84 | for p in $(perl -0pe 's/^.*?(\[\[package\]\]\nname = "(\S+)"\nversion = "(\S+)".*?\n)+$/\2@\3\n/gms' < Cargo.lock.new) ; do 85 | CURRENT_PACKAGES+=([$p]=1) 86 | done 87 | echo Current package set contains ${#CURRENT_PACKAGES[@]} packages. 88 | echo Finding all packages that are no longer needed... 89 | # For each 'package' in the old Cargo.lock 90 | count=0 91 | for p in $(perl -0pe 's/^.*?(\[\[package\]\]\nname = "(\S+)"\nversion = "(\S+)".*?\n)+$/\2@\3\n/gms' < Cargo.lock.old) ; do 92 | count=$(( $count + 1 )) 93 | # If 'package' is not in the current Cargo.lock 94 | if ! [[ ${CURRENT_PACKAGES[$p]} -eq 1 ]]; then 95 | package_without_version=$(cut -f1 -d'@' <<< $p) 96 | PACKAGES_TO_CLEAN+=([$package_without_version]=1) 97 | echo "$p and its dependencies will be cleaned" 98 | for ptc in $(cargo tree --prefix none -e normal,build --target all --invert $p | sed -r 's/^(\S+).*$/\1/'); do 99 | PACKAGES_TO_CLEAN+=([$ptc]=1) 100 | done 101 | fi 102 | done 103 | echo Old package set contains $count packages. 104 | fi 105 | # Save the Cargo.lock 106 | cp Cargo.lock Cargo.lock.back 107 | echo Attempting to clean ${#PACKAGES_TO_CLEAN[@]} packages out of the cache... 108 | SORTED_PACKAGES_TO_CLEAN=$(echo ${!PACKAGES_TO_CLEAN[@]} | xargs -n1 | sort | xargs) 109 | for package in $SORTED_PACKAGES_TO_CLEAN; do 110 | echo Cleaning $package 111 | cargo clean --target $RUST_TARGET_ARCH --release --package $package 112 | done 113 | # Restore the Cargo.lock in case it has been modified by cargo clean 114 | mv Cargo.lock.back Cargo.lock 115 | # Only execute this section if there is a previous Cargo.lock 116 | if [ -f Cargo.lock.old ] ; then 117 | echo "Restoring Cargo.lock to its current commited version" 118 | rm -f Cargo.lock 119 | mv Cargo.lock.new Cargo.lock 120 | fi 121 | - | 122 | echo Saving the current Cargo.lock as Cargo.lock.old for future builds 123 | cp Cargo.lock Cargo.lock.old 124 | finally: 125 | - echo Exiting the pre_build phase on `date` 126 | build: 127 | on-failure: ABORT 128 | commands: 129 | - echo Entered the build phase on `date` 130 | - | 131 | echo Composing the release build command... 132 | CMD="cargo lambda build --release --target $RUST_TARGET_ARCH" 133 | for LAMBDA in $(ls $RUST_LAMBDA_PATH) ; do 134 | echo Will build $LAMBDA 135 | CMD="$CMD --package $LAMBDA" 136 | done 137 | echo $CMD 138 | eval $CMD 139 | - | 140 | echo Packaging lambdas... 141 | for LAMBDA in $(ls $RUST_LAMBDA_PATH) ; do 142 | echo Preping: $LAMBDA 143 | # Remove all content 144 | rm -rf $RUST_LAMBDA_PATH/$LAMBDA/* 145 | # Move the compiled binary into the Lambda folder 146 | mv ./target/lambda/$LAMBDA/bootstrap $RUST_LAMBDA_PATH/$LAMBDA/bootstrap 147 | done 148 | finally: 149 | - | 150 | echo Changing all file times to 2010-01-01 00:00:00 \(arbitrary\) to avoid inducing changes in ZIP packages 151 | find . -not -path '*/.git/*' -exec touch -a -m -t"201001010000.00" {} \; 152 | - echo Exiting the build phase on `date` 153 | post_build: 154 | commands: 155 | - echo Entered the post_build phase on `date` 156 | - | 157 | echo Soft-link lambda folders 158 | mkdir lambdas 159 | ORIG_LAMBDA_FOLDER=$CODEBUILD_SRC_DIR/rust/lambdas 160 | cd lambdas 161 | for LAMBDA_FOLDER in $(ls $ORIG_LAMBDA_FOLDER) ; do 162 | echo Soft-link ./lambdas/$LAMBDA_FOLDER to $ORIG_LAMBDA_FOLDER/$LAMBDA_FOLDER 163 | ln -s $ORIG_LAMBDA_FOLDER/$LAMBDA_FOLDER $LAMBDA_FOLDER 164 | done 165 | cd $CODEBUILD_SRC_DIR 166 | - | 167 | echo Packaging the demo-template file references... 168 | mv demo-template.yml demo-template.yml.bak 169 | aws cloudformation package --template-file demo-template.yml.bak --s3-bucket $ARTIFACT_BUCKET --s3-prefix templates --output-template-file demo-template.yml 170 | finally: 171 | - echo Exiting the post_build phase on `date` 172 | artifacts: 173 | base-directory: . 174 | files: 175 | - demo-template.yml 176 | cache: 177 | paths: 178 | - target/**/* # Rust workspace target directory 179 | - rust_target_md5sum/**/* # Where we store md5sum for code 180 | - Cargo.lock.old # The last seen Cargo.lock 181 | - previous_build_rust_version # The last seen Rust toolchain version 182 | -------------------------------------------------------------------------------- /rust/libs/lambda_apigw_utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use lambda_commons_utils::aws_lambda_events::apigw::{ 2 | ApiGatewayProxyRequest, ApiGatewayProxyResponse, 3 | }; 4 | pub use lambda_http; 5 | use lambda_http::{ 6 | http::{ 7 | header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}, 8 | HeaderMap, 9 | }, 10 | Body, Error, 11 | }; 12 | use serde_json::{json, Value}; 13 | 14 | use std::collections::HashMap; 15 | 16 | pub mod errors; 17 | pub use errors::SimpleError; 18 | 19 | // Re-export other crate that will always be usefull in Lambda functions 20 | pub use lambda_commons_utils; 21 | pub use lambda_commons_utils::aws_config; 22 | pub use lambda_commons_utils::serde_json; 23 | 24 | /// Structure containing references on important values from the Amazon Cognito 25 | /// token as extracted by API Gateway using Proxy integration 26 | #[derive(Debug)] 27 | pub struct CognitoValues<'a> { 28 | /// The Cognito UserId, `sub` field of the OIDC token 29 | pub user_id: &'a str, 30 | /// The Cognito email, if the `email` field is present 31 | pub email: Option<&'a str>, 32 | /// The Cognito username, if the `cognito:username` field is present 33 | pub username: Option<&'a str>, 34 | } 35 | 36 | /// Given an immutable reference to a [Request], returns a [CognitoValues] structure 37 | /// if the [Request] was received from an API Gatway with Lambda proxy integration. 38 | /// Returns [None] if Cognito claims are not present. 39 | pub fn extract_cognito_values(event: &ApiGatewayProxyRequest) -> Option { 40 | if let Some(Value::Object(claims)) = event.request_context.authorizer.fields.get("claims") { 41 | return Some(CognitoValues { 42 | user_id: claims 43 | .get("sub") 44 | .expect("sub is always present") 45 | .as_str() 46 | .expect("sub is always a String"), 47 | email: claims 48 | .get("email") 49 | .filter(|&v| v.is_string()) 50 | .map(|v| v.as_str().expect("just tested it's a string")), 51 | username: claims 52 | .get("cognito:username") 53 | .filter(|&v| v.is_string()) 54 | .map(|v| v.as_str().expect("just tested it's a string")), 55 | }); 56 | } 57 | None 58 | } 59 | 60 | /// Given an immutable reference to a [Request], returns a [HashMap] structure 61 | /// of [HashMap<&str, &str>] 62 | /// Returns [None] if Cognito claims are not present. 63 | pub fn extract_parameters(event: &ApiGatewayProxyRequest) -> HashMap<&str, &str> { 64 | let mut parameters = HashMap::default(); 65 | parameters.extend(event.query_string_parameters.iter()); 66 | parameters.extend( 67 | event 68 | .path_parameters 69 | .iter() 70 | .map(|(k, v)| (k.as_str(), v.as_str())), 71 | ); 72 | parameters 73 | } 74 | 75 | pub fn extract_body(event: &ApiGatewayProxyRequest) -> &str { 76 | event.body.as_ref().map(|s| s.as_str()).unwrap_or("") 77 | } 78 | 79 | pub fn standard_response( 80 | simple_response: SimpleResponse, 81 | ) -> Result { 82 | let SimpleResponse { code, body } = simple_response; 83 | let status_code = code as i64; 84 | let mut headers = HeaderMap::new(); 85 | headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); 86 | headers.insert( 87 | ACCESS_CONTROL_ALLOW_ORIGIN, 88 | std::env::var("ALLOW_ORIGIN") 89 | .expect("Mandatory environment variable `ALLOW_ORIGIN` is not set") 90 | .parse() 91 | .unwrap(), 92 | ); 93 | 94 | let body = match body { 95 | Some(body) => Some(Body::Text(body.to_string())), 96 | None => Some(Body::Empty), 97 | }; 98 | Ok(ApiGatewayProxyResponse { 99 | status_code, 100 | headers: headers.clone(), 101 | multi_value_headers: headers, 102 | body, 103 | is_base64_encoded: false, 104 | }) 105 | } 106 | 107 | #[derive(Debug)] 108 | pub struct SimpleRequest<'a> { 109 | // The Cognito values, if the resquest is authenticated by Cognito/APIGateway 110 | pub cognito_values: Option>, 111 | // Query parameters and path parameters 112 | pub parameters: HashMap<&'a str, &'a str>, 113 | // Raw body 114 | pub body: &'a str, 115 | } 116 | 117 | #[derive(Debug)] 118 | pub struct SimpleResponse { 119 | pub code: u16, 120 | pub body: Option, 121 | } 122 | #[macro_export] 123 | macro_rules! simple_response { 124 | ($code:literal) => { 125 | Ok($crate::SimpleResponse { 126 | code: $code, 127 | body: None, 128 | }) 129 | }; 130 | ($code:literal, $($body:tt)+) => { 131 | Ok($crate::SimpleResponse { 132 | code: $code, 133 | body: Some($($body)+), 134 | }) 135 | }; 136 | } 137 | 138 | impl From for SimpleResponse { 139 | fn from(value: SimpleError) -> Self { 140 | let code = match value { 141 | SimpleError::InvalidInput(_) | SimpleError::InvalidBody => 400, 142 | SimpleError::NotFound { .. } => 404, 143 | SimpleError::Unauthorized => 401, 144 | SimpleError::ServerError(_) | SimpleError::InvalidState(_) => 500, 145 | SimpleError::Custom { code, message } => { 146 | return SimpleResponse { 147 | code, 148 | body: Some(json!({"message": message})), 149 | } 150 | } 151 | }; 152 | SimpleResponse { 153 | code, 154 | body: Some(json!({"message": value.to_string()})), 155 | } 156 | } 157 | } 158 | 159 | pub type SimpleResult = Result; 160 | 161 | #[macro_export(local_inner_macros)] 162 | macro_rules! sync_or_async { 163 | (sync $blk:block) => { 164 | tokio::task::spawn_blocking(move || $blk).await.unwrap() 165 | }; 166 | (async $blk:block) => { 167 | $blk.await 168 | }; 169 | } 170 | 171 | #[macro_export(local_inner_macros)] 172 | macro_rules! lambda_main_internal { 173 | ($sync_or_async:ident, $rc:ident, $with_auth:literal $(,$fn_name:ident = $sdk:ty)*) => { 174 | async fn http_function_handler( 175 | event: $crate::ApiGatewayProxyRequest, 176 | ) -> Result< 177 | $crate::ApiGatewayProxyResponse, 178 | $crate::lambda_http::Error, 179 | > { 180 | $crate::lambda_commons_utils::log::info!("{event:?}"); 181 | 182 | let lambda_result = $crate::sync_or_async!($sync_or_async { 183 | // Extract Cognito values from the claims of the token 184 | let cognito_values = $crate::extract_cognito_values(&event); 185 | $crate::lambda_commons_utils::log::debug!("cognito_values={cognito_values:?}"); 186 | if $with_auth { 187 | // User_id is in fact "sub" so it cannot be absent unless there is no auth at all 188 | if cognito_values.is_none() { 189 | $crate::lambda_commons_utils::log::error!("No token could be found"); 190 | return Err($crate::SimpleError::Unauthorized.into()); 191 | } 192 | } 193 | 194 | let parameters = $crate::extract_parameters(&event); 195 | $crate::lambda_commons_utils::log::debug!("parameters={parameters:?}"); 196 | 197 | let body = $crate::extract_body(&event); 198 | $crate::lambda_commons_utils::log::debug!("body={body}"); 199 | 200 | let simple_request = $crate::SimpleRequest { 201 | cognito_values, 202 | parameters, 203 | body, 204 | }; 205 | $crate::lambda_commons_utils::log::debug!("simple_request={simple_request:?}"); 206 | $rc(simple_request) 207 | }); 208 | 209 | $crate::lambda_commons_utils::log::debug!("lambda_result={lambda_result:?}"); 210 | match lambda_result { 211 | Ok(simple_response) => $crate::standard_response(simple_response), 212 | Err(simple_error) => $crate::standard_response(simple_error.into()), 213 | } 214 | } 215 | $crate::lambda_commons_utils::lambda_main!( 216 | async http_function_handler($crate::ApiGatewayProxyRequest)->$crate::ApiGatewayProxyResponse 217 | $(, $fn_name = $sdk)* 218 | ); 219 | }; 220 | } 221 | 222 | #[macro_export] 223 | /// This macro writes the boiler-plate code for lambda that **DON'T NEED** to ensure that the 224 | /// original API Gateway call was authenticated with Cognito. 225 | /// 226 | /// If you need the [CognitoValues] extracted and included in the [SimpleRequest], see [auth_lambda_main]. 227 | /// 228 | /// It writes the boiler-plate code common to most of (if not all) the Lambda functions of the project 229 | /// The first argument is the functional entry-point that will receive a preprocessed [SimpleRequest] and must 230 | /// return a [SimpleResult]. 231 | /// 232 | /// Following arguments are in the form ` = ` and asks for AWS SDK initialization. 233 | /// - `fct_name` is the function name with which you will retrieve the Client 234 | /// - `SDKClient` is the type of the AWS SDK Client you wish to be returned with the function 235 | /// 236 | /// All in all, this macro allows each lambda code to focus on the actual job it needs to do rather 237 | /// than on writing the same boiler-plate over and over in a project. 238 | /// # Example 239 | /// 240 | /// ``` 241 | /// use lambda_apigw_utils::prelude::*; 242 | /// use serde_json::json; 243 | /// fn echo_process(req: SimpleRequest<'_>) -> SimpleResult { 244 | /// let parameters = req.parameters; 245 | /// let body = req.body; 246 | /// 247 | /// let client = dynamo(); 248 | /// 249 | /// simple_response!(200, json!({"parameters": parameters, "body": body})) 250 | /// } 251 | /// 252 | /// lambda_main!(echo_process, dynamo = aws_sdk_dynamodb::Client); 253 | /// ``` 254 | macro_rules! lambda_main { 255 | ($sync_or_async:ident $rc:ident $(,$fn_name:ident = $sdk:ty)*) => { 256 | $crate::lambda_main_internal!($sync_or_async, $rc, false $(, $fn_name = $sdk)*); 257 | }; 258 | ($rc:ident $(,$fn_name:ident = $sdk:ty)*) => { 259 | $crate::lambda_main_internal!(sync, $rc, false $(, $fn_name = $sdk)*); 260 | }; 261 | } 262 | 263 | #[macro_export] 264 | /// This macro writes the boiler-plate code for lambda that **NEEDS** to ensure that the 265 | /// original API Gateway call was authenticated with Cognito. 266 | /// 267 | /// If you don't need the [CognitoValues] extracted and included in the [SimpleRequest], see [lambda_main]. 268 | /// 269 | /// It writes the boiler-plate code common to most of (if not all) the Lambda functions of the project 270 | /// The first argument is the functional entry-point that will receive a preprocessed [SimpleRequest] and must 271 | /// return a [SimpleResult]. 272 | /// 273 | /// Following arguments are in the form ` = ` and asks for AWS SDK initialization. 274 | /// - `fct_name` is the function name with which you will retrieve the Client 275 | /// - `SDKClient` is the type of the AWS SDK Client you wish to be returned with the function 276 | /// 277 | /// All in all, this macro allows each lambda code to focus on the actual job it needs to do rather 278 | /// than on writing the same boiler-plate over and over in a project. 279 | /// # Example 280 | /// 281 | /// ``` 282 | /// use lambda_apigw_utils::prelude::*; 283 | /// use serde_json::json; 284 | /// fn echo_process(req: SimpleRequest<'_>) -> SimpleResult { 285 | /// let parameters = req.parameters; 286 | /// let body = req.body; 287 | /// 288 | /// let client = dynamo(); 289 | /// 290 | /// simple_response!(200, json!({"parameters": parameters, "body": body})) 291 | /// } 292 | /// 293 | /// auth_lambda_main!(echo_process, dynamo = aws_sdk_dynamodb::Client); 294 | /// ``` 295 | macro_rules! auth_lambda_main { 296 | ($sync_or_async:ident $rc:ident $(,$fn_name:ident = $sdk:ty)*) => { 297 | $crate::lambda_main_internal!($sync_or_async, $rc, true $(, $fn_name = $sdk)*); 298 | }; 299 | ($rc:ident $(,$fn_name:ident = $sdk:ty)*) => { 300 | $crate::lambda_main_internal!(sync, $rc, true $(, $fn_name = $sdk)*); 301 | }; 302 | } 303 | 304 | pub mod prelude { 305 | pub use super::lambda_commons_utils::log; 306 | pub use super::serde_json::{self, json}; 307 | pub use super::{ 308 | auth_lambda_main, lambda_main, simple_response, SimpleError, SimpleRequest, SimpleResult, 309 | }; 310 | } 311 | -------------------------------------------------------------------------------- /ci-template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: CodePipeline pipeline for a the demo 3 | 4 | Parameters: 5 | ProjectName: 6 | Type: String 7 | Description: Name of the project to insert in all resources names 8 | Default: "SheepShedDemo" 9 | CodeStarConnectionArn: 10 | Type: String 11 | Description: The ARN of the CodeStart connection to GitHub, follow the Get Started on https://github.com/JeremieRodon/demo-rust-lambda.git 12 | ForkedRepoId: 13 | Type: String 14 | Description: The ID of the repository in YOUR GitHub account, probably /demo-rust-lambda 15 | 16 | Resources: 17 | ############# 18 | # Artifacts # 19 | ############# 20 | ArtifactBucket: 21 | Type: AWS::S3::Bucket 22 | Properties: 23 | OwnershipControls: 24 | Rules: 25 | - ObjectOwnership: BucketOwnerEnforced 26 | PublicAccessBlockConfiguration: 27 | BlockPublicAcls: true 28 | BlockPublicPolicy: true 29 | IgnorePublicAcls: true 30 | RestrictPublicBuckets: true 31 | LifecycleConfiguration: 32 | Rules: 33 | - Id: TransitToIT 34 | Status: Enabled 35 | Transitions: 36 | - TransitionInDays: 0 37 | StorageClass: INTELLIGENT_TIERING 38 | ArtifactBucketEmptier: 39 | Type: Custom::BucketEmptier 40 | DependsOn: 41 | - ArtifactBucket 42 | - ArtifactBucketEmptierLambda 43 | - ArtifactBucketEmptierLambdaLogGroup 44 | - ArtifactBucketEmptierLambdaRole 45 | Properties: 46 | ServiceToken: !GetAtt ArtifactBucketEmptierLambda.Arn 47 | BucketName: !Ref ArtifactBucket 48 | ArtifactBucketEmptierLambda: 49 | Type: AWS::Lambda::Function 50 | Properties: 51 | FunctionName: !Sub ${ProjectName}-pipeline-bucket-emptier 52 | Runtime: python3.12 53 | Architectures: 54 | - arm64 55 | MemorySize: 128 56 | Timeout: 15 57 | Role: !GetAtt ArtifactBucketEmptierLambdaRole.Arn 58 | Handler: index.lambda_handler 59 | Code: 60 | ZipFile: | 61 | import boto3 62 | import cfnresponse 63 | def lambda_handler(event, context): 64 | try: 65 | bucket_name = event['ResourceProperties']['BucketName'] 66 | physical_id = f"EMPTIER_{bucket_name}" 67 | if event['RequestType'] == 'Delete': 68 | bucket = boto3.resource('s3').Bucket(bucket_name) 69 | bucket.object_versions.delete() 70 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id) 71 | except Exception as e: 72 | cfnresponse.send(event, context, cfnresponse.FAILED, {'Data': str(e)}, physical_id) 73 | ArtifactBucketEmptierLambdaLogGroup: 74 | Type: AWS::Logs::LogGroup 75 | Properties: 76 | LogGroupName: !Sub /aws/lambda/${ArtifactBucketEmptierLambda} 77 | RetentionInDays: 7 78 | ArtifactBucketEmptierLambdaRole: 79 | Type: AWS::IAM::Role 80 | Properties: 81 | RoleName: !Sub role-lambda-${ProjectName}-pipeline-bucket-emptier 82 | AssumeRolePolicyDocument: 83 | Version: 2012-10-17 84 | Statement: 85 | - Effect: Allow 86 | Principal: 87 | Service: lambda.amazonaws.com 88 | Action: sts:AssumeRole 89 | Policies: 90 | - PolicyName: working-rights 91 | PolicyDocument: 92 | Version: 2012-10-17 93 | Statement: 94 | - Effect: Allow 95 | Action: 96 | - s3:DeleteObject 97 | - s3:DeleteObjectVersion 98 | - s3:ListBucket 99 | - s3:ListBucketVersions 100 | Resource: 101 | - !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket} 102 | - !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket}/* 103 | ArtifactBucketEmptierLambdaRoleBasicPolicy: 104 | Type: AWS::IAM::Policy 105 | Properties: 106 | Roles: 107 | - !Ref ArtifactBucketEmptierLambdaRole 108 | PolicyName: basic-lambda 109 | PolicyDocument: 110 | Version: 2012-10-17 111 | Statement: 112 | - Effect: Allow 113 | Action: 114 | - logs:CreateLogStream 115 | - logs:PutLogEvents 116 | Resource: !GetAtt ArtifactBucketEmptierLambdaLogGroup.Arn 117 | ######### 118 | # Build # 119 | ######### 120 | RustBuildProject: 121 | Type: AWS::CodeBuild::Project 122 | DependsOn: ArtifactBucketEmptier 123 | Properties: 124 | Name: !Sub ${ProjectName}-rust-builder 125 | Description: !Sub Building the Rust API for project ${ProjectName} 126 | ServiceRole: !GetAtt BuildProjectRole.Arn 127 | Environment: 128 | Type: ARM_CONTAINER 129 | ComputeType: BUILD_GENERAL1_MEDIUM 130 | Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 131 | EnvironmentVariables: 132 | - Name: ARTIFACT_BUCKET 133 | Value: !Ref ArtifactBucket 134 | Source: 135 | Type: CODEPIPELINE 136 | BuildSpec: ./ci-config/buildspec-rust.yml 137 | Artifacts: 138 | Type: CODEPIPELINE 139 | Cache: 140 | Type: S3 141 | Location: !Sub ${ArtifactBucket}/codebuildcache/${ProjectName}-rust-builder 142 | TimeoutInMinutes: 60 143 | Tags: 144 | - Key: Name 145 | Value: !Sub ${ProjectName}-rust-builder 146 | RustBuildProjectLogGroup: 147 | Type: AWS::Logs::LogGroup 148 | Properties: 149 | LogGroupName: !Sub /aws/codebuild/${RustBuildProject} 150 | RetentionInDays: 14 151 | 152 | PythonBuildProject: 153 | Type: AWS::CodeBuild::Project 154 | DependsOn: ArtifactBucketEmptier 155 | Properties: 156 | Name: !Sub ${ProjectName}-python-builder 157 | Description: !Sub Building the Python API for project ${ProjectName} 158 | ServiceRole: !GetAtt BuildProjectRole.Arn 159 | Environment: 160 | Type: ARM_CONTAINER 161 | ComputeType: BUILD_GENERAL1_SMALL 162 | Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 163 | EnvironmentVariables: 164 | - Name: ARTIFACT_BUCKET 165 | Value: !Ref ArtifactBucket 166 | Source: 167 | Type: CODEPIPELINE 168 | BuildSpec: ./ci-config/buildspec-python.yml 169 | Artifacts: 170 | Type: CODEPIPELINE 171 | TimeoutInMinutes: 60 172 | Tags: 173 | - Key: Name 174 | Value: !Sub ${ProjectName}-python-builder 175 | PythonBuildProjectLogGroup: 176 | Type: AWS::Logs::LogGroup 177 | Properties: 178 | LogGroupName: !Sub /aws/codebuild/${PythonBuildProject} 179 | RetentionInDays: 14 180 | 181 | BuildProjectRole: 182 | Type: AWS::IAM::Role 183 | Properties: 184 | RoleName: !Sub role-codebuild-${ProjectName}-builder 185 | AssumeRolePolicyDocument: 186 | Version: 2012-10-17 187 | Statement: 188 | - Effect: Allow 189 | Principal: 190 | Service: codebuild.amazonaws.com 191 | Action: sts:AssumeRole 192 | Path: / 193 | BuildProjectRolePolicy: 194 | Type: AWS::IAM::Policy 195 | Properties: 196 | Roles: 197 | - !Ref BuildProjectRole 198 | PolicyName: !Sub policy-codebuild-${ProjectName} 199 | PolicyDocument: 200 | Version: 2012-10-17 201 | Statement: 202 | - Effect: Allow 203 | Action: 204 | - s3:PutObject 205 | - s3:GetObject 206 | - s3:DeleteObject 207 | - s3:ListBucket 208 | Resource: 209 | - !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket} 210 | - !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket}/* 211 | - Effect: Allow 212 | Action: 213 | - logs:CreateLogStream 214 | - logs:PutLogEvents 215 | Resource: 216 | - !GetAtt RustBuildProjectLogGroup.Arn 217 | - !GetAtt PythonBuildProjectLogGroup.Arn 218 | 219 | ############ 220 | # Pipeline # 221 | ############ 222 | PipeLineRole: 223 | Type: AWS::IAM::Role 224 | Properties: 225 | RoleName: !Sub role-codepipeline-${ProjectName} 226 | AssumeRolePolicyDocument: 227 | Version: 2012-10-17 228 | Statement: 229 | - Effect: Allow 230 | Principal: 231 | Service: codepipeline.amazonaws.com 232 | Action: sts:AssumeRole 233 | Policies: 234 | - PolicyName: policy-codepipeline 235 | PolicyDocument: 236 | Version: 2012-10-17 237 | Statement: 238 | - Effect: Allow 239 | Action: codestar-connections:UseConnection 240 | Resource: !Ref CodeStarConnectionArn 241 | - Effect: Allow 242 | Action: 243 | - codepipeline:* 244 | - iam:ListRoles 245 | - codebuild:BatchGetBuilds 246 | - codebuild:StartBuild 247 | - cloudformation:Describe* 248 | - cloudFormation:List* 249 | - cloudformation:CreateStack 250 | - cloudformation:DeleteStack 251 | - cloudformation:DescribeStacks 252 | - cloudformation:UpdateStack 253 | - cloudformation:CreateChangeSet 254 | - cloudformation:DeleteChangeSet 255 | - cloudformation:DescribeChangeSet 256 | - cloudformation:ExecuteChangeSet 257 | - cloudformation:SetStackPolicy 258 | - cloudformation:ValidateTemplate 259 | Resource: "*" 260 | - Effect: Allow 261 | Action: 262 | - s3:PutObject 263 | - s3:GetObject 264 | - s3:ListBucket 265 | Resource: 266 | - !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket} 267 | - !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket}/* 268 | - Effect: Allow 269 | Action: iam:PassRole 270 | Resource: "*" 271 | Condition: 272 | StringEqualsIfExists: 273 | "iam:PassedToService": 274 | - cloudformation.amazonaws.com 275 | Path: / 276 | Pipeline: 277 | Type: AWS::CodePipeline::Pipeline 278 | DependsOn: 279 | - ArtifactBucketEmptier 280 | - BuildProjectRolePolicy 281 | Properties: 282 | RoleArn: !GetAtt PipeLineRole.Arn 283 | Name: !Sub cp-${ProjectName}-release 284 | PipelineType: V2 285 | Stages: 286 | - Name: Source 287 | Actions: 288 | - Name: Checkout 289 | ActionTypeId: 290 | Category: Source 291 | Owner: AWS 292 | Version: 1 293 | Provider: CodeStarSourceConnection 294 | Configuration: 295 | ConnectionArn: !Ref CodeStarConnectionArn 296 | FullRepositoryId: !Ref ForkedRepoId 297 | BranchName: master 298 | DetectChanges: true 299 | OutputArtifacts: 300 | - Name: Sources 301 | RunOrder: 1 302 | - Name: BuildInfra 303 | Actions: 304 | - Name: BuildInfraRust 305 | ActionTypeId: 306 | Category: Build 307 | Owner: AWS 308 | Version: 1 309 | Provider: CodeBuild 310 | Configuration: 311 | ProjectName: !Ref RustBuildProject 312 | RunOrder: 1 313 | InputArtifacts: 314 | - Name: Sources 315 | OutputArtifacts: 316 | - Name: RustTemplate 317 | - Name: BuildInfraPython 318 | ActionTypeId: 319 | Category: Build 320 | Owner: AWS 321 | Version: 1 322 | Provider: CodeBuild 323 | Configuration: 324 | ProjectName: !Ref PythonBuildProject 325 | RunOrder: 1 326 | InputArtifacts: 327 | - Name: Sources 328 | OutputArtifacts: 329 | - Name: PythonTemplate 330 | - Name: DeployInfra 331 | Actions: 332 | - Name: DeployInfraRust 333 | ActionTypeId: 334 | Category: Deploy 335 | Owner: AWS 336 | Version: 1 337 | Provider: CloudFormation 338 | Configuration: 339 | ChangeSetName: Deploy 340 | ActionMode: CREATE_UPDATE 341 | StackName: !Sub ${ProjectName}-rust-api 342 | Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND 343 | TemplatePath: RustTemplate::demo-template.yml 344 | ParameterOverrides: !Sub '{"ProjectName": "${ProjectName}", "Lang": "rust"}' 345 | RoleArn: !GetAtt CFDeployerRole.Arn 346 | InputArtifacts: 347 | - Name: RustTemplate 348 | RunOrder: 1 349 | - Name: DeployInfraPython 350 | ActionTypeId: 351 | Category: Deploy 352 | Owner: AWS 353 | Version: 1 354 | Provider: CloudFormation 355 | Configuration: 356 | ChangeSetName: Deploy 357 | ActionMode: CREATE_UPDATE 358 | StackName: !Sub ${ProjectName}-python-api 359 | Capabilities: CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND 360 | TemplatePath: PythonTemplate::demo-template.yml 361 | ParameterOverrides: !Sub '{"ProjectName": "${ProjectName}", "Lang": "python"}' 362 | RoleArn: !GetAtt CFDeployerRole.Arn 363 | InputArtifacts: 364 | - Name: PythonTemplate 365 | RunOrder: 1 366 | ArtifactStore: 367 | Type: S3 368 | Location: !Ref ArtifactBucket 369 | 370 | ################################## 371 | # CloudFormation deployment role # 372 | ################################## 373 | CFDeployerRole: 374 | Type: AWS::IAM::Role 375 | Properties: 376 | RoleName: !Sub role-${ProjectName}-CF-Deployer 377 | AssumeRolePolicyDocument: 378 | Version: 2012-10-17 379 | Statement: 380 | - Effect: Allow 381 | Principal: 382 | Service: cloudformation.amazonaws.com 383 | Action: sts:AssumeRole 384 | Policies: 385 | - PolicyName: !Sub policy-${ProjectName}-CF-Deployer 386 | PolicyDocument: 387 | Version: 2012-10-17 388 | Statement: 389 | - Sid: ReadArtifacts 390 | Effect: Allow 391 | Action: s3:GetObject 392 | Resource: !Sub arn:${AWS::Partition}:s3:::${ArtifactBucket}/* 393 | - Effect: Allow 394 | Action: cloudformation:CreateChangeSet 395 | Resource: "*" 396 | - Effect: Allow 397 | Action: 398 | - cloudformation:DescribeStacks 399 | - cloudformation:CreateStack 400 | - cloudformation:UpdateStack 401 | - cloudformation:RollbackStack 402 | - cloudformation:DeleteStack 403 | Resource: 404 | - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${ProjectName}-python-api 405 | - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${ProjectName}-rust-api 406 | - Effect: Allow 407 | Action: 408 | - lambda:* 409 | - iam:*Role* 410 | - logs:* 411 | - dynamodb:* 412 | - apigateway:* 413 | Resource: "*" 414 | Path: / 415 | -------------------------------------------------------------------------------- /rust/libs/dynamodb_sheep_shed/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | use aws_sdk_dynamodb::{ 4 | error::ProvideErrorMetadata, 5 | operation::{delete_item::DeleteItemError, put_item::PutItemError}, 6 | types::{ReturnValue, Select}, 7 | Client, 8 | }; 9 | use serde_dynamo::{aws_sdk_dynamodb_1::from_item, to_attribute_value, to_item}; 10 | use sheep_shed::{Sheep, SheepShed, Tattoo}; 11 | 12 | /// A [SheepShed] that rely on a DynamoDB database 13 | /// # Important note 14 | /// It is expected that it is always use in the context of 15 | /// a [tokio::runtime::Runtime] of the multi_thread kind as this will heavily 16 | /// rely on calling [tokio::runtime::Handle::current]. 17 | /// 18 | /// Also, the calls to the method of the [SheepShed] trait MUST always 19 | /// be called with [tokio::task::spawn_blocking]. 20 | /// 21 | /// Note that it is of course MUCH easier to just stay in the async context, 22 | /// and for this pet project we could easily keep it all "async" if the 23 | /// SheepShed trait was async. That being said, in real life Traits from 24 | /// external crates are often defined as "sync" so making this 25 | /// async -> sync -> async bridge is unfortunately difficult to avoid at 26 | /// some point (and async Trait are only like a month old anyway). 27 | #[derive(Debug)] 28 | pub struct DynamoDBSheepShed { 29 | client: Client, 30 | table_name: String, 31 | } 32 | 33 | impl DynamoDBSheepShed { 34 | /// Creates a new [DynamoDBSheepShed] from a [Client] and a `table_name` 35 | /// # Panics 36 | /// Panics if called outside of a [tokio] context. 37 | pub fn new(client: Client) -> Self { 38 | let table_name = std::env::var("BACKEND_TABLE_NAME") 39 | .expect("Mandatory environment variable `BACKEND_TABLE_NAME` is not set"); 40 | log::info!("BACKEND_TABLE_NAME={table_name}"); 41 | DynamoDBSheepShed::local_new(client, table_name) 42 | } 43 | 44 | fn local_new(client: Client, table_name: String) -> Self { 45 | Self { client, table_name } 46 | } 47 | 48 | async fn _full_table_scan( 49 | &self, 50 | count_only: bool, 51 | ) -> Result<(usize, Option>), sheep_shed::errors::Error> { 52 | log::info!("_full_table_scan(count_only={count_only})"); 53 | // Request the approximate item count that DynamoDB updates sometimes 54 | let approx_table_size = self 55 | .client 56 | .describe_table() 57 | .table_name(self.table_name.as_str()) 58 | .send() 59 | .await 60 | .map_err(|e| { 61 | let dte = e.into_service_error(); 62 | let err_string = format!("{dte} ({:?}: {:?})", dte.code(), dte.message()); 63 | log::error!("{err_string}"); 64 | sheep_shed::errors::Error::GenericError(err_string) 65 | })? 66 | .table 67 | .expect("table must exist") 68 | .item_count 69 | .unwrap_or_default(); 70 | 71 | log::info!("approx_table_size={approx_table_size}"); 72 | 73 | // The database representation of a Sheep is ~50 bytes 74 | // Therefore, a scan page of 1MB will contain ~20 000 sheeps 75 | // We will be very conservative and devide the table into 100k sheeps segments because 76 | // it would be very counter-productive to have more parallel scans than we would have had pages. 77 | // Bottom line, with 100k items per segment we expect each segment to be fully scanned in 5 requests. 78 | let parallel_scan_threads = min(1 + approx_table_size / 100_000, 1_000_000) as i32; 79 | 80 | let handle = tokio::runtime::Handle::current(); 81 | let futures = (0..parallel_scan_threads) 82 | .into_iter() 83 | .map(|seg| { 84 | let client = self.client.clone(); 85 | let table_name = self.table_name.clone(); 86 | handle.spawn(async move { 87 | let mut items = if !count_only { Some(vec![]) } else { None }; 88 | let mut count = 0; 89 | let mut exclusive_start_key = None; 90 | loop { 91 | let result = client 92 | .scan() 93 | .table_name(&table_name) 94 | .segment(seg) 95 | .total_segments(parallel_scan_threads) 96 | .set_exclusive_start_key(exclusive_start_key) 97 | .select(if count_only { 98 | Select::Count 99 | } else { 100 | Select::AllAttributes 101 | }) 102 | .send() 103 | .await 104 | .map_err(|e| { 105 | let se = e.into_service_error(); 106 | let err_string = 107 | format!("{se} ({:?}: {:?})", se.code(), se.message()); 108 | log::error!("{err_string}"); 109 | sheep_shed::errors::Error::GenericError(err_string) 110 | })?; 111 | exclusive_start_key = result.last_evaluated_key; 112 | count += result.count as usize; 113 | if !count_only { 114 | items.as_mut().unwrap().extend( 115 | result.items.unwrap_or_default().into_iter().map(|i| { 116 | from_item(i).expect("cannot fail unless database corrupt") 117 | }), 118 | ); 119 | } 120 | if exclusive_start_key.is_none() { 121 | break; 122 | } 123 | } 124 | Ok((count, items)) 125 | }) 126 | }) 127 | .collect::>(); 128 | 129 | log::info!("Launched {} green-threads", futures.len()); 130 | 131 | let mut items = if !count_only { Some(vec![]) } else { None }; 132 | let mut count = 0; 133 | for f in futures { 134 | let (sub_count, sub_vec) = f.await.unwrap()?; 135 | count += sub_count; 136 | if !count_only { 137 | items.as_mut().unwrap().extend(sub_vec.unwrap().into_iter()); 138 | } 139 | } 140 | log::info!( 141 | "_full_table_scan => Ok(({count}, {}))", 142 | if items.is_some() { "Some" } else { "None" } 143 | ); 144 | Ok((count, items)) 145 | } 146 | 147 | async fn _sheep_iter_impl( 148 | &self, 149 | ) -> Result, sheep_shed::errors::Error> { 150 | Ok(self._full_table_scan(false).await?.1.unwrap().into_iter()) 151 | } 152 | async fn _sheep_count_impl(&self) -> Result { 153 | Ok(self._full_table_scan(true).await?.0) 154 | } 155 | async fn _add_sheep_impl(&self, sheep: Sheep) -> Result<(), sheep_shed::errors::Error> { 156 | log::info!("_add_sheep_impl(sheep={sheep})"); 157 | let _ = self 158 | .client 159 | .put_item() 160 | .table_name(self.table_name.as_str()) 161 | .set_item(Some(to_item(&sheep).expect("cannot fail"))) 162 | .condition_expression("attribute_not_exists(tattoo)") 163 | .send() 164 | .await 165 | .map_err(|e| { 166 | let pie = e.into_service_error(); 167 | match pie { 168 | PutItemError::ConditionalCheckFailedException(_) => { 169 | sheep_shed::errors::Error::SheepDuplicationError(sheep.tattoo) 170 | } 171 | _ => { 172 | let err_string = format!("{pie} ({:?}: {:?})", pie.code(), pie.message()); 173 | log::error!("{err_string}"); 174 | sheep_shed::errors::Error::GenericError(err_string) 175 | } 176 | } 177 | })?; 178 | log::info!("_add_sheep_impl => Ok(())"); 179 | Ok(()) 180 | } 181 | async fn _kill_sheep_impl(&self, tattoo: &Tattoo) -> Result { 182 | log::info!("_kill_sheep_impl(tattoo={tattoo})"); 183 | let sheep = self 184 | .client 185 | .delete_item() 186 | .table_name(self.table_name.as_str()) 187 | .key("tattoo", to_attribute_value(tattoo).expect("cannot fail")) 188 | .condition_expression("attribute_exists(tattoo)") 189 | .return_values(ReturnValue::AllOld) 190 | .send() 191 | .await 192 | .map_err(|e| { 193 | let die = e.into_service_error(); 194 | match die { 195 | DeleteItemError::ConditionalCheckFailedException(_) => { 196 | sheep_shed::errors::Error::SheepNotPresent(tattoo.clone()) 197 | } 198 | _ => { 199 | let err_string = format!("{die} ({:?}: {:?})", die.code(), die.message()); 200 | log::error!("{err_string}"); 201 | sheep_shed::errors::Error::GenericError(err_string) 202 | } 203 | } 204 | })? 205 | .attributes 206 | .map(|i| from_item(i).expect("cannot fail unless database corrupt")) 207 | .expect("DynamoDB verified Sheep was present"); 208 | log::info!("_kill_sheep_impl => Ok({sheep})"); 209 | Ok(sheep) 210 | } 211 | } 212 | 213 | impl SheepShed for DynamoDBSheepShed { 214 | /// Add a new [Sheep] in the [SheepShed] 215 | /// # Errors 216 | /// It is not allowed to add a duplicated [Sheep], will return an 217 | /// [errors::Error::SheepDuplicationError] if the user tries to add 218 | /// a [Sheep] with an already known [Tattoo] 219 | /// # Panics 220 | /// Panics if called outside of a blocking thread ([tokio::task::spawn_blocking]). 221 | fn add_sheep(&mut self, sheep: Sheep) -> Result<(), sheep_shed::errors::Error> { 222 | tokio::runtime::Handle::current().block_on(self._add_sheep_impl(sheep)) 223 | } 224 | 225 | /// Return the number of [Sheep] in the [SheepShed] 226 | /// # Panics 227 | /// Panics if called outside of a blocking thread ([tokio::task::spawn_blocking]). 228 | fn sheep_count(&self) -> Result { 229 | tokio::runtime::Handle::current().block_on(self._sheep_count_impl()) 230 | } 231 | /// Return an [Iterator] over references of all the [Sheep]s in the [SheepShed] 232 | /// # Panics 233 | /// Panics if called outside of a blocking thread ([tokio::task::spawn_blocking]). 234 | fn sheep_iter(&self) -> Result, sheep_shed::errors::Error> { 235 | tokio::runtime::Handle::current().block_on(self._sheep_iter_impl()) 236 | } 237 | 238 | fn kill_sheep( 239 | &mut self, 240 | tattoo: &sheep_shed::Tattoo, 241 | ) -> Result { 242 | tokio::runtime::Handle::current().block_on(self._kill_sheep_impl(tattoo)) 243 | } 244 | } 245 | 246 | // The test module need to have DynamoDB local running 247 | // You can, for example, use the Java version from AWS: 248 | // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html 249 | // I'm using Correto to launch it, here is the command-line (assuming you are in the directory containing the .jar file and the lib folder): 250 | // java -D"java.library.path=./DynamoDBLocal_lib" -jar DynamoDBLocal.jar -sharedDb -inMemory 251 | #[cfg(test)] 252 | mod tests { 253 | 254 | use aws_sdk_dynamodb::types::{ 255 | AttributeDefinition, BillingMode, KeySchemaElement, KeyType, ScalarAttributeType, 256 | }; 257 | 258 | use super::*; 259 | 260 | fn dynamodb_local_client() -> Client { 261 | let config = aws_sdk_dynamodb::Config::builder() 262 | .endpoint_url("http://localhost:8000") 263 | .behavior_version_latest() 264 | .credentials_provider(aws_sdk_dynamodb::config::Credentials::new( 265 | "fakeMyKeyId", 266 | "fakeSecretAccessKey", 267 | None, 268 | None, 269 | "Static", 270 | )) 271 | .region(Some(aws_sdk_dynamodb::config::Region::from_static( 272 | "eu-west-1", 273 | ))) 274 | .build(); 275 | Client::from_conf(config) 276 | } 277 | 278 | struct TempTable { 279 | client: Client, 280 | table_name: String, 281 | } 282 | 283 | impl TempTable { 284 | fn new(client: Client, table_name: &str) -> Self { 285 | let pkn = "tattoo"; 286 | let pkad = AttributeDefinition::builder() 287 | .attribute_name(pkn) 288 | .attribute_type(ScalarAttributeType::N) 289 | .build() 290 | .unwrap(); 291 | let pk = KeySchemaElement::builder() 292 | .attribute_name(pkn) 293 | .key_type(KeyType::Hash) 294 | .build() 295 | .unwrap(); 296 | tokio::runtime::Handle::current() 297 | .block_on( 298 | client 299 | .create_table() 300 | .table_name(table_name) 301 | .attribute_definitions(pkad) 302 | .key_schema(pk) 303 | .billing_mode(BillingMode::PayPerRequest) 304 | .send(), 305 | ) 306 | .unwrap(); 307 | 308 | Self { 309 | client, 310 | table_name: table_name.to_owned(), 311 | } 312 | } 313 | } 314 | 315 | impl Drop for TempTable { 316 | fn drop(&mut self) { 317 | tokio::runtime::Handle::current() 318 | .block_on( 319 | self.client 320 | .delete_table() 321 | .table_name(&self.table_name) 322 | .send(), 323 | ) 324 | .ok() 325 | .or_else(|| None); 326 | } 327 | } 328 | 329 | fn prep_base_sheep_shed() -> (TempTable, DynamoDBSheepShed) { 330 | let client = dynamodb_local_client(); 331 | let table_name = format!("{}", rand::random::()); 332 | let temp_table = TempTable::new(client.clone(), &table_name); 333 | let sheep_shed = DynamoDBSheepShed::local_new(client, table_name); 334 | (temp_table, sheep_shed) 335 | } 336 | 337 | // The DynamoDBSheepShed expects to be called from an async context 338 | // In Lambda that make sense because they are launch as async. 339 | // So because of the async -> sync -> async expectation of the library, 340 | // we simulate that in our tests by launching them "as-if" it was a Lambda 341 | // context. 342 | // Note that it is of course MUCH easier to just stay in the async context, 343 | // and for this pet project we could easily keep it all "async" if the 344 | // SheepShed trait was async. That being said, in real life Traits from 345 | // external crates are often defined as "sync" so making this 346 | // async -> sync -> async bridge is unfortunately difficult to avoid at 347 | // some point (and async Trait are only like a month old anyway). 348 | macro_rules! impl_test_template { 349 | ($tn: tt) => { 350 | #[test] 351 | fn $tn() { 352 | let rt = tokio::runtime::Builder::new_multi_thread() 353 | .enable_all() 354 | .build() 355 | .unwrap(); 356 | rt.block_on(async { 357 | rt.spawn_blocking(|| { 358 | let (_temp, sheep_shed) = prep_base_sheep_shed(); 359 | sheep_shed::test_templates::$tn(sheep_shed) 360 | }) 361 | .await 362 | .unwrap() 363 | }) 364 | } 365 | }; 366 | } 367 | 368 | impl_test_template!(cannot_duplicate_sheep); 369 | impl_test_template!(sheep_shed_sheep_count); 370 | impl_test_template!(sheep_shed_iterator); 371 | impl_test_template!(cannot_kill_inexistent_sheep); 372 | } 373 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 10 | 11 | 12 | 19 | [![Contributors][contributors-shield]][contributors-url] 20 | [![Forks][forks-shield]][forks-url] 21 | [![Stargazers][stars-shield]][stars-url] 22 | [![Issues][issues-shield]][issues-url] 23 | [![MIT License][license-shield]][license-url] 24 | 25 | 26 |
27 |
28 | 29 | Logo 30 | 31 | 32 |

Demo Rust on Lambda

33 | 34 |

35 | A demonstration of how minimal an effort it takes to use Rust instead of Python for Serverless projects
such as an API Gateway with Lambda functions. 36 |

37 |
38 | 39 | 40 |
41 | Table of Contents 42 |
    43 |
  1. About The Project
  2. 44 |
  3. 45 | Getting Started 46 | 52 |
  4. 53 |
  5. 54 | Usage 55 | 59 |
  6. 60 |
  7. License
  8. 61 |
  9. Contact
  10. 62 |
63 |
64 | 65 | 66 | ## About The Project 67 | 68 | ### Functional requirements 69 | 70 | As an example, we are creating a backend-API that models a ***Sheep Shed***. 71 | 72 | The ***Sheep Shed*** is housing... well... **Sheeps**. Each ***Sheep*** has a ***Tattoo*** which is unique: it is a functionnal error to have sheeps sharing the same tattoo. A ***Sheep*** also have a ***Weight***, which is important down the road. 73 | 74 | The ***Sheep Shed*** obviously has a **Dog** that can, when asked, count the sheeps in the shed. 75 | 76 | The ***Sheep Shed*** unfortunately has a hungry **Wolf** lurking around, who wants to eat the sheeps. This wolf is quite strange, he suffers from Obsessive-Compulsive Disorder. 77 | Even starving, he can only eat a ***Sheep*** if its ***Weight*** expressed in micrograms is a prime number. And of course, if multiple sheeps comply with his OCD he wants the heaviest one! 78 | 79 | Finally, the ***Sheep Shed*** has a resident **Cat**. The cat does not care about the sheeps, shed, wolf or dog. He is interested only in its own business. This is a savant cat that 80 | recently took interest in a [2-ary variant of the Ackermann function](https://en.wikipedia.org/wiki/Ackermann_function#TRS,_based_on_2-ary_function). The only way to currently get his 81 | attention is to ask him about it. 82 | 83 | ### AWS design 84 | 85 | The ***Sheep Shed*** is accessible through an **Amazon API Gateway** exposing 4 paths: 86 | 87 | - POST /sheeps/`` to add a sheep in the shed with the given `Tattoo` and a random weight generated by the API 88 | - GET /dog to retrieve the current sheep count 89 | - GET /cat?m=``&n=`` to ask the cat to compute the Ackermann function for given `m` and `n` 90 | - DELETE /wolf to trigger a raid on the shed by our OCD wolf. 91 | 92 | Each of these paths has its own **AWS Lambda** function. 93 | 94 | The backend is an **Amazon DynamoDB** table. 95 | 96 | ### But... Why? 97 | 98 | Ok that's just a demo but the crux of it is: 99 | 100 | - The **Dog** performs a task that only require to scan DynamoDB with virtually no operationnal overhead other than driving the scan pagination 101 | - The **sheep insertion** performs a random number generation, but is also almost entirely tied to a single DynamoDB interaction (PutItem) 102 | - The **Wolf** require to not only scan the entire DynamoDB table, but also to compute the prime numbers to be able to efficiently test if the weight of each sheep is itself a prime number 103 | then, if a suitable sheep is found, he eats (DeleteItem) it 104 | - The **Cat** performs a purely algorithmic task with no I/O required. 105 | 106 | As a result, we can compare the size of the advantage of Rust over Python in these various situations. 107 | 108 | *NB1: The DynamoDB table layout is intentionaly bad: it would be possible to create indexes to drastically accelerate the search of a suitable sheep for the wolf, but that's not the subject of 109 | this demonstration* 110 | 111 | *NB2: Initially I thought that activities tied to DynamoDB (Network I/O) operations would greatly reduce the advantage of Rust over Python (because 112 | packets don't go faster between Lambda and DynamoDB depending on the language used). But it turns out that even for "pure" IO bound activities Rust 113 | lambdas are crushing Python lambdas...* 114 | 115 |

(back to top)

116 | 117 | 118 | ## Getting Started 119 | 120 | You can easily deploy the demo in you own AWS account in less than 15 minutes. The cost of deploying and loadtesting will 121 | be less than $1: CodePipeline/CodeBuild will stay well within their Free-tier; API Gateway, Lambda and DynamoDB 122 | are all in pay-per-request mode at an aggregated rate of ~$5/million req and you will make a few tens of thousands 123 | of request (it will cost you pennies). 124 | 125 | Here is an overview of what will be deployed: 126 |
127 | Architecture 128 |

This PNG can be edited using Draw.io

129 |
130 | 131 | ### Prerequisites 132 | 133 | You need **an existing AWS account**, with permissions to use the following services: 134 | 135 | - AWS CodeStar Connections 136 | - AWS CloudFormation 137 | - AWS CodePipeline 138 | - Amazon Simple Storage Service (S3) 139 | - AWS CodeBuild 140 | - AWS Lambda 141 | - Amazon API Gateway 142 | - Amazon DynamoDB 143 | - AWS IAM (roles will be created for Lambda, CodeBuild, CodePipeline and CloudFormation) 144 | - Amazon CloudWatch Logs 145 | 146 | You also need **a GitHub account**, as the deployment method I propose here rely on you being able to fork this repository (CodePipeline only accepts source GitHub repositories that you own for obvious security reasons). 147 | 148 | ### Preparation 149 | 150 | #### 1. Fork the repo 151 | 152 | Fork this repository in you own GitHub account. Copy the ID of the new repository (\/demo-rust-lambda), you will need it later. Be mindfull of the case. 153 | 154 | The simplest technique is to copy it from the browser URL: 155 | 156 | ![Step 0](images/get-started-0.png) 157 | 158 | #### Important 159 | 160 | In the following instructions, there is an *implicit* instruction to **always ensure your AWS Console 161 | is set on the AWS Region you intend to use**. You can use any region you like, just stick to it. 162 | 163 | #### 2. Create a CodeStar connection to your GitHub account 164 | 165 | This step is only necessary if you don't already have a CodeStar Connection to your GitHub account. If you do, you can reuse it: just retrieve its ARN and keep it on hand. 166 | 167 | 1. Go to the CodePipeline console, select Settings > Connections, use the GitHub provider, choose any name you like, click Connect to GitHub 168 | 169 | ![Step 1](images/get-started-1.png) 170 | 171 | 2. Assuming you were already logged-in on GitHub, it will ask you if you consent to let AWS do stuff in your GitHub account. Yes you do. 172 | 173 | ![Step 2](images/get-started-2.png) 174 | 175 | 3. You will be brought back to the AWS Console. Choose the GitHub Apps that was created for you in the list (don't mind the number on the screenshot, yours will be different), then click Connect. 176 | 177 | ![Step 3](images/get-started-3.png) 178 | 179 | 4. The connection is now created, copy its ARN somewhere, you will need it later. 180 | 181 | ![Step 4](images/get-started-4.png) 182 | 183 | ### Deployment 184 | 185 | Now you are ready to deploy, download the CloudFormation template [ci-template.yml](https://github.com/JeremieRodon/demo-rust-lambda/blob/master/ci-template.yml) 186 | from the link or from your newly forked repository if you prefer. 187 | 188 | 5. Go to the CloudFormation console and create a new stack. 189 | 190 | ![Step 5](images/get-started-5.png) 191 | 192 | 6. Ensure *Template is ready* is selected and *Upload a template file*, then specify the `ci-template.yml` template that you just downloaded. 193 | 194 | ![Step 6](images/get-started-6.png) 195 | 196 | 7. Choose any Stack name you like, set your CodeStar Connection Arn (previously copied) in `CodeStarConnectionArn` and your forked repository ID in `ForkedRepoId` 197 | 198 | ![Step 7](images/get-started-7.png) 199 | 200 | 8. Skip the *Configure stack options*, leaving everything unchanged 201 | 202 | 9. At the *Review and create* stage, acknowledge that CloudFormation will create roles and Submit. 203 | 204 | ![Step 8](images/get-started-8.png) 205 | 206 | At this point, everything will roll on its own, the full deployment should take ~8 minutes, largely due to the quite long first compilation of Rust lambdas. 207 | 208 | If you whish to follow what is happening, keep the CloudFormation tab open in your browser and open another one on the CodePipeline console. 209 | 210 | ### Cleanup 211 | 212 | To cleanup the demo resources, you need to remove the CloudFormation stacks **IN ORDER**: 213 | 214 | - **First** remove the two API stacks named `-rust-api` and `-python-api` 215 | - **/!\\ Wait until both are successfully removed /!\\** 216 | - **Then** remove the CICD stack (the one you created yourself) 217 | 218 | You **MUST** follow that order of operation because the CICD stack owns the IAM Role used by the other two to performs their operation; 219 | therefore destroying the CICD stack first will prevent the API stacks from operating. 220 | 221 | Removing the CloudFormation stacks correctly will cleanup every resources created for this demo, no further cleanup is needed. 222 | 223 |

(back to top)

224 | 225 | 226 | ## Usage 227 | 228 | ### Generating traffic on the APIs 229 | 230 | The `utils` folder of the repository contains scripts to generate traffic on each API. The easiest way is to use `execute_default_benches.sh`: 231 | 232 | ```sh 233 | cd utils 234 | ./execute_default_benches.sh --rust-api --python-api 235 | ``` 236 | 237 | *You can find the URL of each API (`RUST_API_URL` and `PYTHON_API_URL` in the script above) in the Outputs sections of the respective CloudFormation stacks (stacks of the APIs, not the CICD) or directly in the API Gateway console.* 238 | 239 | It will execute a bunch of API calls (~4k/API) and typically takes ~10minutes to run, depending on your internet connection and latence to the APIs. 240 | 241 | For reference, here is an execution report with my APIs deployed in the Paris region (as I live there...): 242 | 243 | ```sh 244 | ./execute_default_benches.sh \ 245 | --rust-api https://swmafop2bd.execute-api.eu-west-3.amazonaws.com/v1/ \ 246 | --python-api https://pv6wlmzjo0.execute-api.eu-west-3.amazonaws.com/v1/ 247 | ``` 248 | 249 | It outputs: 250 | 251 | ```sh 252 | Launching test... 253 | ############################################################### 254 | # This will take a while and appear to hang, but don't worry! # 255 | ############################################################### 256 | PYTHON CAT: ./invoke_cat.sh https://pv6wlmzjo0.execute-api.eu-west-3.amazonaws.com/v1/ 257 | Calls took 72555ms 258 | RUST CAT: ./invoke_cat.sh https://swmafop2bd.execute-api.eu-west-3.amazonaws.com/v1/ 259 | Calls took 21036ms 260 | PYTHON SHEEPS: ./insert_sheeps.sh https://pv6wlmzjo0.execute-api.eu-west-3.amazonaws.com/v1/ 261 | Insertion took 23430ms 262 | RUST SHEEPS: ./insert_sheeps.sh https://swmafop2bd.execute-api.eu-west-3.amazonaws.com/v1/ 263 | Insertion took 23429ms 264 | PYTHON DOG: ./invoke_dog.sh https://pv6wlmzjo0.execute-api.eu-west-3.amazonaws.com/v1/ 265 | Calls took 26990ms 266 | RUST DOG: ./invoke_dog.sh https://swmafop2bd.execute-api.eu-west-3.amazonaws.com/v1/ 267 | Calls took 23385ms 268 | PYTHON WOLF: ./invoke_wolf.sh https://pv6wlmzjo0.execute-api.eu-west-3.amazonaws.com/v1/ 269 | Calls took 194190ms 270 | RUST WOLF: ./invoke_wolf.sh https://swmafop2bd.execute-api.eu-west-3.amazonaws.com/v1/ 271 | Calls took 27279ms 272 | Done. 273 | ``` 274 | 275 | Of course, you can also play with the individual scripts of the `utils` folder, just invoke them with `--help` to see what you can do with them: 276 | 277 | ```sh 278 | ./invoke_cat.sh --help 279 | ``` 280 | 281 | ```sh 282 | Usage: ./invoke_cat.sh [] 283 | 284 | Repeatedly call GET /cat?m=&n= with m=3 and n=8 unless overritten 285 | 286 | -p|--parallel The number of concurrent task to use. (Default: 100) 287 | -c|--call-count The number of call to make. (Default: 1000) 288 | -m The 'm' number for the Ackermann algorithm. (Default: 3) 289 | -n The 'n' number for the Ackermann algorithm. (Default: 8) 290 | 291 | OPTIONS: 292 | -h|--help Show this help 293 | ``` 294 | 295 | ### Exploring the results with CloudWatch Log Insights 296 | 297 | After you generated load, you can compare the performance of the lambdas using CloudWatch Log Insights. 298 | 299 | Go to the CloudWatch Log Insights console, set the date/time range appropriately, select the 8 log groups of our Lambdas (4 Rust, 4 Python) and set the query: 300 | 301 | ![Log-Insights](images/log-insights.png) 302 | 303 | Here is the query: 304 | 305 | ```text 306 | filter @type = "REPORT" 307 | | fields greatest(@initDuration, 0) + @duration as duration, ispresent(@initDuration) as coldStart 308 | | parse @log /^\d+:.*?-(?(rust|python)-.+)$/ 309 | | stats count(*) as count, 310 | avg(duration) as avgDuration, min(duration) as minDuration, max(duration) as maxDuration, stddev(duration) as StdDevDuration, 311 | avg(@billedDuration) as avgBilled, min(@billedDuration) as minBilled, max(@billedDuration) as maxBilled, stddev(@billedDuration) as StdDevBilled, 312 | avg(@maxMemoryUsed / 1024 / 1024) as avgRam, min(@maxMemoryUsed / 1024 / 1024) as minRam, max(@maxMemoryUsed / 1024 / 1024) as maxRam, stddev(@maxMemoryUsed / 1024 / 1024) as StdDevRam 313 | by Lambda, coldStart 314 | ``` 315 | 316 | This query gives you the average, min, max and standard deviation for 3 metrics: duration, billed duration and memory used. Result are grouped by lambda function and separated between coldstart and non-coldstart runs. 317 | 318 | And here are the results yielded by my tests (Duration: ms, Billed: ms, Ram: MB; StdDev removed for bievety): 319 | 320 | --- 321 | 322 | | Lambda |-| coldStart | count |-| avgDuration | minDuration | maxDuration |-| avgBilled | minBilled | maxBilled |-| avgRam | minRam | maxRam | 323 | | --- |-| --- | --- |-| --- | --- | --- |-| --- | --- | --- |-| --- | --- | --- | 324 | | rust-delete-wolf-ocd |-| no | 1256 |-| 95.1301 | 42.8 | 389.74 |-| 95.6274 | 43 | 390 |-| 25.2633 | 22.8882 | 26.7029 | 325 | | rust-delete-wolf-ocd |-| yes | 54 |-| 350.7987 | 328.48 | 371.89 |-| 351.2778 | 329 | 372 |-| 21.9698 | 21.9345 | 22.8882 | 326 | | python-delete-wolf-ocd |-| no | 2289 |-| 4271.3851 | 2243.41 | 7006.29 |-| 4271.8873 | 2244 | 7007 |-| 88.5117 | 83.9233 | 92.5064 | 327 | | python-delete-wolf-ocd |-| yes | 102 |-| 9261.8528 | 7984.55 | 9601.49 |-| 8976.1667 | 7700 | 9298 |-| 80.8192 | 80.1086 | 81.0623 | 328 | | rust-get-dog-count |-| no | 988 |-| 17.4312 | 13.05 | 39.5 |-| 17.9362 | 14 | 40 |-| 23.6073 | 21.9345 | 23.8419 | 329 | | rust-get-dog-count |-| yes | 12 |-| 193.2633 | 182.49 | 218.09 |-| 193.9167 | 183 | 219 |-| 21.5371 | 20.9808 | 21.9345 | 330 | | python-get-dog-count |-| no | 900 |-| 669.4292 | 603.35 | 845.58 |-| 669.9178 | 604 | 846 |-| 76.8926 | 76.2939 | 78.2013 | 331 | | python-get-dog-count |-| yes | 100 |-| 3003.9024 | 2796.17 | 3204.47 |-| 2713.36 | 2522 | 2914 |-| 76.2939 | 76.2939 | 76.2939 | 332 | | rust-post-sheep-random |-| no | 989 |-| 8.1096 | 4.67 | 76.32 |-| 8.5875 | 5 | 77 |-| 22.8351 | 20.9808 | 23.8419 | 333 | | rust-post-sheep-random |-| yes | 11 |-| 149.8936 | 140.53 | 163.69 |-| 150.3636 | 141 | 164 |-| 21.0675 | 20.9808 | 21.9345 | 334 | | python-post-sheep-random |-| no | 900 |-| 599.0018 | 549.25 | 693.93 |-| 599.4967 | 550 | 694 |-| 76.7962 | 76.2939 | 78.2013 | 335 | | python-post-sheep-random |-| yes | 100 |-| 2958.1016 | 2823.31 | 3338.99 |-| 2657.59 | 2544 | 2839 |-| 76.2749 | 75.3403 | 77.2476 | 336 | | rust-get-cat-ackermann |-| no | 960 |-| 124.1233 | 90.98 | 174.78 |-| 124.6073 | 91 | 175 |-| 15.8966 | 14.3051 | 16.2125 | 337 | | rust-get-cat-ackermann |-| yes | 40 |-| 150.1008 | 130.29 | 164.68 |-| 150.55 | 131 | 165 |-| 14.3051 | 14.3051 | 14.3051 | 338 | | python-get-cat-ackermann |-| no | 898 |-| 5935.7388 | 5883.13 | 6093.28 |-| 5936.2327 | 5884 | 6094 |-| 29.9335 | 29.5639 | 30.5176 | 339 | | python-get-cat-ackermann |-| yes | 102 |-| 6034.4553 | 5985.93 | 6158.94 |-| 5948.3824 | 5906 | 6071 |-| 29.9379 | 29.5639 | 30.5176 | 340 | 341 | --- 342 | 343 | Kind of speaks for itself, right? Rust is on average **50x faster**, **33x cheaper** and **4x** more memory efficient! 344 | 345 | ***NB**: Note that Rust is 50x faster but **only** 33x cheaper because when using Rust (or any custom runtime) on Lambda the Cold Start is billed by AWS, whereas with Python (and other native Lambda runtimes) the Cold Start is generaly not billed (for now)* 346 | 347 |

(back to top)

348 | 349 | ### Visualizing the results with GSheets 350 | 351 | You can duplicate and use the GSheet document I used to produce the charts of my talk on the subject: 352 | 353 | 354 | 355 | If you export the results given by CloudWatch Log Insights as CSV, you can directly paste them in the first Sheet (named `All`). Just **make sure that the `Lambda` and `coldStart` columns are exactly in the same order** as what I had, because the other sheets for the Sheeps, Dogs, etc... are hard-linked to the cells of the first one (there is no "search" going on). 356 | 357 |

(back to top)

358 | 359 | 360 | ## License 361 | 362 | Distributed under the MIT License. See `LICENSE.txt` for more information. 363 | 364 |

(back to top)

365 | 366 | 367 | ## Contact 368 | 369 | Jérémie RODON - 370 | 371 | [![X][twitter-x-shield]][twitter-x-url] 372 | 373 | [![LinkedIn][linkedin-shield]][linkedin-url] 374 | 375 | Project Link: [https://github.com/JeremieRodon/demo-rust-lambda](https://github.com/JeremieRodon/demo-rust-lambda) 376 | 377 |

(back to top)

378 | 379 | 380 | 381 | [contributors-shield]: https://img.shields.io/github/contributors/JeremieRodon/demo-rust-lambda.svg?style=for-the-badge 382 | [contributors-url]: https://github.com/JeremieRodon/demo-rust-lambda/graphs/contributors 383 | [forks-shield]: https://img.shields.io/github/forks/JeremieRodon/demo-rust-lambda.svg?style=for-the-badge 384 | [forks-url]: https://github.com/JeremieRodon/demo-rust-lambda/network/members 385 | [stars-shield]: https://img.shields.io/github/stars/JeremieRodon/demo-rust-lambda.svg?style=for-the-badge 386 | [stars-url]: https://github.com/JeremieRodon/demo-rust-lambda/stargazers 387 | [issues-shield]: https://img.shields.io/github/issues/JeremieRodon/demo-rust-lambda.svg?style=for-the-badge 388 | [issues-url]: https://github.com/JeremieRodon/demo-rust-lambda/issues 389 | [license-shield]: https://img.shields.io/github/license/JeremieRodon/demo-rust-lambda.svg?style=for-the-badge 390 | [license-url]: https://github.com/JeremieRodon/demo-rust-lambda/blob/master/LICENSE.txt 391 | [linkedin-shield]: https://img.shields.io/badge/linkedin-0077B5?style=for-the-badge&logo=linkedin&logoColor=white 392 | [linkedin-url]: https://linkedin.com/in/JeremieRodon 393 | [twitter-x-shield]: https://img.shields.io/badge/Twitter/X-000000?style=for-the-badge&logo=x&logoColor=white 394 | [twitter-x-url]: https://twitter.com/JeremieRodon 395 | -------------------------------------------------------------------------------- /demo-template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: The demo SheepShed API in the chosen language 4 | 5 | Parameters: 6 | ProjectName: 7 | Type: String 8 | Lang: 9 | Type: String 10 | Description: The language of deployment 11 | AllowedValues: 12 | - "rust" 13 | - "python" 14 | 15 | Conditions: 16 | cIsRust: !Equals [!Ref Lang, "rust"] 17 | cIsPython: !Equals [!Ref Lang, "python"] 18 | 19 | Globals: 20 | Function: 21 | Runtime: !If 22 | - cIsRust 23 | - provided.al2023 24 | - python3.12 25 | MemorySize: !If 26 | - cIsRust 27 | - 128 28 | - 128 29 | Timeout: 30 30 | Handler: !If 31 | - cIsRust 32 | - rust.handler 33 | - index.lambda_handler 34 | Architectures: 35 | - arm64 36 | Layers: !If 37 | - cIsPython 38 | - [!Ref CommonsLambdaLayer] 39 | - !Ref AWS::NoValue 40 | Environment: 41 | Variables: 42 | BACKEND_TABLE_NAME: !Ref BackendTable 43 | ALLOW_ORIGIN: "*" 44 | RUST_LOG: debug,hyper=info,tracing=info,aws_config=info,aws_smithy_runtime=info,aws_smithy_runtime_api=info,rustls=info 45 | 46 | Resources: 47 | ################# 48 | # Backend table # 49 | ################# 50 | BackendTable: 51 | Type: AWS::DynamoDB::Table 52 | Properties: 53 | TableName: !Sub ${ProjectName}-${Lang}-backend 54 | BillingMode: PAY_PER_REQUEST 55 | AttributeDefinitions: 56 | - AttributeName: tattoo 57 | AttributeType: N 58 | KeySchema: 59 | - AttributeName: tattoo 60 | KeyType: HASH 61 | 62 | ############################## 63 | # Shared Python Lambda Layer # 64 | ############################## 65 | CommonsLambdaLayer: 66 | Type: AWS::Lambda::LayerVersion 67 | Condition: cIsPython 68 | Properties: 69 | CompatibleRuntimes: 70 | - python3.12 71 | Content: python/layers/commons 72 | Description: Lambda layer containing various utils for interacting with ApiGateway and DynamoDB 73 | LayerName: !Sub ${ProjectName}-${Lang}-commons 74 | 75 | ########### 76 | # Lambdas # 77 | ########### 78 | #################### 79 | # GET /cat?m=2&n=3 # 80 | #################### 81 | GetCatAckermannFunction: 82 | Type: AWS::Serverless::Function 83 | Properties: 84 | FunctionName: !Sub ${ProjectName}-${Lang}-get-cat-ackermann 85 | CodeUri: lambdas/get-cat-ackermann 86 | Events: 87 | SheepShedAPI: 88 | Type: Api 89 | Properties: 90 | RestApiId: !Ref SheepShedAPI 91 | Path: /cat 92 | Method: get 93 | GetCatAckermannFunctionLogGroup: 94 | Type: AWS::Logs::LogGroup 95 | Properties: 96 | LogGroupName: !Sub /aws/lambda/${GetCatAckermannFunction} 97 | RetentionInDays: 90 98 | 99 | ############ 100 | # GET /dog # 101 | ############ 102 | GetDogCountFunction: 103 | Type: AWS::Serverless::Function 104 | Properties: 105 | FunctionName: !Sub ${ProjectName}-${Lang}-get-dog-count 106 | CodeUri: lambdas/get-dog-count 107 | Events: 108 | SheepShedAPI: 109 | Type: Api 110 | Properties: 111 | RestApiId: !Ref SheepShedAPI 112 | Path: /dog 113 | Method: get 114 | Policies: 115 | - Version: 2012-10-17 116 | Statement: 117 | - Sid: DescribeShed 118 | Effect: Allow 119 | Action: dynamodb:DescribeTable 120 | Resource: !GetAtt BackendTable.Arn 121 | - Sid: CountSheeps 122 | Effect: Allow 123 | Action: dynamodb:Scan 124 | Resource: !GetAtt BackendTable.Arn 125 | Condition: 126 | StringEquals: 127 | "dynamodb:Select": COUNT 128 | GetDogCountFunctionLogGroup: 129 | Type: AWS::Logs::LogGroup 130 | Properties: 131 | LogGroupName: !Sub /aws/lambda/${GetDogCountFunction} 132 | RetentionInDays: 90 133 | 134 | ################ 135 | # DELETE /wolf # 136 | ################ 137 | DeleteWolfOcdFunction: 138 | Type: AWS::Serverless::Function 139 | Properties: 140 | FunctionName: !Sub ${ProjectName}-${Lang}-delete-wolf-ocd 141 | CodeUri: lambdas/delete-wolf-ocd 142 | Events: 143 | SheepShedAPI: 144 | Type: Api 145 | Properties: 146 | RestApiId: !Ref SheepShedAPI 147 | Path: /wolf 148 | Method: delete 149 | Policies: 150 | - Version: 2012-10-17 151 | Statement: 152 | - Sid: DescribeShed 153 | Effect: Allow 154 | Action: dynamodb:DescribeTable 155 | Resource: !GetAtt BackendTable.Arn 156 | - Sid: ListSheeps 157 | Effect: Allow 158 | Action: dynamodb:Scan 159 | Resource: !GetAtt BackendTable.Arn 160 | Condition: 161 | StringEquals: 162 | "dynamodb:Select": ALL_ATTRIBUTES 163 | - Sid: DevourSheep 164 | Effect: Allow 165 | Action: dynamodb:DeleteItem 166 | Resource: !GetAtt BackendTable.Arn 167 | Condition: 168 | StringEquals: 169 | "dynamodb:ReturnValues": ALL_OLD 170 | DeleteWolfOcdFunctionLogGroup: 171 | Type: AWS::Logs::LogGroup 172 | Properties: 173 | LogGroupName: !Sub /aws/lambda/${DeleteWolfOcdFunction} 174 | RetentionInDays: 90 175 | 176 | ####################### 177 | # POST /sheep/{Tattoo} # 178 | ####################### 179 | PostSheepRandomFunction: 180 | Type: AWS::Serverless::Function 181 | Properties: 182 | FunctionName: !Sub ${ProjectName}-${Lang}-post-sheep-random 183 | CodeUri: lambdas/post-sheep-random 184 | Events: 185 | SheepShedAPI: 186 | Type: Api 187 | Properties: 188 | RestApiId: !Ref SheepShedAPI 189 | Path: /sheep/{Tattoo} 190 | Method: post 191 | Policies: 192 | - Version: 2012-10-17 193 | Statement: 194 | - Sid: AddSheep 195 | Effect: Allow 196 | Action: dynamodb:PutItem 197 | Resource: !GetAtt BackendTable.Arn 198 | Condition: 199 | ForAllValues:StringEquals: 200 | "dynamodb:Attributes": 201 | - tattoo 202 | - weight 203 | StringEquals: 204 | "dynamodb:ReturnValues": NONE 205 | PostSheepRandomFunctionLogGroup: 206 | Type: AWS::Logs::LogGroup 207 | Properties: 208 | LogGroupName: !Sub /aws/lambda/${PostSheepRandomFunction} 209 | RetentionInDays: 90 210 | 211 | ####### 212 | # API # 213 | ####### 214 | SheepShedAPI: 215 | Type: AWS::Serverless::Api 216 | Properties: 217 | Name: !Sub api-${Lang}-${ProjectName} 218 | Description: !Sub Backend REST API for the SheepShed with ${Lang} lambdas 219 | EndpointConfiguration: 220 | Type: REGIONAL 221 | MergeDefinitions: false 222 | OpenApiVersion: 3.0.1 223 | FailOnWarnings: true 224 | DefinitionBody: 225 | openapi: "3.0.1" 226 | info: 227 | title: Sheep shed API 228 | description: A sheep shed 229 | version: "1" 230 | paths: 231 | /cat: 232 | options: 233 | tags: 234 | - options 235 | description: Preflight CORS checks for the PATH 236 | responses: 237 | "200": 238 | description: "200 response" 239 | headers: 240 | Access-Control-Allow-Origin: 241 | $ref: "#/components/headers/Access-Control-Allow-Origin" 242 | Access-Control-Allow-Methods: 243 | $ref: "#/components/headers/Access-Control-Allow-Methods" 244 | Access-Control-Allow-Headers: 245 | $ref: "#/components/headers/Access-Control-Allow-Headers" 246 | x-amazon-apigateway-integration: 247 | type: mock 248 | requestTemplates: 249 | application/json: '{"statusCode" : 200}' 250 | responses: 251 | default: 252 | statusCode: "200" 253 | responseParameters: 254 | method.response.header.Access-Control-Allow-Origin: "'*'" 255 | method.response.header.Access-Control-Allow-Methods: "'OPTIONS,GET'" 256 | method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 257 | responseTemplates: 258 | application/json: "{}" 259 | get: 260 | description: >- 261 | Ask the cat to compute the Ackermann algorithm for some value of m and n 262 | parameters: 263 | - $ref: "#/components/parameters/AckermannNumberN" 264 | - $ref: "#/components/parameters/AckermannNumberM" 265 | responses: 266 | "200": 267 | $ref: "#/components/responses/AckermannResult" 268 | "400": 269 | $ref: "#/components/responses/GenericError" 270 | x-amazon-apigateway-integration: 271 | type: aws_proxy 272 | httpMethod: POST 273 | uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetCatAckermannFunction.Arn}/invocations 274 | passthroughBehavior: when_no_match 275 | x-amazon-apigateway-request-validator: basic 276 | /dog: 277 | options: 278 | tags: 279 | - options 280 | description: Preflight CORS checks for the PATH 281 | responses: 282 | "200": 283 | description: "200 response" 284 | headers: 285 | Access-Control-Allow-Origin: 286 | $ref: "#/components/headers/Access-Control-Allow-Origin" 287 | Access-Control-Allow-Methods: 288 | $ref: "#/components/headers/Access-Control-Allow-Methods" 289 | Access-Control-Allow-Headers: 290 | $ref: "#/components/headers/Access-Control-Allow-Headers" 291 | x-amazon-apigateway-integration: 292 | type: mock 293 | requestTemplates: 294 | application/json: '{"statusCode" : 200}' 295 | responses: 296 | default: 297 | statusCode: "200" 298 | responseParameters: 299 | method.response.header.Access-Control-Allow-Origin: "'*'" 300 | method.response.header.Access-Control-Allow-Methods: "'OPTIONS,GET'" 301 | method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 302 | responseTemplates: 303 | application/json: "{}" 304 | get: 305 | description: Ask the dog to count the sheeps in the shed 306 | responses: 307 | "200": 308 | $ref: "#/components/responses/SheepCount" 309 | x-amazon-apigateway-integration: 310 | type: aws_proxy 311 | httpMethod: POST 312 | uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetDogCountFunction.Arn}/invocations 313 | passthroughBehavior: when_no_match 314 | x-amazon-apigateway-request-validator: basic 315 | /sheep/{Tattoo}: 316 | options: 317 | tags: 318 | - options 319 | description: Preflight CORS checks for the PATH 320 | parameters: 321 | - $ref: "#/components/parameters/Tattoo" 322 | responses: 323 | "200": 324 | description: "200 response" 325 | headers: 326 | Access-Control-Allow-Origin: 327 | $ref: "#/components/headers/Access-Control-Allow-Origin" 328 | Access-Control-Allow-Methods: 329 | $ref: "#/components/headers/Access-Control-Allow-Methods" 330 | Access-Control-Allow-Headers: 331 | $ref: "#/components/headers/Access-Control-Allow-Headers" 332 | x-amazon-apigateway-integration: 333 | type: mock 334 | requestTemplates: 335 | application/json: '{"statusCode" : 200}' 336 | responses: 337 | default: 338 | statusCode: "200" 339 | responseParameters: 340 | method.response.header.Access-Control-Allow-Origin: "'*'" 341 | method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'" 342 | method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 343 | responseTemplates: 344 | application/json: "{}" 345 | post: 346 | description: Generate a new sheep for the shed with the given Tattoo and a random Weight 347 | parameters: 348 | - $ref: "#/components/parameters/Tattoo" 349 | responses: 350 | "200": 351 | $ref: "#/components/responses/Sheep" 352 | "400": 353 | $ref: "#/components/responses/GenericError" 354 | x-amazon-apigateway-integration: 355 | type: aws_proxy 356 | httpMethod: POST 357 | uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostSheepRandomFunction.Arn}/invocations 358 | passthroughBehavior: when_no_match 359 | x-amazon-apigateway-request-validator: basic 360 | /wolf/: 361 | options: 362 | tags: 363 | - options 364 | description: Preflight CORS checks for the PATH 365 | responses: 366 | "200": 367 | description: "200 response" 368 | headers: 369 | Access-Control-Allow-Origin: 370 | $ref: "#/components/headers/Access-Control-Allow-Origin" 371 | Access-Control-Allow-Methods: 372 | $ref: "#/components/headers/Access-Control-Allow-Methods" 373 | Access-Control-Allow-Headers: 374 | $ref: "#/components/headers/Access-Control-Allow-Headers" 375 | x-amazon-apigateway-integration: 376 | type: mock 377 | requestTemplates: 378 | application/json: '{"statusCode" : 200}' 379 | responses: 380 | default: 381 | statusCode: "200" 382 | responseParameters: 383 | method.response.header.Access-Control-Allow-Origin: "'*'" 384 | method.response.header.Access-Control-Allow-Methods: "'OPTIONS,DELETE'" 385 | method.response.header.Access-Control-Allow-Headers: "'Content-Type'" 386 | responseTemplates: 387 | application/json: "{}" 388 | delete: 389 | description: >- 390 | The hungry wolf will eat a sheep from the shed, but only if it finds one 391 | with a weight that satisfy its Obsessive-Compulsive Disorder (OCD) 392 | responses: 393 | "204": 394 | $ref: "#/components/responses/Empty" 395 | "404": 396 | $ref: "#/components/responses/GenericError" 397 | x-amazon-apigateway-integration: 398 | type: aws_proxy 399 | httpMethod: POST 400 | uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DeleteWolfOcdFunction.Arn}/invocations 401 | passthroughBehavior: when_no_match 402 | x-amazon-apigateway-request-validator: basic 403 | components: 404 | ################################################################################ 405 | # Headers # 406 | ################################################################################ 407 | headers: 408 | Access-Control-Allow-Headers: 409 | schema: 410 | type: string 411 | Access-Control-Allow-Methods: 412 | schema: 413 | type: string 414 | Access-Control-Allow-Origin: 415 | schema: 416 | type: string 417 | ################################################################################ 418 | # Schemas # 419 | ################################################################################ 420 | schemas: 421 | ackermannresult: 422 | type: object 423 | description: Ackermann algorithm result 424 | required: 425 | - result 426 | properties: 427 | result: 428 | type: integer 429 | format: int64 430 | minimum: 0 431 | sheep: 432 | type: object 433 | description: A sheep 434 | required: 435 | - tattoo 436 | - weight 437 | properties: 438 | tattoo: 439 | type: integer 440 | format: int64 441 | minimum: 0 442 | weight: 443 | type: integer 444 | format: int64 445 | minimum: 80000000000 446 | maximum: 160000000000 447 | description: The weight of the sheep, expressed in micrograms 448 | sheepcount: 449 | type: object 450 | description: The sheep count in the shed 451 | required: 452 | - count 453 | properties: 454 | count: 455 | type: integer 456 | format: int64 457 | minimum: 0 458 | ################################################################################ 459 | # Parameters # 460 | ################################################################################ 461 | parameters: 462 | Tattoo: 463 | name: Tattoo 464 | description: The tattoo of a sheep 465 | in: path 466 | required: true 467 | schema: 468 | type: string 469 | pattern: ^\d{1,20}$ 470 | AckermannNumberN: 471 | name: "n" 472 | description: >- 473 | The number 'n' for the Ackermann algorithm. We use the 2-ary function as defined on 474 | Wikipedia: https://en.wikipedia.org/wiki/Ackermann_function#TRS,_based_on_2-ary_function 475 | in: query 476 | required: true 477 | schema: 478 | type: integer 479 | format: int32 480 | minimum: 0 481 | maximum: 50000 482 | AckermannNumberM: 483 | name: "m" 484 | description: >- 485 | The number 'm' for the Ackermann algorithm. We use the 2-ary function as defined on 486 | Wikipedia: https://en.wikipedia.org/wiki/Ackermann_function#TRS,_based_on_2-ary_function 487 | in: query 488 | required: true 489 | schema: 490 | type: integer 491 | format: int32 492 | minimum: 0 493 | maximum: 4 494 | ################################################################################ 495 | # Request bodies # 496 | ################################################################################ 497 | requestBodies: {} 498 | ################################################################################ 499 | # Responses objects # 500 | ################################################################################ 501 | responses: 502 | AckermannResult: 503 | description: Ackermann algorithm result 504 | headers: 505 | Access-Control-Allow-Origin: 506 | $ref: "#/components/headers/Access-Control-Allow-Origin" 507 | content: 508 | application/json: 509 | schema: 510 | $ref: "#/components/schemas/ackermannresult" 511 | Sheep: 512 | description: A sheep 513 | headers: 514 | Access-Control-Allow-Origin: 515 | $ref: "#/components/headers/Access-Control-Allow-Origin" 516 | content: 517 | application/json: 518 | schema: 519 | $ref: "#/components/schemas/sheep" 520 | SheepCount: 521 | description: The number of sheeps currently in the shed 522 | headers: 523 | Access-Control-Allow-Origin: 524 | $ref: "#/components/headers/Access-Control-Allow-Origin" 525 | content: 526 | application/json: 527 | schema: 528 | $ref: "#/components/schemas/sheepcount" 529 | GenericError: 530 | description: The generic error 531 | headers: 532 | Access-Control-Allow-Origin: 533 | $ref: "#/components/headers/Access-Control-Allow-Origin" 534 | content: 535 | application/json: 536 | schema: 537 | type: object 538 | description: The standard error object for this API 539 | required: 540 | - message 541 | properties: 542 | message: 543 | type: string 544 | description: The message giving details about the error 545 | Empty: 546 | description: An empty response 547 | headers: 548 | Access-Control-Allow-Origin: 549 | $ref: "#/components/headers/Access-Control-Allow-Origin" 550 | ################################################################################ 551 | # Security Definitions # 552 | ################################################################################ 553 | securitySchemes: {} 554 | ################################################################################ 555 | # Security # 556 | ################################################################################ 557 | security: [] 558 | ################################################################################ 559 | # Tags # 560 | ################################################################################ 561 | tags: 562 | - name: options 563 | description: All the options API methods 564 | ################################################################################ 565 | # Validators # 566 | ################################################################################ 567 | x-amazon-apigateway-request-validators: 568 | basic: 569 | validateRequestBody: true 570 | validateRequestParameters: true 571 | ################################################################################ 572 | # Custom API Gateway responses # 573 | ################################################################################ 574 | x-amazon-apigateway-gateway-responses: 575 | DEFAULT_4XX: 576 | responseParameters: 577 | gatewayresponse.header.Access-Control-Allow-Origin: "'*'" 578 | responseTemplates: 579 | application/json: '{"message":$context.error.messageString}' 580 | DEFAULT_5XX: 581 | responseParameters: 582 | gatewayresponse.header.Access-Control-Allow-Origin: "'*'" 583 | responseTemplates: 584 | application/json: '{"message":$context.error.messageString}' 585 | ################################################################################ 586 | # Documentation # 587 | ################################################################################ 588 | x-amazon-apigateway-documentation: 589 | version: v1.0 590 | createdDate: "2024-03-05T08:28:00Z" 591 | documentationParts: 592 | - location: 593 | type: API 594 | properties: 595 | info: 596 | description: Sheep shed API 597 | StageName: v1 598 | MethodSettings: 599 | - ResourcePath: "/*" 600 | HttpMethod: "*" 601 | ThrottlingBurstLimit: 100000 602 | ThrottlingRateLimit: 10000 603 | 604 | Outputs: 605 | ApiUrl: 606 | Description: The URL of the API 607 | Value: !Sub https://${SheepShedAPI}.execute-api.${AWS::Region}.amazonaws.com/v1/ 608 | --------------------------------------------------------------------------------