Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM swift:6.2.0

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends make git
44 changes: 44 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker-compose
{
"name": "Docker from Docker Compose",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

// Use this environment variable if you need to bind mount your local source code into a new container.
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
},

"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/aws-cli:1": {}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"lldb.library": "/usr/lib/liblldb.so"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"swiftlang.swift-vscode"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "docker --version",

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "vscode"
}
36 changes: 36 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: '3'

services:
app:
build:
context: .
dockerfile: Dockerfile

volumes:
# Forwards the local Docker socket to the container.
- /var/run/docker.sock:/var/run/docker-host.sock
# Update this to wherever you want VS Code to mount the folder of your project
- ../..:/workspaces:cached

# Overrides default command so things don't shut down after the process ends.
entrypoint: /usr/local/share/docker-init.sh
depends_on:
- localstack
environment:
- LOCALSTACK_ENDPOINT=http://localstack:4566
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_REGION=us-east-1
command: sleep infinity

# Uncomment the next four lines if you will use a ptrace-based debuggers like C++, Go, and Rust.
cap_add:
- SYS_PTRACE
security_opt:
- seccomp:unconfined

# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

localstack:
image: localstack/localstack
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot

version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly
3 changes: 3 additions & 0 deletions .sourcekit-lsp/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$schema": "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/release/6.1/config.schema.json"
}
1 change: 1 addition & 0 deletions .swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6.2.0
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ localstack:
docker run -it --rm -p "4566:4566" localstack/localstack

local_setup_dynamo_db:
aws --endpoint-url=http://localhost:4566 dynamodb create-table \
aws --endpoint-url=http://localstack:4566 dynamodb create-table \
--table-name Breeze \
--attribute-definitions AttributeName=itemKey,AttributeType=S \
--key-schema AttributeName=itemKey,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
--billing-mode PAY_PER_REQUEST \
--region us-east-1

local_invoke_demo_app:
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/awslabs/swift-aws-lambda-runtime", from: "2.2.0"),
.package(url: "https://github.com/awslabs/swift-aws-lambda-runtime", from: "2.5.0"),
.package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "0.5.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
.package(url: "https://github.com/soto-project/soto.git", from: "7.0.0"),
Expand Down
66 changes: 43 additions & 23 deletions Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,38 @@ import Logging
/// Defines the interface for a Breeze DynamoDB service.
///
/// Provides methods to access the database manager and to gracefully shutdown the service.
public protocol BreezeDynamoDBServing: Actor {
func dbManager() async -> BreezeDynamoDBManaging
func gracefulShutdown() throws
public protocol BreezeDynamoDBServing: Service {
var dbManager: BreezeDynamoDBManaging { get }
func onGracefulShutdown() async throws
func syncShutdown() throws
}

/// Provides methods to access the DynamoDB database manager and to gracefully shutdown the service.
public actor BreezeDynamoDBService: BreezeDynamoDBServing {
public struct BreezeDynamoDBService: BreezeDynamoDBServing {

private let dbManager: BreezeDynamoDBManaging
public let dbManager: BreezeDynamoDBManaging
private let logger: Logger
private let awsClient: AWSClient
private let httpClient: HTTPClient
private var isShutdown = false

private let shutdownState: ShutdownState

/// Error types for BreezeDynamoDBService
enum BreezeDynamoDBServiceError: Error {
case alreadyShutdown
}

/// Actor to manage shutdown state safely
private actor ShutdownState {
private var isShutdown = false

func markShutdown() throws {
guard !isShutdown else {
throw BreezeDynamoDBServiceError.alreadyShutdown
}
isShutdown = true
}
}

/// Initializes the BreezeDynamoDBService with the provided configuration.
/// - Parameters:
/// - config: The configuration for the DynamoDB service.
Expand All @@ -46,11 +64,14 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing {
httpConfig: BreezeHTTPClientConfig,
logger: Logger,
DBManagingType: BreezeDynamoDBManaging.Type = BreezeDynamoDBManager.self
) async {
) {
logger.info("Init DynamoDBService with config...")
logger.info("region: \(config.region)")
logger.info("tableName: \(config.tableName)")
logger.info("keyName: \(config.keyName)")
if config.endpoint != nil {
logger.info("endpoint: \(config.endpoint!)")
}
self.logger = logger

let timeout = HTTPClient.Configuration.Timeout(
Expand All @@ -73,37 +94,36 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing {
tableName: config.tableName,
keyName: config.keyName
)
self.shutdownState = ShutdownState()
logger.info("DBManager is ready.")
}

/// Returns the BreezeDynamoDBManaging instance.
public func dbManager() async -> BreezeDynamoDBManaging {
logger.info("Starting DynamoDBService...")
return self.dbManager
public func run() async throws {
try await gracefulShutdown()
try await onGracefulShutdown()
Comment thread
Andrea-Scuderi marked this conversation as resolved.
}

/// Gracefully shutdown the service and its components.
///
/// - Throws: An error if the shutdown process fails.
/// This method ensures that the AWS client and HTTP client are properly shutdown before marking the service as shutdown.
/// It also logs the shutdown process.
/// This method is idempotent;
/// - Important: This method must be called at leat once to ensure that resources are released properly. If the method is not called, it will lead to a crash.
public func gracefulShutdown() throws {
guard !isShutdown else { return }
isShutdown = true
/// This method is idempotent and will throw if called multiple times to prevent double shutdown.
/// - Important: This method must be called at least once to ensure that resources are released properly. If the method is not called, it will lead to a crash.
public func onGracefulShutdown() async throws {
try await shutdownState.markShutdown()
logger.info("Stopping DynamoDBService...")
try awsClient.syncShutdown()
try await awsClient.shutdown()
logger.info("DynamoDBService is stopped.")
logger.info("Stopping HTTPClient...")
try httpClient.syncShutdown()
try await httpClient.shutdown()
logger.info("HTTPClient is stopped.")
}

deinit {
guard !isShutdown else { return }
try? awsClient.syncShutdown()
try? httpClient.syncShutdown()
/// Sync shutdown
public func syncShutdown() throws {
try awsClient.syncShutdown()
try httpClient.syncShutdown()
}
}

4 changes: 2 additions & 2 deletions Sources/BreezeLambdaAPI/BreezeAPIConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ public struct BreezeAPIConfiguration: APIConfiguring {
/// This method is used to retrieve the name of the primary key in the DynamoDB table that will be used by the Breeze Lambda API.
/// - Important: The key name is essential for identifying items in the DynamoDB table.
func keyName() throws -> String {
guard let tableName = Lambda.env("DYNAMO_DB_KEY") else {
guard let keyName = Lambda.env("DYNAMO_DB_KEY") else {
throw BreezeLambdaAPIError.keyNameNotFound
}
return tableName
return keyName
}

/// Returns the endpoint for the Breeze Lambda API.
Expand Down
20 changes: 9 additions & 11 deletions Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ import AWSLambdaRuntime
public actor BreezeLambdaAPI<T: BreezeCodable>: Service {

let logger = Logger(label: "service-group-breeze-lambda-api")
let timeout: TimeAmount
private let serviceGroup: ServiceGroup
private let apiConfig: any APIConfiguring
private let dynamoDBService: BreezeDynamoDBService

/// Initializes the BreezeLambdaAPI with the provided API configuration.
/// - Parameter apiConfig: An object conforming to `APIConfiguring` that provides the necessary configuration for the Breeze API.
Expand All @@ -56,26 +56,24 @@ public actor BreezeLambdaAPI<T: BreezeCodable>: Service {
public init(apiConfig: APIConfiguring = BreezeAPIConfiguration()) async throws {
do {
self.apiConfig = apiConfig
self.timeout = .seconds(apiConfig.dbTimeout)
let config = try apiConfig.getConfig()
let httpConfig = BreezeHTTPClientConfig(
timeout: .seconds(60),
timeout: .seconds(apiConfig.dbTimeout),
logger: logger
)
let operation = try apiConfig.operation()
let dynamoDBService = await BreezeDynamoDBService(
self.dynamoDBService = BreezeDynamoDBService(
config: config,
httpConfig: httpConfig,
logger: logger
)
let breezeLambdaService = BreezeLambdaService<T>(
dynamoDBService: dynamoDBService,
operation: operation,
logger: logger
)
let dbManager = dynamoDBService.dbManager
let breezeApi = BreezeLambdaHandler<T>(dbManager: dbManager, operation: operation)
let runtime = LambdaRuntime(body: breezeApi.handle)
self.serviceGroup = ServiceGroup(
services: [breezeLambdaService],
gracefulShutdownSignals: [.sigterm, .sigint],
services: [runtime, dynamoDBService],
gracefulShutdownSignals: [.sigint],
cancellationSignals: [.sigterm],
logger: logger
)
} catch {
Expand Down
Loading