Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd260f76fe | |||
| e6336d8dc3 | |||
| f28da1e09e | |||
| 2869ac6433 | |||
| 295cd506ae |
@@ -1,6 +1,3 @@
|
||||
> **Warning**
|
||||
> The guides are now part of Swift.org and will continue to be evolved there. This repo is now archived
|
||||
|
||||
# Swift on Server Development Guide
|
||||
|
||||
## Introduction
|
||||
@@ -9,7 +6,6 @@ This guide is designed to help teams and individuals running Swift Server applic
|
||||
It focuses on how to build, test, deploy and debug such application and provides tips in those areas.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Setup and code editing](docs/setup-and-ide-alternatives.md)
|
||||
- [Building](docs/building.md)
|
||||
- [Testing](docs/testing.md)
|
||||
@@ -18,14 +14,4 @@ It focuses on how to build, test, deploy and debug such application and provides
|
||||
- [Debugging multithreading issues and memory checks](docs/llvm-sanitizers.md)
|
||||
- [Deployment](docs/deployment.md)
|
||||
|
||||
### Server-side library development
|
||||
|
||||
Server-side libraries should, to the best of their ability, play well with established patterns in the SSWG library ecosystem.
|
||||
They should also utilize the core observability libraries, such as: logging, metrics and distributed tracing, where applicable.
|
||||
|
||||
The below guidelines are aimed to help library developers with some of the typical questions and challenges they might face when designing server-side focused libraries with Swift:
|
||||
|
||||
- [SwiftLog: Log level guidelines](docs/libs/log-levels.md)
|
||||
- [Swift Concurrency Adoption Guidelines](docs/concurrency-adoption-guidelines.md)
|
||||
|
||||
_The guide is a community effort, and all are invited to share their tips and know-how. Please provide a PR if you have ideas for improving this guide!_
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
# Allocations
|
||||
|
||||
For high-performance software in Swift, it's often important to understand where your heap allocations are coming from. The next step can then be to reduce the number of allocations your software makes.
|
||||
|
||||
This is very similar to other performance questions: Before you can optimise performance you need to understand where you spend your resources. And resources can be CPU time, as well as memory, or heap allocations.
|
||||
In this document we will solely focus on the number of heap allocations, not their size.
|
||||
|
||||
On macOS, you can use Instruments's "Allocations" instrument. The Allocations instrument shows you two sets of values: The live allocations (i.e. allocated and not freed) as well as the transient allocations (all allocations made).
|
||||
|
||||
Your production workloads however will likely run on Linux and depending on your setup the number of allocations can differ significantly between macOS and Linux.
|
||||
|
||||
## Preparation
|
||||
|
||||
To not waste your time, be sure to do any profiling in _release mode_. Swift's optimiser will produce significantly faster code which will also allocate less in release mode. Usually this means you need to run
|
||||
|
||||
swift run -c release
|
||||
|
||||
#### Install `perf`
|
||||
|
||||
Follow the [installation instructions](linux-perf.md) in the Linux `perf` utility guide.
|
||||
|
||||
#### Clone the `FlameGraph` project
|
||||
|
||||
To see some pretty graphs, clone the [`FlameGraph`](https://github.com/brendangregg/FlameGraph) repository on the machine/container where you need it. The rest of this guide will assume that it's available at `/FlameGraph`:
|
||||
|
||||
```
|
||||
git clone https://github.com/brendangregg/FlameGraph
|
||||
```
|
||||
|
||||
Tip: With Docker, you may want to bind mount the `FlameGraph` repository into the container using
|
||||
|
||||
```
|
||||
docker run -it --rm \
|
||||
--privileged \
|
||||
-v "/path/to/FlameGraphOnYourMachine:/FlameGraph:ro" \
|
||||
-v "$PWD:PWD" -w "$PWD" \
|
||||
swift:latest
|
||||
```
|
||||
|
||||
or similar.
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
In this guide, we will be using the [Linux `perf`](https://perf.wiki.kernel.org/index.php/Main_Page) tool. If you're struggling to get `perf` to work, have a look at our [information regarding `perf`](linux-perf.md). If you're running in a Docker container, don't forget that you'll need a privileged container. And generally, you will need `root` access, so you may need to prefix the commands with `sudo`.
|
||||
|
||||
## Getting a `perf` user probe
|
||||
|
||||
In this guide, we will be counting the number of allocations. Most allocations from a Swift program (on Linux) will be done through the `malloc` function.
|
||||
|
||||
To get information about when an allocation function is called, we will install a `perf` "user probes" on the allocation functions. Because Swift also uses other allocation functions such as `calloc` and `posix_memalign`, we'll install a user probe for them all. From then on, there will be an event in `perf` that will fire whenever one of the allocation functions is called.
|
||||
|
||||
```bash
|
||||
# figures out the path to libc
|
||||
libc_path=$(readlink -e /lib64/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6)
|
||||
|
||||
# delete all existing user probes on libc (instead of * you can also list them individually)
|
||||
perf probe --del 'probe_libc:*'
|
||||
|
||||
# installs a probe on `malloc`, `calloc`, and `posix_memalign`
|
||||
perf probe -x "$libc_path" --add malloc --add calloc --add posix_memalign
|
||||
```
|
||||
|
||||
The result (hopefully) looks somewhat like this:
|
||||
|
||||
```
|
||||
Added new events:
|
||||
probe_libc:malloc (on malloc in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
|
||||
probe_libc:calloc (on calloc in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
|
||||
probe_libc:posix_memalign (on posix_memalign in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
|
||||
|
||||
[...]
|
||||
```
|
||||
|
||||
What `perf` is telling you here is that it added a new events called `probe_libc:malloc`, `probe_libc:calloc`, ... which will fire every time the respective function is called.
|
||||
|
||||
Let's confirm that our `probe_libc:malloc` probe actually works by running:
|
||||
|
||||
perf stat -e probe_libc:malloc -- bash -c 'echo Hello World'
|
||||
|
||||
which should output something like
|
||||
|
||||
```
|
||||
Hello World
|
||||
|
||||
Performance counter stats for 'bash -c echo Hello World':
|
||||
|
||||
1021 probe_libc:malloc
|
||||
|
||||
0.003840500 seconds time elapsed
|
||||
|
||||
0.000000000 seconds user
|
||||
0.003867000 seconds sys
|
||||
```
|
||||
|
||||
Which seems to have allocated 1021 times, great. If that probe fired 0 times, something went wrong.
|
||||
|
||||
## Running the allocation analysis
|
||||
|
||||
After we have confirmed that our user probe on `malloc` works in general, let's dial it up a little. The first thing we'll need is a program that we'd like to analyse the allocations of.
|
||||
|
||||
For example, we could analyse a program which does 10 subsequent HTTP requests using [AsyncHTTPClient](https://github.com/swift-server/async-http-client). If you're interested in the full source code, please expand below.
|
||||
|
||||
<details>
|
||||
<summary>Demo program source code</summary>
|
||||
|
||||
With the following dependencies
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.3.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.29.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.2"),
|
||||
],
|
||||
```
|
||||
|
||||
We could write this program
|
||||
|
||||
```swift
|
||||
import AsyncHTTPClient
|
||||
import NIO
|
||||
import Logging
|
||||
|
||||
let urls = Array(repeating:"http://httpbin.org/get", count: 10)
|
||||
var logger = Logger(label: "ahc-alloc-demo")
|
||||
|
||||
logger.info("running HTTP requests", metadata: ["count": "\(urls.count)"])
|
||||
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
|
||||
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoop),
|
||||
backgroundActivityLogger: logger)
|
||||
|
||||
func doRemainingRequests(_ remaining: ArraySlice<String>,
|
||||
overallResult: EventLoopPromise<Void>,
|
||||
eventLoop: EventLoop) {
|
||||
var remaining = remaining
|
||||
if let first = remaining.popFirst() {
|
||||
httpClient.get(url: first, logger: logger).map { [remaining] _ in
|
||||
eventLoop.execute { // for shorter stacks
|
||||
doRemainingRequests(remaining, overallResult: overallResult, eventLoop: eventLoop)
|
||||
}
|
||||
}.whenFailure { error in
|
||||
overallResult.fail(error)
|
||||
}
|
||||
} else {
|
||||
return overallResult.succeed(())
|
||||
}
|
||||
}
|
||||
|
||||
let promise = eventLoop.makePromise(of: Void.self)
|
||||
// Kick off the process
|
||||
doRemainingRequests(urls[...],
|
||||
overallResult: promise,
|
||||
eventLoop: eventLoop)
|
||||
|
||||
promise.futureResult.whenComplete { result in
|
||||
switch result {
|
||||
case .success:
|
||||
logger.info("all HTTP requests succeeded")
|
||||
case .failure(let error):
|
||||
logger.error("HTTP request failure", metadata: ["error": "\(error)"])
|
||||
}
|
||||
|
||||
httpClient.shutdown { maybeError in
|
||||
if let error = maybeError {
|
||||
logger.error("AHC shutdown failed", metadata: ["error": "\(error)"])
|
||||
}
|
||||
eventLoop.shutdownGracefully { maybeError in
|
||||
if let error = maybeError {
|
||||
logger.error("EventLoop shutdown failed", metadata: ["error": "\(error)"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("exiting")
|
||||
```
|
||||
</details>
|
||||
|
||||
Assuming you have a program as a Swift package, we should first of all compile it in release mode using `swift build -c release`. Then you should find a binary called `.build/release/your-program-name` which we can then analyse.
|
||||
|
||||
### Allocation counts
|
||||
|
||||
Before we go into visualising the allocations as a flame graph, let's start with the simplest analysis: Getting the total number of allocations
|
||||
|
||||
```
|
||||
perf stat -e 'probe_libc:*' -- .build/release/your-program-name
|
||||
```
|
||||
|
||||
The above command instructs perf to run your program and count the number of times the `probe_libc:malloc` probe was hit. This should be the number of allocations done by your program.
|
||||
|
||||
The output should look something like
|
||||
|
||||
```
|
||||
Performance counter stats for '.build/release/your-program-name':
|
||||
|
||||
68 probe_libc:posix_memalign
|
||||
35 probe_libc:calloc_1
|
||||
0 probe_libc:calloc
|
||||
2977 probe_libc:malloc
|
||||
|
||||
[...]
|
||||
```
|
||||
|
||||
In this case, my program allocated 2,977 times through `malloc` and a few more times through the other allocation functions. If you just want to compare the effects of a pull request you may just want to run this `perf stat` command twice. If you would like to find out _where_ your allocations come from, read on.
|
||||
|
||||
Please note that in this guide we'll use `-e probe_libc:*` instead of individually listing every event like `-e probe_libc:malloc,probe_libc:calloc,probe_libc:calloc_1,probe_libc:posix_memalign`. This assumes that you have _no other_ `perf` user probes installed. If you do, please specify each event you would like to use individually.
|
||||
|
||||
### Collecting the raw data
|
||||
|
||||
With `perf`, we can't really create live graphs whilst the program is running. For most analyses, we want to first record some raw data (usually with `perf record`) and later on transform the recorded data into a graph.
|
||||
|
||||
To get started, let's have `perf` run the program for us and collect the information using the `libc_probe:malloc` we set up before.
|
||||
|
||||
```
|
||||
perf record --call-graph dwarf,16384 \
|
||||
-m 50000 \
|
||||
-e 'probe_libc:*' -- \
|
||||
.build/release/your-program-name
|
||||
```
|
||||
|
||||
Let's break down this command a little:
|
||||
|
||||
- `perf record` instructs `perf` to `record` data, makes sense.
|
||||
- `--call-graph dwarf,16384` instructs `perf` to use the [DWARF](http://www.dwarfstd.org) information to create the call graphs. It also sets the maximum stack dump size to 16k which should be enough to get you full stack traces. Unfortunately, using DWARF is rather slow (see below) but it creates the best call graphs for you.
|
||||
- `-m 50000`: The size of the ring buffer that `perf` uses to buffer. This is given in multiples of `PAGE_SIZE` (usually 4kB) and especially with DWARF this needs to be pretty huge to prevent data loss.
|
||||
- `-e 'probe_libc:*'`: Record when the `malloc`/`calloc`/... probes fire
|
||||
|
||||
What you want to see if output like this
|
||||
|
||||
```
|
||||
<your program's output>
|
||||
[ perf record: Woken up 2 times to write data ]
|
||||
[ perf record: Captured and wrote 401.088 MB perf.data (49640 samples) ]
|
||||
```
|
||||
|
||||
If perf tells you about "lost chunks" and asks you to "check the IO/CPU overhead", you should jump to the 'Overcoming "lost chunks"' section at the end of this document.
|
||||
|
||||
### Flame graphs
|
||||
|
||||
After a successful `perf record`, you can invoke the following command line to produce an SVG file with the flame graph
|
||||
|
||||
```bash
|
||||
perf script | \
|
||||
/FlameGraph/stackcollapse-perf.pl - | \
|
||||
swift demangle --simplified | \
|
||||
/FlameGraph/flamegraph.pl --countname allocations \
|
||||
--width 1600 > out.svg
|
||||
```
|
||||
|
||||
Let's expand a little on what the above command does:
|
||||
|
||||
- It runs `perf script` which dumps the binary information that `perf record` recorded into a textual form.
|
||||
- Next, we invoke `stackcollapse-perf` on it which transforms the stacks that `perf script` outputs into the right format for Flame Graphs,
|
||||
- then we invoke `swift demangle --simplified` which will give us nice symbol names,
|
||||
- and lastly we create the Flame Graph itself
|
||||
|
||||
After this command has run (which may run for a while), you should have an SVG file that you can open in your browser.
|
||||
|
||||
For the above example program, please see an example flame graph below. Note how you can hover over the stack frames and get more information. To focus on a sub tree, you can click any stack frame too.
|
||||
|
||||
Generally, in flame graphs, the X axis just means "count", it does **not** mean time. In other words, whether a stack appears on the left or the right is not determined when that stack was live (this is different in flame _charts_).
|
||||
|
||||
Note that this flame graph is _not_ a CPU flame graph, 1 sample means 1 allocation here and not time spent on the CPU. Also be aware that stack frames that appear wide don't necessarily allocate directly, it means that they or something they call has allocated a lot. For example, `BaseSocketChannel.readable` is a very wide frame, and yet, it is not a function which allocates directly. However, it calls other functions (such as other parts of SwiftNIO and AsyncHTTPClient) that do allocate a lot. It may take a little while to get familiar with flame graphs but there are great resources available online.
|
||||
|
||||

|
||||
|
||||
## Allocation flame graphs on macOS
|
||||
|
||||
So far, this tutorial focussed on Linux and the `perf` tool. You can however create the same graphs on macOS. The process is fairly similar.
|
||||
|
||||
First, let's collect the raw data using [DTrace](https://en.wikipedia.org/wiki/DTrace).
|
||||
|
||||
```
|
||||
sudo dtrace -n 'pid$target::malloc:entry,pid$target::posix_memalign:entry,pid$target::calloc:entry,pid$target::malloc_zone_malloc:entry,pid$target::malloc_zone_calloc:entry,pid$target::malloc_zone_memalign:entry { @s[ustack(100)] = count(); } ::END { printa(@s); }' -c .build/release/your-program > raw.stacks
|
||||
```
|
||||
|
||||
Similar to `perf`'s user probes, dtrace also has probes and the above command instructs DTrace to aggregate the number of calls to the allocation functions `malloc`, `posix_memalign`, `calloc`, and the `malloc_zone_*` equivalents. On Apple platforms, Swift uses a slightly larger number of allocation functions than on Linux, therefore we need to specify a few more functions.
|
||||
|
||||
Once we collected the data, we can also create an SVG file using
|
||||
|
||||
```bash
|
||||
cat raw.stacks |\
|
||||
/FlameGraph/stackcollapse.pl - | \
|
||||
swift demangle --simplified | \
|
||||
/FlameGraph/flamegraph.pl --countname allocations \
|
||||
--width 1600 > out.svg
|
||||
```
|
||||
|
||||
which you will notice is very similar to the `perf` invocation. The only differences are:
|
||||
|
||||
- We use `cat raw.stacks` instead of `perf script` because we already have the textual data in a file with DTrace
|
||||
- Instead of `stackcollapse-perf.pl` (which parses `perf script` output) we use `stackcollapse.pl` (which parses DTrace aggregation output)
|
||||
|
||||
## Other `perf` tricks
|
||||
|
||||
### Prettifying Swift's allocation pattern
|
||||
|
||||
Allocations in Swift usually have a very distinct shape:
|
||||
- Some code creates for example a class instance (which allocates).
|
||||
- This calls `swift_allocObject`,
|
||||
- which calls `swift_slowAlloc`,
|
||||
- which calls `malloc` (where we have our probe).
|
||||
|
||||
To make our flame graphs look nicer, we can apply a small transformation after we have demangled the collapsed stacks:
|
||||
|
||||
```
|
||||
sed -e 's/specialized //g' \
|
||||
-e 's/;swift_allocObject;swift_slowAlloc;__libc_malloc/;A/g'
|
||||
```
|
||||
|
||||
which will get rid of `"specialized "` and replaces `swift_allocObject` calling `swift_slowAlloc`, calling `malloc` with just an `A` (for allocation). The full command will then look like
|
||||
|
||||
```
|
||||
perf script | \
|
||||
/FlameGraph/stackcollapse-perf.pl - | \
|
||||
swift demangle --simplified | \
|
||||
sed -e 's/specialized //g' \
|
||||
-e 's/;swift_allocObject;swift_slowAlloc;__libc_malloc/;A/g' | \
|
||||
/FlameGraph/flamegraph.pl --countname allocations --flamechart --hash \
|
||||
> out.svg
|
||||
```
|
||||
|
||||
### Overcoming "lost chunks"
|
||||
|
||||
When using `perf` with the DWARF call stack unwinding, it is unfortunately easy to run into the following issue
|
||||
|
||||
```
|
||||
[ perf record: Woken up 189 times to write data ]
|
||||
Warning:
|
||||
Processed 4346 events and lost 144 chunks!
|
||||
|
||||
Check IO/CPU overload!
|
||||
|
||||
[ perf record: Captured and wrote 30.868 MB perf.data (3817 samples) ]
|
||||
```
|
||||
|
||||
When `perf` tells you that it lost a number of chunks it means that it lost data. If `perf` lost data, you have a few options:
|
||||
|
||||
- Reduce the amount of work your program is doing. For every allocation, `perf` will need to record a stack trace.
|
||||
- Reduce the maximum "stack dump" that `perf` records by changing the `--call-graph dwarf` parameter to for example `--call-graph dwarf,2048`. The default is to record a maximum of 4096 bytes which gives you pretty deep stacks, if you don't need that you can reduce the number. The tradeoff is that the flame graph may show you `[unknown]` stack frames which means that there are missing stack frames there. The unit is bytes.
|
||||
- You can raise the number of the `-m` parameter which is the size of the ring buffer that `perf` uses in memory (in multiples of `PAGE_SIZE`, usually that is 4kB)
|
||||
- You can give up nice call graphs and replace `--call-tree dwarf` with `--call-tree fp` (`fp` stands for frame pointer).
|
||||
@@ -1,573 +0,0 @@
|
||||
# Server Side Swift on AWS with Fargate, Vapor, and MongoDB Atlas
|
||||
|
||||
This guide illustrates how to deploy a Server-Side Swift workload on AWS. The workload is a REST API for tracking a To Do List. It uses the [Vapor](https://vapor.codes/) framework to program the API methods. The methods store and retrieve data in a [MongoDB Atlas](https://www.mongodb.com/atlas/database) cloud database. The Vapor application is containerized and deployed to AWS on AWS Fargate using the [AWS Copilot](https://aws.github.io/copilot-cli/) toolkit.
|
||||
|
||||
## Architecture
|
||||

|
||||
|
||||
- Amazon API Gateway receives API requests
|
||||
- API Gateway locates your application containers in AWS Fargate through internal DNS managed by AWS Cloud Map
|
||||
- API Gateway forwards the requests to the containers
|
||||
- The containers run the Vapor framework and have methods to GET and POST items
|
||||
- Vapor stores and retrieves items in a MongoDB Atlas cloud database which runs in a MongoDB managed AWS account
|
||||
|
||||
## Prerequisites
|
||||
To build this sample application, you need:
|
||||
|
||||
- [AWS Account](https://console.aws.amazon.com/)
|
||||
- [MongoDB Atlas Database](https://www.mongodb.com/atlas/database)
|
||||
- [AWS Copilot](https://aws.github.io/copilot-cli/) - a command-line tool used to create containerized workloads on AWS
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) - to compile your Swift code into a Docker image
|
||||
- [Vapor](https://vapor.codes/) - to code the REST service
|
||||
- [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) - install the CLI and [configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) it with credentials to your AWS account
|
||||
|
||||
## Step 1: Create Your Database
|
||||
If you are new to MongoDB Atlas, follow this [Getting Started Guide](https://www.mongodb.com/docs/atlas/getting-started/). You need to create the following items:
|
||||
- Atlas Account
|
||||
- Cluster
|
||||
- Database Username / Password
|
||||
- Database
|
||||
- Collection
|
||||
|
||||
In subsequent steps, you provide values to these items to configure the application.
|
||||
|
||||
## Step 2: Initialize a New Vapor Project
|
||||
|
||||
Create a folder for your project.
|
||||
|
||||
```
|
||||
mkdir todo-app && cd todo-app
|
||||
```
|
||||
|
||||
Initialize a Vapor project named *api*.
|
||||
|
||||
```
|
||||
vapor new api -n
|
||||
```
|
||||
|
||||
## Step 3: Add Project Dependencies
|
||||
Vapor initializes a *Package.swift* file for the project dependencies. Your project requires an additional library, [MongoDBVapor](https://github.com/mongodb/mongodb-vapor). Add the MongoDBVapor library to the project and target dependencies of your *Package.swift* file.
|
||||
|
||||
Your updated file should look like this:
|
||||
|
||||
**api/Package.swift**
|
||||
```swift
|
||||
// swift-tools-version:5.6
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "api",
|
||||
platforms: [
|
||||
.macOS(.v12)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.7.0")),
|
||||
.package(url: "https://github.com/mongodb/mongodb-vapor", .upToNextMajor(from: "1.1.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "App",
|
||||
dependencies: [
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
.product(name: "MongoDBVapor", package: "mongodb-vapor")
|
||||
],
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
|
||||
]
|
||||
),
|
||||
.executableTarget(name: "Run", dependencies: [.target(name: "App")]),
|
||||
.testTarget(name: "AppTests", dependencies: [
|
||||
.target(name: "App"),
|
||||
.product(name: "XCTVapor", package: "vapor"),
|
||||
])
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Step 4: Update the Dockerfile
|
||||
You deploy your Swift Server code to AWS Fargate as a Docker image. Vapor generates an initial Dockerfile for your application. Your application requires a few modifications to this Dockerfile:
|
||||
|
||||
- pull the *build* and *run* images from the [Amazon ECR Public Gallery](https://gallery.ecr.aws) container repository
|
||||
- install *libssl-dev* in the build image
|
||||
- install *libxml2* and *curl* in the run image
|
||||
|
||||
Replace the contents of the Vapor generated Dockerfile with the following code:
|
||||
|
||||
**api/Dockerfile**
|
||||
```Dockerfile
|
||||
# ================================
|
||||
# Build image
|
||||
# ================================
|
||||
FROM public.ecr.aws/docker/library/swift:5.6.2-focal as build
|
||||
|
||||
# Install OS updates
|
||||
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
&& apt-get -q update \
|
||||
&& apt-get -q dist-upgrade -y \
|
||||
&& apt-get -y install libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set up a build area
|
||||
WORKDIR /build
|
||||
|
||||
# First just resolve dependencies.
|
||||
# This creates a cached layer that can be reused
|
||||
# as long as your Package.swift/Package.resolved
|
||||
# files do not change.
|
||||
COPY ./Package.* ./
|
||||
RUN swift package resolve
|
||||
|
||||
# Copy entire repo into container
|
||||
COPY . .
|
||||
|
||||
# Build everything, with optimizations
|
||||
RUN swift build -c release --static-swift-stdlib
|
||||
|
||||
# Switch to the staging area
|
||||
WORKDIR /staging
|
||||
|
||||
# Copy main executable to staging area
|
||||
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./
|
||||
|
||||
# Copy resources bundled by SPM to staging area
|
||||
RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
|
||||
|
||||
# Copy any resources from the public directory and views directory if the directories exist
|
||||
# Ensure that by default, neither the directory nor any of its contents are writable.
|
||||
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
|
||||
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
|
||||
|
||||
# ================================
|
||||
# Run image
|
||||
# ================================
|
||||
FROM public.ecr.aws/ubuntu/ubuntu:focal
|
||||
|
||||
# Make sure all system packages are up to date, and install only essential packages.
|
||||
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
&& apt-get -q update \
|
||||
&& apt-get -q dist-upgrade -y \
|
||||
&& apt-get -q install -y \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
curl \
|
||||
libxml2 \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
# Create a vapor user and group with /app as its home directory
|
||||
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
|
||||
|
||||
# Switch to the new home directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built executable and any staged resources from builder
|
||||
COPY --from=build --chown=vapor:vapor /staging /app
|
||||
|
||||
# Ensure all further commands run as the vapor user
|
||||
USER vapor:vapor
|
||||
|
||||
# Let Docker bind to port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Start the Vapor service when the image is run, default to listening on 8080 in production environment
|
||||
ENTRYPOINT ["./Run"]
|
||||
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||
```
|
||||
## Step 5: Update the Vapor Source Code
|
||||
Vapor also generates the sample files needed to code an API. You must customize these files with code that exposes your To Do List API methods and interacts with your MongoDB database.
|
||||
|
||||
The *configure.swift* file initializes an application-wide pool of connections to your MongoDB database. It retrieves the connection string to your MongoDB database from an environment variable at runtime.
|
||||
|
||||
Replace the contents of the file with the following code:
|
||||
|
||||
**api/Sources/App/configure.swift**
|
||||
```swift
|
||||
import MongoDBVapor
|
||||
import Vapor
|
||||
|
||||
public func configure(_ app: Application) throws {
|
||||
|
||||
let MONGODB_URI = Environment.get("MONGODB_URI") ?? ""
|
||||
|
||||
try app.mongoDB.configure(MONGODB_URI)
|
||||
|
||||
ContentConfiguration.global.use(encoder: ExtendedJSONEncoder(), for: .json)
|
||||
ContentConfiguration.global.use(decoder: ExtendedJSONDecoder(), for: .json)
|
||||
|
||||
try routes(app)
|
||||
}
|
||||
```
|
||||
|
||||
The *routes.swift* file defines the methods to your API. These include a *POST Item* method to insert a new item and a *GET Items* method to retrieve a list of all existing items. See comments in the code to understand what happens in each section.
|
||||
|
||||
Replace the contents of the file with the following code:
|
||||
|
||||
**api/Sources/App/routes.swift**
|
||||
```swift
|
||||
import Vapor
|
||||
import MongoDBVapor
|
||||
|
||||
// define the structure of a ToDoItem
|
||||
struct ToDoItem: Content {
|
||||
var _id: BSONObjectID?
|
||||
let name: String
|
||||
var createdOn: Date?
|
||||
}
|
||||
|
||||
// import the MongoDB database and collection names from environment variables
|
||||
let MONGODB_DATABASE = Environment.get("MONGODB_DATABASE") ?? ""
|
||||
let MONGODB_COLLECTION = Environment.get("MONGODB_COLLECTION") ?? ""
|
||||
|
||||
// define an extenstion to the Vapor Request object to interact with the database and collection
|
||||
extension Request {
|
||||
|
||||
var todoCollection: MongoCollection<ToDoItem> {
|
||||
self.application.mongoDB.client.db(MONGODB_DATABASE).collection(MONGODB_COLLECTION, withType: ToDoItem.self)
|
||||
}
|
||||
}
|
||||
|
||||
// define the api routes
|
||||
func routes(_ app: Application) throws {
|
||||
|
||||
// an base level route used for container healthchecks
|
||||
app.get { req in
|
||||
return "OK"
|
||||
}
|
||||
|
||||
// GET items returns a JSON array of all items in the database
|
||||
app.get("items") { req async throws -> [ToDoItem] in
|
||||
try await req.todoCollection.find().toArray()
|
||||
}
|
||||
|
||||
// POST item inserts a new item into the database and returns the item as JSON
|
||||
app.post("item") { req async throws -> ToDoItem in
|
||||
|
||||
var item = try req.content.decode(ToDoItem.self)
|
||||
item.createdOn = Date()
|
||||
|
||||
let response = try await req.todoCollection.insertOne(item)
|
||||
item._id = response?.insertedID.objectIDValue
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The *main.swift* file defines the startup and shutdown code for the application. Change the code to include a *defer* statement to close the connection to your MongoDB database when the application ends.
|
||||
|
||||
Replace the contents of the file with the following code:
|
||||
|
||||
**api/Sources/Run/main.swift**
|
||||
```swift
|
||||
import App
|
||||
import Vapor
|
||||
import MongoDBVapor
|
||||
|
||||
var env = try Environment.detect()
|
||||
try LoggingSystem.bootstrap(from: &env)
|
||||
let app = Application(env)
|
||||
try configure(app)
|
||||
|
||||
// shutdown and cleanup the MongoDB connection when the application terminates
|
||||
defer {
|
||||
app.mongoDB.cleanup()
|
||||
cleanupMongoSwift()
|
||||
app.shutdown()
|
||||
}
|
||||
|
||||
try app.run()
|
||||
```
|
||||
|
||||
## Step 6: Initialize AWS Copilot
|
||||
[AWS Copilot](https://aws.github.io/copilot-cli/) is a command-line utility for generating a containerized application in AWS. You use Copilot to build and deploy your Vapor code as containers in Fargate. Copilot also creates and tracks an AWS Systems Manager secret parameter for the value of your MongoDB connection string. You store this value as a secret as it contains the username and password to your database. You never want to store this in your source code. Finally, Copilot creates an API Gateway to expose a public endpoint for your API.
|
||||
|
||||
Initialize a new Copilot application.
|
||||
|
||||
```bash
|
||||
copilot app init todo
|
||||
```
|
||||
|
||||
Add a new Copilot *Backend Service*. The service refers to the Dockerfile of your Vapor project for instructions on how to build the container.
|
||||
|
||||
```bash
|
||||
copilot svc init --name api --svc-type "Backend Service" --dockerfile ./api/Dockerfile
|
||||
```
|
||||
|
||||
Create a Copilot environment for your application. An environment typically aligns to a phase, such as dev, test, or prod. When prompted, select the AWS credentials profile you configured with the AWS CLI.
|
||||
|
||||
```bash
|
||||
copilot env init --name dev --app todo --default-config
|
||||
```
|
||||
|
||||
Deploy the *dev* environment:
|
||||
|
||||
```bash
|
||||
copilot env deploy --name dev
|
||||
```
|
||||
|
||||
## Step 7: Create a Copilot Secret for Database Credentials
|
||||
|
||||
Your application requires credentials to authenticate to your MongoDB Atlas database. You should never store this sensitive information in your source code. Create a Copilot *secret* to store the credentials. This stores the connection string to your MongoDB cluster in an AWS Systems Manager Secret Parameter.
|
||||
|
||||
Determine the connection string from the MongoDB Atlas website. Select the *Connect* button on your cluster page and the *Connect your application*.
|
||||
|
||||

|
||||
|
||||
Select *Swift version 1.2.0* as the Driver and copy the displayed connection string. It looks something like this:
|
||||
|
||||
```bash
|
||||
mongodb+srv://username:<password>@mycluster.mongodb.net/?retryWrites=true&w=majority
|
||||
```
|
||||
|
||||
The connection string contains your database username and a placeholder for the password. Replace the **\<password\>** section with your database password. Then create a new Copilot secret named MONGODB_URI and save your connection string when prompted for the value.
|
||||
|
||||
```bash
|
||||
copilot secret init --app todo --name MONGODB_URI
|
||||
```
|
||||
|
||||
Fargate injects the secret value as an environment variable into your container at runtime. In Step 5 above, you extracted this value in your *api/Sources/App/configure.swift* file and used it to configure your MongoDB connection.
|
||||
|
||||
## Step 8: Configure the Backend Service
|
||||
|
||||
Copilot generates a *manifest.yml* file for your application that defines the attributes of your service, such as the Docker image, network, secrets, and environment variables. Change the manifest file generated by Copilot to add the following properties:
|
||||
|
||||
- configure a health check for the container image
|
||||
- add a reference to the MONGODB_URI secret
|
||||
- configure the service network as *private*
|
||||
- add environment variables for the MONGODB_DATABASE and MONGODB_COLLECTION
|
||||
|
||||
To implement these changes, replace the contents of the *manifest.yml* file with the following code. Update the values of MONGODB_DATABASE and MONGODB_COLLECTION to reflect the names of the database and cluster you created in MongoDB Atlas for this application.
|
||||
|
||||
If you are building this solution on a **Mac M1/M2** machine, uncomment the **platform** property in the manifest.yml file to specify an ARM build. The default value is *linux/x86_64*.
|
||||
|
||||
**copilot/api/manifest.yml**
|
||||
```yaml
|
||||
# The manifest for the "api" service.
|
||||
# Read the full specification for the "Backend Service" type at:
|
||||
# https://aws.github.io/copilot-cli/docs/manifest/backend-service/
|
||||
|
||||
# Your service name will be used in naming your resources like log groups, ECS services, etc.
|
||||
name: api
|
||||
type: Backend Service
|
||||
|
||||
# Your service is reachable at "http://api.${COPILOT_SERVICE_DISCOVERY_ENDPOINT}:8080" but is not public.
|
||||
|
||||
# Configuration for your containers and service.
|
||||
image:
|
||||
# Docker build arguments. For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/backend-service/#image-build
|
||||
build: api/Dockerfile
|
||||
# Port exposed through your container to route traffic to it.
|
||||
port: 8080
|
||||
healthcheck:
|
||||
command: ["CMD-SHELL", "curl -f http://localhost:8080 || exit 1"]
|
||||
interval: 10s
|
||||
retries: 2
|
||||
timeout: 5s
|
||||
start_period: 0s
|
||||
|
||||
# Mac M1/M2 users - uncomment the following platform line
|
||||
# the default platform is linux/x86_64
|
||||
|
||||
# platform: linux/arm64
|
||||
|
||||
cpu: 256 # Number of CPU units for the task.
|
||||
memory: 512 # Amount of memory in MiB used by the task.
|
||||
count: 2 # Number of tasks that should be running in your service.
|
||||
exec: true # Enable running commands in your container.
|
||||
|
||||
# define the network as private. this will place Fargate in private subnets
|
||||
network:
|
||||
vpc:
|
||||
placement: private
|
||||
|
||||
# Optional fields for more advanced use-cases.
|
||||
#
|
||||
# Pass environment variables as key value pairs.
|
||||
variables:
|
||||
MONGODB_DATABASE: home
|
||||
MONGODB_COLLECTION: todolist
|
||||
|
||||
# Pass secrets from AWS Systems Manager (SSM) Parameter Store.
|
||||
secrets:
|
||||
MONGODB_URI: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/MONGODB_URI
|
||||
|
||||
# You can override any of the values defined above by environment.
|
||||
#environments:
|
||||
# test:
|
||||
# count: 2 # Number of tasks to run for the "test" environment.
|
||||
# deployment: # The deployment strategy for the "test" environment.
|
||||
# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments.
|
||||
```
|
||||
|
||||
## Step 9: Create a Copilot Addon Service for your API Gateway
|
||||
|
||||
Copilot does not have the capability to add an API Gateway to your application. You can, however, add additional AWS resources to your application using [Copilot "Addons"](https://aws.github.io/copilot-cli/docs/developing/additional-aws-resources/#how-to-do-i-add-other-resources).
|
||||
|
||||
Define an addon by creating an *addons* folder under your Copilot service folder and creating a CloudFormation yaml template to define the services you wish to create.
|
||||
|
||||
Create a folder for the addon:
|
||||
|
||||
```bash
|
||||
mkdir -p copilot/api/addons
|
||||
```
|
||||
|
||||
Create a file to define the API Gateway:
|
||||
|
||||
```bash
|
||||
touch copilot/api/addons/apigateway.yml
|
||||
```
|
||||
|
||||
Create a file to pass parameters from the main service into the addon service:
|
||||
|
||||
```bash
|
||||
touch copilot/api/addons/addons.parameters.yml
|
||||
```
|
||||
|
||||
Copy the following code into the *addons.parameters.yml* file. It passes the ID of the Cloud Map service into the addon stack.
|
||||
|
||||
**copilot/api/addons/addons.parameters.yml**
|
||||
```yaml
|
||||
Parameters:
|
||||
DiscoveryServiceARN: !GetAtt DiscoveryService.Arn
|
||||
```
|
||||
|
||||
Copy the following code into the *addons/apigateway.yml* file. It creates an API Gateway using the DiscoveryServiceARN to integrate with the Cloud Map service Copilot created for your Fargate containers.
|
||||
|
||||
**copilot/api/addons/apigateway.yml**
|
||||
```yaml
|
||||
Parameters:
|
||||
App:
|
||||
Type: String
|
||||
Description: Your application's name.
|
||||
Env:
|
||||
Type: String
|
||||
Description: The environment name your service, job, or workflow is being deployed to.
|
||||
Name:
|
||||
Type: String
|
||||
Description: The name of the service, job, or workflow being deployed.
|
||||
DiscoveryServiceARN:
|
||||
Type: String
|
||||
Description: The ARN of the Cloud Map discovery service.
|
||||
|
||||
Resources:
|
||||
ApiVpcLink:
|
||||
Type: AWS::ApiGatewayV2::VpcLink
|
||||
Properties:
|
||||
Name: !Sub "${App}-${Env}-${Name}"
|
||||
SubnetIds:
|
||||
!Split [",", Fn::ImportValue: !Sub "${App}-${Env}-PrivateSubnets"]
|
||||
SecurityGroupIds:
|
||||
- Fn::ImportValue: !Sub "${App}-${Env}-EnvironmentSecurityGroup"
|
||||
|
||||
ApiGatewayV2Api:
|
||||
Type: "AWS::ApiGatewayV2::Api"
|
||||
Properties:
|
||||
Name: !Sub "${Name}.${Env}.${App}.api"
|
||||
ProtocolType: "HTTP"
|
||||
CorsConfiguration:
|
||||
AllowHeaders:
|
||||
- "*"
|
||||
AllowMethods:
|
||||
- "*"
|
||||
AllowOrigins:
|
||||
- "*"
|
||||
|
||||
ApiGatewayV2Stage:
|
||||
Type: "AWS::ApiGatewayV2::Stage"
|
||||
Properties:
|
||||
StageName: "$default"
|
||||
ApiId: !Ref ApiGatewayV2Api
|
||||
AutoDeploy: true
|
||||
|
||||
ApiGatewayV2Integration:
|
||||
Type: "AWS::ApiGatewayV2::Integration"
|
||||
Properties:
|
||||
ApiId: !Ref ApiGatewayV2Api
|
||||
ConnectionId: !Ref ApiVpcLink
|
||||
ConnectionType: "VPC_LINK"
|
||||
IntegrationMethod: "ANY"
|
||||
IntegrationType: "HTTP_PROXY"
|
||||
IntegrationUri: !Sub "${DiscoveryServiceARN}"
|
||||
TimeoutInMillis: 30000
|
||||
PayloadFormatVersion: "1.0"
|
||||
|
||||
ApiGatewayV2Route:
|
||||
Type: "AWS::ApiGatewayV2::Route"
|
||||
Properties:
|
||||
ApiId: !Ref ApiGatewayV2Api
|
||||
RouteKey: "$default"
|
||||
Target: !Sub "integrations/${ApiGatewayV2Integration}"
|
||||
```
|
||||
|
||||
## Step 10: Deploy the Copilot Service
|
||||
When deploying your service, Copilot executes the following actions:
|
||||
|
||||
- builds your Vapor Docker image
|
||||
- deploys the image to the Amazon Elastic Container Registry (ECR) in your AWS account
|
||||
- creates and deploys an AWS CloudFormation template into your AWS account. CloudFormation creates all the services defined in your application.
|
||||
|
||||
```bash
|
||||
copilot svc deploy --name api --app todo --env dev
|
||||
```
|
||||
|
||||
## Step 11: Configure MongoDB Atlas Network Access
|
||||
MongoDB Atlas uses an IP Access List to restrict access to your database to a specific list of source IP addresses. In your application, traffic from your containers originates from the public IP addresses of the NAT Gateways in your application's network. You must configure MongoDB Atlas to allow traffic from these IP addresses.
|
||||
|
||||
To get the IP address of the NAT Gateways, run the following AWS CLI command:
|
||||
|
||||
```bash
|
||||
aws ec2 describe-nat-gateways --filter "Name=tag-key, Values=copilot-application" --query 'NatGateways[?State == `available`].NatGatewayAddresses[].PublicIp' --output table
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```bash
|
||||
---------------------
|
||||
|DescribeNatGateways|
|
||||
+-------------------+
|
||||
| 1.1.1.1 |
|
||||
| 2.2.2.2 |
|
||||
+-------------------+
|
||||
```
|
||||
|
||||
Use the IP addresses to create a Network Access rule in your MongoDB Atlas account for each address.
|
||||
|
||||

|
||||
|
||||
## Step 12: Use your API
|
||||
|
||||
To get the endpoint for your API, use the following AWS CLI command:
|
||||
|
||||
```bash
|
||||
aws apigatewayv2 get-apis --query 'Items[?Name==`api.dev.todo.api`].ApiEndpoint' --output table
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```bash
|
||||
------------------------------------------------------------
|
||||
| GetApis |
|
||||
+----------------------------------------------------------+
|
||||
| https://[your-api-endpoint] |
|
||||
+----------------------------------------------------------+
|
||||
```
|
||||
|
||||
Use cURL or a tool such as [Postman](https://www.postman.com/) to interact with your API:
|
||||
|
||||
Add a To Do List item
|
||||
|
||||
```bash
|
||||
curl --request POST 'https://[your-api-endpoint]/item' --header 'Content-Type: application/json' --data-raw '{"name": "my todo item"}'
|
||||
```
|
||||
|
||||
Retrieve To Do List items
|
||||
|
||||
```bash
|
||||
curl https://[your-api-endpoint]/items
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
When finished with your application, use Copilot to delete it. This deletes all the services created in your AWS account.
|
||||
|
||||
```bash
|
||||
copilot app delete --name todo
|
||||
```
|
||||
@@ -1,123 +0,0 @@
|
||||
# Deploying to AWS on Amazon Linux 2
|
||||
|
||||
This guide describes how to launch an AWS instance running Amazon Linux 2 and configure it to run Swift. The approach taken here is a step by step approach through the console. This is a great way to learn, but for a more mature approach we recommend using Infrastructure as Code tools such as AWS Cloudformation, and the instances are created and managed through automated tools such as Autoscaling Groups. For one approach using those tools see this blog article: https://aws.amazon.com/blogs/opensource/continuous-delivery-with-server-side-swift-on-amazon-linux-2/
|
||||
|
||||
## Launch AWS Instance
|
||||
|
||||
Use the Service menu to select the EC2 service.
|
||||
|
||||

|
||||
|
||||
Click on "Instances" in the "Instances" menu
|
||||
|
||||

|
||||
|
||||
Click on "Launch Instance", either on the top of the screen, or if this is the first instance you have created in the region, in the main section of the screen.
|
||||
|
||||

|
||||
|
||||
Choose an Amazon Machine Image (AMI). In this case the guide is assuming that we will be using Amazon Linux 2, so select that AMI type.
|
||||
|
||||

|
||||
|
||||
Choose an instance type. Larger instances types will have more memory and CPU, but will be more expensive. A smaller instance type will be sufficient to experiment. In this case I have a `t2.micro` instance type selected.
|
||||
|
||||

|
||||
|
||||
Configure instance details. If you want to access this instance directly to the internet, ensure that the subnet that you select is auto-assigns a public IP. It is assumed that the VPC has internet connectivity, which means that it needs to have a Internet Gateway (IGW) and the correct networking rules, but this is the case for the default VPC. If you wish to set this instance up in a private (non-internet accessible) VPC you will need to set up a bastion host, AWS Systems Manager Session Manager, or some other mechanism to connect to the instance.
|
||||
|
||||

|
||||
|
||||
Add storage. The AWS EC2 launch wizard will suggest some form of storage by default. For our testing purposes this should be fine, but if you know that you need more storage, or a different storage performance requirements, then you can change the size and volume type here.
|
||||
|
||||

|
||||
|
||||
Add tags. It is recommended you add as many tags as you need to correctly identify this server later. Especially if you have many servers, it can be difficult to remember which one was used for which purpose. At a very minimum, add a `Name` tag with something memorable.
|
||||
|
||||

|
||||
|
||||
Configure security group. The security group is a stateful firewall that limits the traffic that is accepted by your instance. It is recommended to limit this as much as possible. In this case we are configuring it to only allow traffic on port 22 (ssh). It is recommended to restrict the source as well. To limit it to your workstation's current IP, click on the dropdown under "Source" and select "My IP".
|
||||
|
||||

|
||||
|
||||
Launch instance. Click on "Launch", and select a key pair that you will use to connect to the instance. If you already have a keypair that you have used previously, you can reuse it here by selecting "Choose an existing key pair". Otherwise you can create a keypair now by selecting "Create a new key pair".
|
||||
|
||||

|
||||
|
||||
Wait for instance to launch. When it is ready it will show as "running" under "Instance state", and "2/2 checks pass" under "Status Checks". Click on the instance to view the details on the bottom pane of the window, and look for the "IPv4 Public IP".
|
||||
|
||||

|
||||
|
||||
Connect to instance. Using the keypair that you used or created in the launch step and the IP in the previous step, run ssh. Be sure to use the `-A` option with ssh so that in a future step we will be able to use the same key to connect to a second instance.
|
||||
|
||||

|
||||
|
||||
We have two options to compile the binary: either directly on the instance or using Docker. We will go through both options here.
|
||||
|
||||
## Compile on instance
|
||||
There are two alternative ways to compile code on the instance, either by:
|
||||
|
||||
- [downloading and using the toolchain directly on the instance](#compile-using-a-downloaded-toolchain),
|
||||
- or by [using docker, and compiling inside a docker container](#compile-with-docker)
|
||||
|
||||
### Compile using a downloaded toolchain
|
||||
Run the following command in the SSH terminal. Note that there may be a more up to date version of the swift toolchain. Check https://swift.org/download/#releases for the latest available toolchain url for Amazon Linux 2.
|
||||
|
||||
```
|
||||
SwiftToolchainUrl="https://swift.org/builds/swift-5.4.1-release/amazonlinux2/swift-5.4.1-RELEASE/swift-5.4.1-RELEASE-amazonlinux2.tar.gz"
|
||||
sudo yum install ruby binutils gcc git glibc-static gzip libbsd libcurl libedit libicu libsqlite libstdc++-static libuuid libxml2 tar tzdata ruby -y
|
||||
cd $(mktemp -d)
|
||||
wget ${SwiftToolchainUrl} -O swift.tar.gz
|
||||
gunzip < swift.tar.gz | sudo tar -C / -xv --strip-components 1
|
||||
```
|
||||
|
||||
Finally, check that Swift is correctly installed by running the Swift REPL: `swift`.
|
||||
|
||||

|
||||
|
||||
Let's now download and build an test application. We will use the `--static-swift-stdlib` option so that it can be deployed to a different server without the Swift toolchain installed. These examples will deploy SwiftNIO's [example HTTP server](https://github.com/apple/swift-nio/tree/master/Sources/NIOHTTP1Server), but you can test with your own project.
|
||||
|
||||
```
|
||||
git clone https://github.com/apple/swift-nio.git
|
||||
cd swift-nio
|
||||
swift build -v --static-swift-stdlib -c release
|
||||
```
|
||||
|
||||
## Compile with Docker
|
||||
|
||||
Ensure that Docker and git are installed on the instance:
|
||||
|
||||
```
|
||||
sudo yum install docker git
|
||||
sudo usermod -a -G docker ec2-user
|
||||
sudo systemctl start docker
|
||||
```
|
||||
|
||||
You may have to log out and log back in to be able to use Docker. Check by running `docker ps`, and ensure that it runs without errors.
|
||||
|
||||
Download and compile SwiftNIO's [example HTTP server](https://github.com/apple/swift-nio/tree/master/Sources/NIOHTTP1Server):
|
||||
|
||||
```
|
||||
docker run --rm -v "$PWD:/workspace" -w /workspace swift:5.4-amazonlinux2 /bin/bash -cl ' \
|
||||
swift build -v --static-swift-stdlib -c release
|
||||
```
|
||||
## Test binary
|
||||
Using the same steps as above, launch a second instance (but don't run any of the bash commands above!). Be sure to use the same SSH keypair.
|
||||
|
||||
From within the AWS management console, navigate to the EC2 service and find the instance that you just launched. Click on the instance to see the details, and find the internal IP. In my example, the internal IP is `172.31.3.29`
|
||||
|
||||
From the original build instance, copy the binary to the new server instance:
|
||||
```scp .build/release/NIOHTTP1Server ec2-user@172.31.3.29```
|
||||
|
||||
Now connect to the new instance:
|
||||
```ssh ec2-user@172.31.3.29```
|
||||
|
||||
From within the new instance, test the Swift binary:
|
||||
```
|
||||
NIOHTTP1Server localhost 8080 &
|
||||
curl localhost:8080
|
||||
```
|
||||
|
||||
From here, options are endless and will depend on your application of Swift. If you wish to run a web service be sure to open the Security Group to the correct port and from the correct source. When you are done testing Swift, shut down the instance to avoid paying for unneeded compute. From the EC2 dashboard, select both instances, select "Actions" from the menu, then select "Instance state" and then finally "terminate".
|
||||
|
||||

|
||||
@@ -1,41 +1,31 @@
|
||||
# Build system
|
||||
|
||||
The recommended way to build server applications is with [Swift Package Manager](https://swift.org/package-manager/). SwiftPM provides a cross-platform foundation for building Swift code and works nicely for having one code base that can be edited as well as run on many Swift platforms.
|
||||
The recommended way of managing the builds for server applications is to use the [Swift Package Manager](https://swift.org/package-manager/), it provides a cross-platform foundation for building Swift code and works nicely for having one code base that can be edited as well as run on many Swift platforms
|
||||
|
||||
## Building
|
||||
SwiftPM works from the command line and is also integrated within Xcode.
|
||||
|
||||
You can build your code either by running `swift build` from the terminal, or by triggering the build action in Xcode.
|
||||
Running `swift build` from the terminal will trigger the build (or triggering the build action in Xcode).
|
||||
|
||||
### Docker Usage
|
||||
Swift binaries are architecture-specific, so running the build command on macOS will create a macOS binary, and similarly running the command on Linux will create a Linux binary.
|
||||
Swift is architecture specific, so running the build command on macOS will create a macOS binary. Building on macOS is useful for development and for taking advantage of the great tooling that comes with Xcode. However, most server applications are designed to run on Linux.
|
||||
|
||||
Many Swift developers use macOS for development, which enables taking advantage of the great tooling that comes with Xcode. However, most server applications are designed to run on Linux.
|
||||
To build on Linux and create a Linux binary, use Docker. For example:
|
||||
|
||||
If you are developing on macOS, Docker is a useful tool for building on Linux and creating Linux binaries. Apple publishes official Swift Docker images to [Docker Hub](https://hub.docker.com/_/swift).
|
||||
`$ docker run -v "$PWD:/code" -w /code swift:5.3 swift build`
|
||||
|
||||
For example, to build your application using the latest Swift Docker image:
|
||||
Note, if you want to run the Swift compiler for Intel CPUs on an Apple Silicon (M1) Mac, please add `--platform linux/amd64 -e QEMU_CPU=max` to the commandline. For example:
|
||||
|
||||
`$ docker run -v "$PWD:/code" -w /code swift:latest swift build`
|
||||
`$ docker run -v "$PWD:/code" -w /code --platform linux/amd64 -e QEMU_CPU=max swift:5.3 swift build`
|
||||
|
||||
Note, if you want to run the Swift compiler for Intel CPUs on an Apple Silicon (M1) Mac, please add `--platform linux/amd64 -e QEMU_CPU=max` to the command line. For example:
|
||||
The above commands will run the build using the latest Swift 5.3 Docker image, utilizing bind mounts to the sources on your Mac. Apple publishes Docker images to Docker Hub.
|
||||
|
||||
`$ docker run -v "$PWD:/code" -w /code --platform linux/amd64 -e QEMU_CPU=max swift:latest swift build`
|
||||
|
||||
The above command will run the build using the latest Swift Docker image, utilizing bind mounts to the sources on your Mac.
|
||||
|
||||
### Debug vs. Release Mode
|
||||
By default, SwiftPM will build a debug version of the application. Note that debug versions are not suitable for running in production as they are significantly slower. To build a release version of your app, run `swift build -c release`.
|
||||
|
||||
### Locating Binaries
|
||||
Binary artifacts that can be deployed are found under `.build/x86_64-unknown-linux` on Linux, and `.build/x86_64-apple-macosx` on macOS.
|
||||
|
||||
SwiftPM can show you the full binary path using `swift build --show-bin-path -c release`.
|
||||
Binary artifacts that could be deployed are be found under .build/x86_64-unknown-linux, or .build/x86_64-apple-macosx for macOS binaries. SwiftPM can show you the full binary path using `swift build --show-bin-path -c release`.
|
||||
|
||||
### Building for production
|
||||
|
||||
- Build production code in release mode by compiling with `swift build -c release`. Running code compiled in debug mode will hurt performance significantly.
|
||||
- Build production code in release mode by compiling with `swift build -c release`. Running code compiled in debug mode will hurt performance signficantly.
|
||||
|
||||
- For best performance in Swift 5.2 or later, pass `-Xswiftc -cross-module-optimization` (this won't work in Swift versions before 5.2) - enabling this should be verified with performance tests (as any optimization changes) as it may sometimes cause performance regressions.
|
||||
- For best performance in Swift 5.2 or later, pass `-Xswiftc -cross-module-optimization` (this won't work in Swift versions before 5.2)
|
||||
|
||||
- Integrate [`swift-backtrace`](https://github.com/swift-server/swift-backtrace) into your application to make sure backtraces are printed on crash. Backtraces do not work out-of-the-box on Linux, and this library helps to fill the gap. Eventually this will become a language feature and not require a discrete library.
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
# Swift Concurrency adoption guidelines for Swift Server Libraries
|
||||
|
||||
This writeup attempts to provide a set of guidelines to follow by authors of server-side Swift libraries. Specifically a lot of the discussion here revolves around what to do about existing APIs and libraries making extensive use of Swift NIO’s `EventLoopFuture` and related types.
|
||||
|
||||
Swift Concurrency is a multi-year effort. It is very valuable for the server community to participate in this multi-year adoption of the concurrency features, one by one, and provide feedback while doing so. As such, we should not hold off adopting concurrency features until Swift 6 as we may miss valuable opportunity to improve the concurrency model.
|
||||
|
||||
In 2021 we saw structured concurrency and actors arrive with Swift 5.5. Now is a great time to provide APIs using those primitives. In the future we will see fully checked Swift concurrency. This will come with breaking changes. For this reason adopting the new concurrency features can be split into two phases.
|
||||
|
||||
|
||||
## What you can do right now
|
||||
|
||||
### API Design
|
||||
|
||||
Firstly, existing libraries should strive to add `async` functions where possible to their user-facing “surface” APIs in addition to existing `*Future` based APIs wherever possible. These additive APIs can be gated on the Swift version and can be added without breaking existing users' code, for example like this:
|
||||
|
||||
```swift
|
||||
extension Worker {
|
||||
func work() -> EventLoopFuture<Value> { ... }
|
||||
|
||||
#if compiler(>=5.5) && canImport(_Concurrency)
|
||||
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
|
||||
func work() async throws -> Value { ... }
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
If a function cannot fail but was using futures before, it should not include the `throws` keyword in its new incarnation.
|
||||
|
||||
Such adoption can begin immediately, and should not cause any issues to existing users of existing libraries.
|
||||
|
||||
### SwiftNIO helper functions
|
||||
|
||||
To allow an easy transition to async code, SwiftNIO offers a number of helper methods on `EventLoopFuture` and `-Promise`.
|
||||
|
||||
On every `EventLoopFuture` you can call `.get()` to transition the future into an `await`-able invocation. If you want to translate async/await calls to an `EventLoopFuture` we recommend the following pattern:
|
||||
|
||||
```swift
|
||||
#if compiler(>=5.5) && canImport(_Concurrency)
|
||||
|
||||
func yourAsyncFunctionConvertedToAFuture(on eventLoop: EventLoop)
|
||||
-> EventLoopFuture<Result> {
|
||||
let promise = context.eventLoop.makePromise(of: Out.self)
|
||||
promise.completeWithTask {
|
||||
try await yourMethod(yourInputs)
|
||||
}
|
||||
return promise.futureResult
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
Further helpers exist for `EventLoopGroup`, `Channel`, `ChannelOutboundInvoker` and `ChannelPipeline`.
|
||||
|
||||
|
||||
### `#if` guarding code using Concurrency
|
||||
|
||||
In order to have code using concurrency along with code not using concurrency, you may have to `#if` guard certain pieces of code. The correct way to do so is the following:
|
||||
|
||||
```swift
|
||||
#if compiler(>=5.5) && canImport(_Concurrency)
|
||||
...
|
||||
#endif
|
||||
```
|
||||
|
||||
Please note that you do _not_ need to _import_ the `_Concurrency` at all, if it is present it is imported automatically.
|
||||
|
||||
```swift
|
||||
#if compiler(>=5.5) && canImport(_Concurrency)
|
||||
// DO NOT DO THIS.
|
||||
// Instead don't do any import and it'll import automatically when possible.
|
||||
import _Concurrency
|
||||
#endif
|
||||
```
|
||||
|
||||
|
||||
### Sendable Checking
|
||||
|
||||
> [SE-0302][SE-0302] introduced the `Sendable` protocol, which is used to indicate which types have values that can safely be copied across actors or, more generally, into any context where a copy of the value might be used concurrently with the original. Applied uniformly to all Swift code, `Sendable` checking eliminates a large class of data races caused by shared mutable state.
|
||||
>
|
||||
> -- from [Staging in Sendable checking][sendable-staging], which outlines the `Sendable` adoption plan for Swift 6.
|
||||
|
||||
In the future we will see fully checked Swift concurrency. The language features to support this are the `Sendable` protocol and the `@Sendable` keyword for closures. Since sendable checking will break existing Swift code, a new major Swift version is required.
|
||||
|
||||
To ease the transition to fully checked Swift code, it is possible to annotate your APIs with the `Sendable` protocol today.
|
||||
|
||||
You can start adopting Sendable and getting appropriate warnings in Swift 5.5 already by passing the `-warn-concurrency` flag, you can do so in SwiftPM for the entire project like so:
|
||||
|
||||
```
|
||||
swift build -Xswiftc -Xfrontend -Xswiftc -warn-concurrency
|
||||
```
|
||||
|
||||
|
||||
#### Sendable checking today
|
||||
|
||||
Sendable checking is currently disabled in Swift 5.5(.0) because it was causing a number of tricky situations for which we lacked the tools to resolve.
|
||||
|
||||
Most of these issues have been resolved on today’s `main` branch of the compiler, and are expected to land in the next Swift 5.5 releases. It may be worthwhile waiting for adoption until the next version(s) after 5.5.0.
|
||||
|
||||
For example, one of such capabilities is the ability for tuples of `Sendable` types to conform to `Sendable` as well. We recommend holding off adoption of `Sendable` until this patch lands in Swift 5.5 (which should be relatively soon). With this change, the difference between Swift 5.5 with `-warn-concurrency` enabled and Swift 6 mode should be very small, and manageable on a case by case basis.
|
||||
|
||||
#### Backwards compatibility of declarations and “checked” Swift Concurrency
|
||||
|
||||
Adopting Swift Concurrency will progressively cause more warnings, and eventually compile time errors in Swift 6 when sendability checks are violated, marking potentially unsafe code.
|
||||
|
||||
It may be difficult for a library to maintain a version that is compatible with versions prior to Swift 6 while also fully embracing the new concurrency checks. For example, it may be necessary to mark generic types as `Sendable`, like so:
|
||||
|
||||
```swift
|
||||
struct Container<Value: Sendable>: Sendable { ... }
|
||||
```
|
||||
|
||||
Here, the `Value` type must be marked `Sendable` for Swift 6’s concurrency checks to work properly with such container. However, since the `Sendable` type does not exist in Swift prior to Swift 5.5, it would be difficult to maintain a library that supports both Swift 5.4+ as well as Swift 6.
|
||||
|
||||
In such situations, it may be helpful to utilize the following trick to be able to share the same `Container` declaration between both Swift versions of the library:
|
||||
|
||||
```swift
|
||||
#if swift(>=5.5) && canImport(_Concurrency)
|
||||
public typealias MYPREFIX_Sendable = Swift.Sendable
|
||||
#else
|
||||
public typealias MYPREFIX_Sendable = Any
|
||||
#endif
|
||||
```
|
||||
|
||||
> **NOTE:** Yes, we're using `swift(>=5.5)` here, while we're using `compiler(>=5.5)` to guard specific APIs using concurrency features.
|
||||
|
||||
The `Any` alias is effectively a no-op when applied as generic constraint, and thus this way it is possible to keep the same `Container<Value>` declaration working across Swift versions.
|
||||
|
||||
### Task Local Values and Logging
|
||||
|
||||
The newly introduced Task Local Values API ([SE-0311][SE-0311]) allows for implicit carrying of metadata along with `Task` execution. It is a natural fit for tracing and carrying metadata around with task execution, and e.g. including it in log messages.
|
||||
|
||||
We are working on adjusting [SwiftLog](https://github.com/apple/swift-log) to become powerful enough to automatically pick up and log specific task local values. This change will be introduced in a source compatible way.
|
||||
|
||||
For now libraries should continue using logger metadata, but we expect that in the future a lot of the cases where metadata is manually passed to each log statement can be replaced with setting task local values.
|
||||
|
||||
### Preparing for the concept of Deadlines
|
||||
|
||||
Deadlines are another feature that closely relate to Swift Concurrency, and were originally pitched during the early versions of the Structured Concurrency proposal and later on moved out of it. The Swift team remains interested in introducing deadline concepts to the language and some preparation for it already has been performed inside the concurrency runtime. Right now however, there is no support for deadlines in Swift Concurrency and it is fine to continue using mechanisms like `NIODeadline` or similar mechanisms to cancel tasks after some period of time has passed.
|
||||
|
||||
Once Swift Concurrency gains deadline support, they will manifest in being able to cancel a task (and its child tasks) once such deadline (point in time) has been exceeded. For APIs to be “ready for deadlines” they don’t have to do anything special other than preparing to be able to deal with `Task`s and their cancellation.
|
||||
|
||||
### Cooperatively handling Task cancellation
|
||||
|
||||
`Task` cancellation exists today in Swift Concurrency and is something that libraries may already handle. In practice it means that any asynchronous function (or function which is expected to be called from within `Task`s), may use the [`Task.isCancelled`](https://developer.apple.com/documentation/swift/task/3814832-iscancelled) or [`try Task.checkCancellation()`](https://developer.apple.com/documentation/swift/task/3814826-checkcancellation) APIs to check if the task it is executing in was cancelled, and if so, it may cooperatively abort any operation it was currently performing.
|
||||
|
||||
Cancellation can be useful in long running operations, or before kicking off some expensive operation. For example, an HTTP client MAY check for cancellation before it sends a request — it perhaps does not make sense to send a request if it is known the task awaiting on it does not care for the result anymore after all!
|
||||
|
||||
Cancellation in general can be understood as “the one waiting for the result of this task is not interested in it anymore”, and it usually is best to throw a “cancelled” error when the cancellation is encountered. However, in some situations returning a “partial” result may also be appropriate (e.g. if a task is collecting many results, it may return those it managed to collect until now, rather than returning none or ignoring the cancellation and collecting all remaining results).
|
||||
|
||||
## What to expect with Swift 6
|
||||
|
||||
### Sendable: Global variables & imported code
|
||||
|
||||
Today, Swift 5.5 does not yet handle global variables at all within its concurrency checking model. This will soon change but the exact semantics are not set in stone yet. In general, avoid using global properties and variables wherever possible to avoid running into issues in the future. Consider deprecating global variables if able to.
|
||||
|
||||
Some global variables have special properties, such as `errno` which contains the error code of system calls. It is a thread local variable and therefore safe to read from any thread/`Task`. We expect to improve the importer to annotate such globals with some kind of “known to be safe” annotation, such that the Swift code using it, even in fully checked concurrency mode won’t complain about it. Having that said, using `errno` and other “thread local” APIs is very error prone in Swift Concurrency because thread-hops may occur at any suspension point, so the following snippet is very likely incorrect:
|
||||
|
||||
```swift
|
||||
sys_call(...)
|
||||
await ...
|
||||
let err = errno // BAD, we are most likely on a different thread here (!)
|
||||
```
|
||||
|
||||
Please take care when interacting with any thread-local API from Swift Concurrency. If your library had used thread local storage before, you will want to move them to use [task-local values](https://github.com/apple/swift-evolution/blob/main/proposals/0311-task-locals.md) instead as they work correctly with Swift’s structured concurrency tasks.
|
||||
|
||||
Another tricky situation is with imported C code. There may be no good way to annotate the imported types as Sendable (or it would be too troublesome to do so by hand). Swift is likely to gain improved support for imported code and potentially allow ignoring some of the concurrency safety checks on imported code.
|
||||
|
||||
These relaxed semantics for imported code are not implemented yet, but keep it in mind when working with C APIs from Swift and trying to adopt the `-warn-concurrency` mode today. Please file any issues you hit on [bugs.swift.org](https://bugs.swift.org/secure/Dashboard.jspa) so we can inform the development of these checking heuristics based on real issues you hit.
|
||||
|
||||
### Custom Executors
|
||||
|
||||
We expect that Swift Concurrency will allow custom executors in the future. A custom executor would allow the ability to run actors / tasks “on” such executor. It is possible that `EventLoop`s could become such executors, however the custom executor proposal has not been pitched yet.
|
||||
|
||||
While we expect potential performance gains from using custom executors “on the same event loop” by avoiding asynchronous hops between calls to different actors, their introduction will not fundamentally change how NIO libraries are structured.
|
||||
|
||||
The guidance here will evolve as Swift Evolution proposals for Custom Executors are proposed, but don’t hold off adopting Swift Concurrency until custom executors “land” - it is important to start adoption early. For most code we believe that the gains from adopting Swift Concurrency vastly outweigh the slight performance cost actor-hops might induce.
|
||||
|
||||
|
||||
### Reduce use of SwiftNIO Futures as “Concurrency Library“
|
||||
|
||||
SwiftNIO currently provides a number of concurrency types for the Swift on Server ecosystem. Most notably `EventLoopFuture`s and `EventLoopPromise`s, that are used widely for asynchronous results. While the SSWG recommended using those at the API level in the past for easier interplay of server libraries, we advise to deprecate or remove such APIs once Swift 6 lands. The swift-server ecosystem should go all in on the structured concurrency features the languages provides. For this reason, it is crucial to provide async/await APIs today, to give your library users time to adopt the new APIs.
|
||||
|
||||
Some NIO types will remain however in the public interfaces of Swift on server libraries. We expect that networking clients and servers continue to be initialized with `EventLoopGroup`s. The underlying transport mechanism (`NIOPosix` and `NIOTransportServices`) should become implementation details however and should not be exposed to library adopters.
|
||||
|
||||
### SwiftNIO 3
|
||||
|
||||
While subject to change, it is likely that SwiftNIO will cut a 3.0 release in the months after Swift 6.0, at which point in time Swift will have enabled “full” `Sendable` checking.
|
||||
|
||||
You should not expect NIO to suddenly become “more async”, NIO’s inherent design principles are about performing small tasks on the event loop and using Futures for any async operations. The design of NIO is not expected to change. Channel pipelines are not expected to become "async" in the Swift Concurrency meaning of the word. This is because SwiftNIO is, at its heard, an IO system, and that poses a challenge to the co-operative, shared, thread-pool used by Swift Concurrency. This thread pool must not be blocked by any operation, because doing so will starve the pool and prevent further progress of other async tasks.
|
||||
|
||||
I/O systems however must, at some point, block a thread waiting for more I/O events, either in an I/O syscall or in something like epoll_wait. This is how NIO works: each of the event loop threads ultimately blocks on epoll_wait. We can’t do that inside the cooperative thread pool, as to do so would starve it for other async tasks, so we’d have to do so on a different thread. As such, SwiftNIO should not be used _on_ the cooperative threadpool, but should take ownership and full control of its threads–because it is an I/O system.
|
||||
|
||||
It would be possible to make all NIO work happen on the co-operative pool, and thread-hop between each I/O operation and dispatching it onto the async/await pool, however this is not acceptable for high performance I/O: the context switch for _each I/O operation_ is too expensive. As a result, SwiftNIO is not planning to just adopt Swift Concurrency for the ease of use it brings, because in its specific context, the context switches are not an acceptable tradeoff. SwiftNIO could however cooperate with Swift Concurrency with the arrival of "custom executors" in the language runtime, however this has not been fully proposed yet, so we are not going to speculate about this too much.
|
||||
|
||||
The NIO team will however use the chance to remove deprecated APIs and improve some APIs. The scope of changes should be comparable to the NIO1 → NIO2 version bump. If your SwiftNIO code compiles today without warnings, chances are high that it will continue to work without modifications in NIO3.
|
||||
|
||||
After the release of NIO3, NIO2 will see bug fixes only.
|
||||
|
||||
### End-user code breakage
|
||||
|
||||
It is expected that Swift 6 will break some code. As mentioned SwiftNIO 3 is also going to be released sometime around Swift 6 dropping. Keeping this in mind, it might be a good idea to align major version releases around the same time, along with updating version requirements to Swift 6 and NIO 3 in your libraries.
|
||||
|
||||
Both Swift and SwiftNIO are not planning to do “vast amounts of change”, so adoption should be possible without major pains.
|
||||
|
||||
### Guidance for library users
|
||||
|
||||
As soon as Swift 6 comes out, we recommend using the latest Swift 6 toolchains, even if using the Swift 5.5.n language mode (which may yield only warnings rather than hard failures on failed Sendability checks). This will result in better warnings and compiler hints, than just using a 5.5 toolchain.
|
||||
|
||||
[sendable-staging]: https://github.com/DougGregor/swift-evolution/blob/sendable-staging/proposals/nnnn-sendable-staging.md
|
||||
[SE-0302]: https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md
|
||||
[SE-0311]: https://github.com/apple/swift-evolution/blob/main/proposals/0311-task-locals.md
|
||||
@@ -1,15 +1,9 @@
|
||||
|
||||
## Deployment to Servers or Public Cloud
|
||||
|
||||
The following guides can help with the deployment to public cloud providers:
|
||||
* [AWS](aws.md)
|
||||
* [DigitalOcean](digital-ocean.md)
|
||||
* [Heroku](heroku.md)
|
||||
* [Kubernetes & Docker](packaging.md#docker)
|
||||
* [GCP](gcp.md)
|
||||
* _Have a guides for other popular public clouds like Azure? Add it here!_
|
||||
Once an application is built for production, it needs to be packaged for deployment. There are several strategies for packaging Swift applications for deployment, see the [Packaging Guide](packaging.md) for more information.
|
||||
|
||||
If you are deploying to you own servers (e.g. bare metal, VMs or Docker) there are several strategies for packaging Swift applications for deployment, see the [Packaging Guide](packaging.md) for more information.
|
||||
A separate guide for [Ubuntu on DigitalOcean](digital-ocean.md) is also available.
|
||||
|
||||
### Deploying a Debuggable Configuration (Production on Linux)
|
||||
|
||||
@@ -20,3 +14,5 @@ If you are deploying to you own servers (e.g. bare metal, VMs or Docker) there a
|
||||
instead of `./my-program` to get something something akin to a 'crash report' on crash.
|
||||
|
||||
- If you don't have `--privileged` (or `--security-opt seccomp=unconfined`) containers (meaning you won't be able to use `lldb`) or you don't want to use lldb, consider using a library like [`swift-backtrace`](https://github.com/swift-server/swift-backtrace) to get stack traces on crash.
|
||||
|
||||
_TODO: add guides for popular public clouds like AWS, GCP, Azure, Heroku, etc._
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
# Deploying to Google Cloud Platform (GCP)
|
||||
|
||||
This guide describes how to build and run your Swift Server on serverless
|
||||
architecture with [Google Cloud Build](https://cloud.google.com/build) and
|
||||
[Google Cloud Run](https://cloud.google.com/run). We'll use
|
||||
[Artifact Registry](https://cloud.google.com/artifact-registry/docs/docker/quickstart)
|
||||
to store the Docker images.
|
||||
|
||||
## Google Cloud Platform Setup
|
||||
|
||||
You can read about
|
||||
[Getting Started with GCP](https://cloud.google.com/gcp/getting-started/) in
|
||||
more detail. In order to run Swift Server applications, we need to:
|
||||
|
||||
- enable [Billing](https://console.cloud.google.com/billing) (requires a credit
|
||||
card). Note that when creating a new account, GCP provides you with $300 of
|
||||
free credit to use in the first 90 days. You can follow this guide for free
|
||||
for a new account. Everything in this guide should fall into the "Free Tier"
|
||||
category at GCP (120 build minutes per day, 2 million Cloud Run requests per
|
||||
month
|
||||
[Free Tier Usage Limits](https://cloud.google.com/free/docs/gcp-free-tier#free-tier-usage-limits))
|
||||
- enable the
|
||||
[Cloud Build API](https://console.cloud.google.com/apis/api/cloudbuild.googleapis.com/overview)
|
||||
- enable the
|
||||
[Cloud Run Admin API](https://console.cloud.google.com/apis/api/run.googleapis.com/overview)
|
||||
- enable the
|
||||
[Artifact Registry API](https://console.cloud.google.com/apis/api/artifactregistry.googleapis.com/overview)
|
||||
- [create a Repository in the Artifact Registry](https://console.cloud.google.com/artifacts/create-repo)
|
||||
(Format: Docker, Region: your choice)
|
||||
|
||||
## Project Requirements
|
||||
|
||||
Please verify that your server listens on `0.0.0.0`, not `127.0.0.1` and it's
|
||||
recommended to use the environment variable `$PORT` instead of a hard-coded
|
||||
value. For the workflow to pass, two files are essential, both need to be in the
|
||||
project root:
|
||||
|
||||
1. Dockerfile
|
||||
2. cloudbuild.yaml
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
You should test your Dockerfile with `docker build . -t test` and
|
||||
`docker run -p 8080:8080 test` and make sure it builds and runs locally.
|
||||
|
||||
The _Dockerfile_ is the same as in the [packaging guide](./packaging.md#docker).
|
||||
Replace `<executable-name>` with your `executableTarget` (ie. "Server"):
|
||||
|
||||
```Dockerfile
|
||||
#------- build -------
|
||||
FROM swift:centos as builder
|
||||
|
||||
# set up the workspace
|
||||
RUN mkdir /workspace
|
||||
WORKDIR /workspace
|
||||
|
||||
# copy the source to the docker image
|
||||
COPY . /workspace
|
||||
|
||||
RUN swift build -c release --static-swift-stdlib
|
||||
|
||||
#------- package -------
|
||||
FROM centos:8
|
||||
# copy executable
|
||||
COPY --from=builder /workspace/.build/release/<executable-name> /
|
||||
|
||||
# set the entry point (application name)
|
||||
CMD ["<executable-name>"]
|
||||
```
|
||||
|
||||
### `cloudbuild.yaml`
|
||||
|
||||
The `cloudbuild.yaml` files contains a set of steps to build the server image
|
||||
directly in the cloud and deploy a new Cloud Run instance after the successful
|
||||
build. `${_VAR}` are
|
||||
["substitution variables"](https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values)
|
||||
that are available during build time and can be passed on into the runtime
|
||||
environment in the "deploy" phase. We will set the variables later when we
|
||||
configure the [Build Trigger](#deployment) (Step 5).
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- '-c'
|
||||
- |
|
||||
docker pull ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:latest || exit 0
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- build
|
||||
- -t
|
||||
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:$SHORT_SHA
|
||||
- -t
|
||||
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:latest
|
||||
- .
|
||||
- --cache-from
|
||||
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:latest
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
[
|
||||
'push',
|
||||
'${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:$SHORT_SHA'
|
||||
]
|
||||
- name: 'gcr.io/cloud-builders/gcloud'
|
||||
args:
|
||||
- run
|
||||
- deploy
|
||||
- swift-service
|
||||
- --image=${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:$SHORT_SHA
|
||||
- --port=8080
|
||||
- --region=${_REGION}
|
||||
- --memory=512Mi
|
||||
- --platform=managed
|
||||
- --allow-unauthenticated
|
||||
- --min-instances=0
|
||||
- --max-instances=5
|
||||
images:
|
||||
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:$SHORT_SHA'
|
||||
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY_NAME}/${_SERVICE_NAME}:latest'
|
||||
timeout: 1800s
|
||||
```
|
||||
|
||||
### The steps in detail
|
||||
|
||||
1. Pull the latest image from the Artifact Registry to retrieve cached layers
|
||||
2. Build the image with `$SHORT_SHA` and `latest` tag
|
||||
3. Push the image to the Artifact Registry
|
||||
4. Deploy the image to Cloud Run
|
||||
|
||||
`images` specifies the build images to store in the registry. The default
|
||||
`timeout` is 10 minutes, so we'll need to increase it for Swift builds. We use
|
||||
`8080` as the default port here, though it's recommended to remove this line and
|
||||
have the server listen on `$PORT`.
|
||||
|
||||
## Deployment
|
||||
|
||||

|
||||
|
||||
Push all files to a remote repository. Cloud Build currently supports, GitHub,
|
||||
Bitbucket and GitLab. now) and head to
|
||||
[Cloud Build Triggers](https://console.cloud.google.com/cloud-build/triggers)
|
||||
and click "Create Trigger":
|
||||
|
||||
1. Add a name and description
|
||||
2. Event: "Push to a branch" is active
|
||||
3. Source: "Connect New Repository" and authorize with your code provider, and
|
||||
add the repository where your Swift server code is hosted. Note that you need
|
||||
to configure
|
||||
[GitHub](https://cloud.google.com/build/docs/automating-builds/build-repos-from-github),
|
||||
[GitLab](https://cloud.google.com/build/docs/automating-builds/build-repos-from-gitlab)
|
||||
or
|
||||
[Bitbucket](https://cloud.google.com/build/docs/automating-builds/build-repos-from-bitbucket-cloud)
|
||||
to allow GCP access first.
|
||||
4. Configuration: "Cloud Build configuration file" / Location: Repository
|
||||
5. Advanced:
|
||||
[Substitution variables](https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values):
|
||||
You need to set the variables for region, repository name and service name
|
||||
here. You can pick a
|
||||
[region of your choice](https://cloud.google.com/about/locations/) (ie.
|
||||
`us-central1`). All custom variables must start with an underscore
|
||||
(`_REGION`). `_REPOSITORY_NAME` and `_SERVICE_NAME` are up to you. If you use
|
||||
environment variables for example to connect to a database or 3rd party
|
||||
services, you can set the values here too.
|
||||
6. "Create"
|
||||
|
||||
As a last step before deploying the new service, go to the
|
||||
[Cloud Build Settings](https://console.cloud.google.com/cloud-build/settings)
|
||||
and make sure "Cloud Run" is enabled. This gives Cloud Build the necessary IAM
|
||||
permissions to deploy Cloud Run services.
|
||||
|
||||

|
||||
|
||||
In the Trigger overview page, you should see your new "swift-service" trigger.
|
||||
Click on "RUN" on the right to start the trigger manually from the `main`
|
||||
branch. With a simple Hummingbird project the build takes about 7-8 minutes.
|
||||
Vapor takes about 25 minutes on the standard/small build machines, which are
|
||||
fairly slow. "Jordane" from the Vapor Discord community
|
||||
[recommends using `machineType: E2_HIGHCPU_8`](https://discord.com/channels/431917998102675485/447893851374616576/915819735738888222)
|
||||
in the `cloudbuild.yaml` to speed up deployments:
|
||||
|
||||
```yaml
|
||||
options:
|
||||
machineType: 'E2_HIGHCPU_8'
|
||||
```
|
||||
|
||||
After a successful build you should see the service URL in the build logs:
|
||||
|
||||

|
||||
|
||||
You can head over to Cloud Run and see your service running there:
|
||||
|
||||

|
||||
|
||||
The trigger will deploy every new commit on `main`. You can also enable Pull
|
||||
Request triggers for feature-driven workflows. Cloud Build also allows
|
||||
blue/green builds, auto-scaling and much more.
|
||||
|
||||
You can now connect your custom domain to the new service and go live.
|
||||
|
||||
## Cleanup
|
||||
|
||||
- delete the Cloud Run service
|
||||
- delete the Cloud Build trigger
|
||||
- delete the Artifact Registry repository
|
||||
@@ -1,130 +0,0 @@
|
||||
# Linux `perf`
|
||||
|
||||
## `perf`, what's that?
|
||||
|
||||
The Linux [`perf` tool](https://perf.wiki.kernel.org/index.php/Main_Page) is an incredibly powerful tool, that can amongst other things be used for:
|
||||
|
||||
- Sampling CPU-bound processes (or the whole) system to analyse which part of your application is consuming the CPU time
|
||||
- Accessing CPU performance counters (PMU)
|
||||
- "user probes" (uprobes) which trigger for example when a certain function in your application runs
|
||||
|
||||
In general, `perf` can count and/or record the call stacks of your threads when a certain event occurs. These events can be triggered by:
|
||||
|
||||
- Time (e.g. 1000 times per second), useful for time profiling. For an example use, see the [CPU performance debugging guide](performance.md).
|
||||
- System calls, useful to see where your system calls are happening.
|
||||
- Various system events, for example if you'd like to know when context switches occur.
|
||||
- CPU performance counters, useful if your performance issues can be traced down to micro-architectural details of your CPU (such as branch mispredictions). For an example, see [SwiftNIO's Advanced Performance Analysis guide](https://github.com/apple/swift-nio/blob/main/docs/advanced-performance-analysis.md).
|
||||
- and much more
|
||||
|
||||
## Getting `perf` to work
|
||||
|
||||
Unfortunately, getting `perf` to work depends on your environment. Below, please find a selection of environments and how to get `perf` to work there.
|
||||
|
||||
### Installing `perf`
|
||||
|
||||
Technically, `perf` is part of the Linux kernel sources and you'd want a `perf` version that exactly matches your Linux kernel version. In many cases however a "close-enough" `perf` version will do too. If in doubt, use a `perf` version that's slightly older than your kernel over one that's newer.
|
||||
|
||||
- Ubuntu
|
||||
|
||||
```
|
||||
apt-get update && apt-get -y install linux-tools-generic
|
||||
```
|
||||
|
||||
See below for more information because Ubuntu packages a different `perf` per kernel version.
|
||||
- Debian
|
||||
|
||||
```
|
||||
apt-get update && apt-get -y install linux-perf
|
||||
```
|
||||
|
||||
- Fedora/RedHat derivates
|
||||
|
||||
```
|
||||
yum install -y perf
|
||||
```
|
||||
|
||||
You can confirm that your `perf` installation works using `perf stat -- sleep 0.1` (if you're already `root`) or `sudo perf stat -- sleep 0.1`.
|
||||
|
||||
|
||||
##### `perf` on Ubuntu when you can't match the kernel version
|
||||
|
||||
On Ubuntu (and other distributions that package `perf` per kernel version) you may see an error after installing `linux-tools-generic`. The error message will look similar to
|
||||
|
||||
```
|
||||
$ perf stat -- sleep 0.1
|
||||
WARNING: perf not found for kernel 5.10.25
|
||||
|
||||
You may need to install the following packages for this specific kernel:
|
||||
linux-tools-5.10.25-linuxkit
|
||||
linux-cloud-tools-5.10.25-linuxkit
|
||||
|
||||
You may also want to install one of the following packages to keep up to date:
|
||||
linux-tools-linuxkit
|
||||
linux-cloud-tools-linuxkit
|
||||
```
|
||||
|
||||
The best fix for this is to follow what `perf` says and to install one of the above packages. If you're in a Docker container, this may not be possible because you'd need to match the kernel version (which is especially difficult in Docker for Mac because it uses a VM). For example, the suggested `linux-tools-5.10.25-linuxkit` is not actually available.
|
||||
|
||||
As a workaround, you can try one of the following options
|
||||
|
||||
- If you're already `root` and prefer a shell `alias` (only valid in this shell)
|
||||
|
||||
```
|
||||
alias perf=$(find /usr/lib/linux-tools/*/perf | head -1)
|
||||
```
|
||||
|
||||
- If you're a user and would like to prefer to link `/usr/local/bin/perf`
|
||||
|
||||
```
|
||||
sudo ln -s "$(find /usr/lib/linux-tools/*/perf | head -1)" /usr/local/bin/perf
|
||||
```
|
||||
|
||||
After this, you should be able to use `perf stat -- sleep 0.1` (if you're already `root`) or `sudo perf stat -- sleep 0.1` successfully.
|
||||
|
||||
### Bare metal
|
||||
|
||||
For a bare metal Linux machine, all you need to do is to install `perf` which should then work in full fidelity.
|
||||
|
||||
### In Docker (running on bare-metal Linux)
|
||||
|
||||
You will need to launch your container with `docker run --privileged` (don't run this in production) and then you should have full access to perf (including the PMU).
|
||||
|
||||
To validate that `perf` works correctly, run for example `perf stat -- sleep 0.1`. Whether you'll see the `<not supported>` next to some information will depend on if you have access to the CPU's performance counters (the PMU). In Docker on bare metal, this should work, ie. no `<not supported>`s should show up.
|
||||
|
||||
### Docker for Mac
|
||||
|
||||
Docker for Mac is like Docker on bare metal but with some extra complexity because we're actually running the Docker containers hosted in a Linux VM. So matching the kernel version will be difficult.
|
||||
|
||||
If you follow the above installation instructions, you should nevertheless get `perf` to work but you won't have access to the CPU's performance counters (the PMU) so you'll see a few events show up as `<not supported>`.
|
||||
|
||||
```
|
||||
$ perf stat -- sleep 0.1
|
||||
|
||||
Performance counter stats for 'sleep 0.1':
|
||||
|
||||
0.44 msec task-clock # 0.004 CPUs utilized
|
||||
1 context-switches # 0.002 M/sec
|
||||
0 cpu-migrations # 0.000 K/sec
|
||||
57 page-faults # 0.129 M/sec
|
||||
<not supported> cycles
|
||||
<not supported> instructions
|
||||
<not supported> branches
|
||||
<not supported> branch-misses
|
||||
|
||||
0.102869000 seconds time elapsed
|
||||
|
||||
0.000000000 seconds user
|
||||
0.001069000 seconds sys
|
||||
```
|
||||
|
||||
### In a VM
|
||||
|
||||
In a virtual machine, you would install `perf` just like on bare metal. And either `perf` will work just fine with all its features or it will look similarly to what you get on Docker for Mac.
|
||||
|
||||
What you need your hypervisor to support (& allow) is "PMU passthrough" or "PMU virtualisation". VMware Fusion does support PMU virtualisation which they call vPMC (VM settings -> Processors & Memory -> Advanced -> Allow code profiling applications in this VM). If you're on a Mac this setting is unfortunately only supported up to including macOS Catalina (and [not on Big Sur](https://kb.vmware.com/s/article/81623)).
|
||||
|
||||
If you use `libvirt` to manage your hypervisor and VMs, you can use `sudo virsh edit your-domain` and replace the `<cpu .../>` XML tag with
|
||||
|
||||
<cpu mode='host-passthrough' check='none'/>
|
||||
|
||||
to allow the PMU to be passed through to the guest. For other hypervisors, an internet search will usually reveal how to enable PMU passthrough.
|
||||
@@ -1,10 +1,6 @@
|
||||
# Library guidelines: Log Levels
|
||||
# Log Levels
|
||||
|
||||
This guide serves as guidelines for library authors with regard to what [SwiftLog](https://github.com/apple/swift-log) log levels are appropriate for use in libraries, and in what situations to use what level.
|
||||
|
||||
Libraries need to be well-behaved across various use cases, and cannot assume a specific style of logging backend will be used with them. It is up to developers implementing specific applications and systems to configure those specifics of their application, and some may choose to log to disk, some to memory, or some may employ sophisticated log aggregators. In all those cases a library should behave "well", meaning that it should not overwhelm typical ("stdout") log backends by logging too much, alerting too much by over-using `error` level log statements etc.
|
||||
|
||||
This is aimed for library authors with regards to what [SwiftLog](https://github.com/apple/swift-log) log levels are appropriate for use in libraries, and also general logging style hints.
|
||||
This guide serves as guidelines for library authors with regards to what [SwiftLog](https://github.com/apple/swift-log) log levels are appropriate for use in libraries, and in what situations to use what level.
|
||||
|
||||
## Guidelines for Libraries
|
||||
|
||||
@@ -18,15 +14,14 @@ SwiftLog defines the following 7 log levels via the [`Logger.Level` enum](https:
|
||||
* `error`
|
||||
* `critical`
|
||||
|
||||
Out of those, only levels _less severe than_ info (exclusively) are generally okay to be used by libraries.
|
||||
|
||||
Out of those, only levels _less severe than_ info (exclusive) are generally okay to be used by libraries.
|
||||
In the following section we'll explore how to use them in practice.
|
||||
|
||||
### Recommended log levels
|
||||
|
||||
It is always fine for a library to log at `trace` and `debug` levels, and these two should be the primary levels any library is logging at.
|
||||
|
||||
`trace` is the finest log level, and end-users of a library will not usually use it unless debugging very specific issues. You should consider it as a way for library developers to "log everything we could possibly need to diagnose a hard to reproduce bug." Unrestricted logging at `trace` level may take a toll on the performance of a system, and developers can assume trace level logging will not be used in production deployments, unless enabled specifically to locate some specific issue.
|
||||
`trace` is the finest log level, and end-users of a library will not usually use it unless debugging very specific issues. You should consider it as a way for library developers to "log everything we could possibly need to diagnose a hard to reproduce bug." It is expected to take a toll on the performance of a system, and developers can assume trace level logging will not be used in production deployments, unless enabled specifically to locate some specific issue.
|
||||
|
||||
This is in contrast with `debug` which some users _may_ choose to run enabled on their production systems.
|
||||
|
||||
@@ -34,33 +29,32 @@ This is in contrast with `debug` which some users _may_ choose to run enabled on
|
||||
>
|
||||
> Debug level logging should not completely undermine the performance of a production system.
|
||||
|
||||
As such, `debug` logging should provide a high value understanding of what is going on in the library for end users, using domain relevant language. Logging at `debug` level should not be overly noisy or dive deep into internals; this is what `trace` is intended for.
|
||||
As such, `debug` logging should not be overly noisy. It should provide a high value understanding of what is going on in the library for end users without diving deep into its internals. This is what `trace` is intended for.
|
||||
|
||||
Use `warning` level sparingly. Whenever possible, try to rather return or throw `Error` to end users that are descriptive enough so they can inspect, log them and figure out the issue. Potentially, they may then enable debug logging to find out more about the issue.
|
||||
Use `warning` level sparingly. Whenever possible, try to rather return or throw `Error` to end users that are descriptive enough so they can inspect, log them and figure out the issue. Potentially they may then enable debug logging to find out more about the issue.
|
||||
|
||||
It is okay to log a `warning` "once", for example on system startup. This may include some one off "more secure configuration is available, try upgrading to it!" log statement upon a server's startup. You may also log warnings from background processes, which otherwise have no other means of informing the end user about some issue.
|
||||
|
||||
Logging on `error` level is similar to warnings: prefer to avoid doing so whenever possible. Instead, report errors via your library's API. For example, it is _not_ a good idea to log "connection failed" from an HTTP client. Perhaps the end-user intended to make this request to a known offline server to _confirm_ it is offline? From their perspective, this connection error is not a "real" error, it is just what they expected -- as such the HTTP client should return or throw such an error, but _not_ log it.
|
||||
Logging on `error` level is similar to warnings: prefer to avoid doing so whenever possible. Instead, report errors via your library's API. For example, it is _not_ a good idea to log "connection failed" from an HTTP client. Perhaps the end-user intended to make this request to a known offline server to _confirm_ it is offline? From their perspective this connection error is not a "real" error, it is just what they expected -- as such the HTTP client should return or throw such an error, but _not_ log it.
|
||||
|
||||
Do also note that in situations when you decide to log an error, be mindful of error rates. Will this error potentially be logged for every single operation while some network failure is happening? Some teams and companies have alerting systems set up based on the rate of errors logged in a system, and if it exceeds some threshold it may start calling and paging people in the middle of the night. When logging at error level, consider if the issue indeed is something that should be waking up people at night. You may also want to consider offering configuration in your library: "at what log level should this issue be reported?" This can come in handy in clustered systems which may log network failures themselves, or depend on external systems detecting and reporting this.
|
||||
|
||||
Logging `critical` logs is allowed for libraries, however as the name implies - only in the most critical situations. Most often this implies that the library will *stop functioning* after such log has been issued. End users are thought to expect that a logged critical error is _very_ important, and they may have set up their systems to page people in the middle of the night to investigate the production system _right now_ when such log statements are detected. So please be careful about logging these kinds of errors.
|
||||
|
||||
Some libraries and situations may not be entirely clear with regard to what log level is "best" for them. In such situations, it sometimes is worth it to allow the end-users of the library to be able to configure the levels of specific groups of messages. You can see this in action in the Soto library [here](https://github.com/soto-project/soto-core/pull/423/files#diff-4a8ca7e54da5b22287900dd8cf6b47ded38a94194c1f0b544119030c81a2f238R649) where an `Options` object allows end users to configure the level at which requests are logged (`options.requestLogLevel`) which is then used as `log.log(self.options.requestLogLevel)`.
|
||||
Some libraries and situations may not be entirely clear with regards to what log level is "best" for them. In such situations, it sometimes is worth it to allow the end-users of the library to be able to configure the levels of specific groups of messages. You can see this in action in the Soto library [here](https://github.com/soto-project/soto-core/pull/423/files#diff-4a8ca7e54da5b22287900dd8cf6b47ded38a94194c1f0b544119030c81a2f238R649) where an `Options` object allows end users to configure the level at which requests are logged (`options.requestLogLevel`) which is then used as `log.log(self.options.requestLogLevel)`.
|
||||
|
||||
#### Examples
|
||||
|
||||
`trace` level logging:
|
||||
|
||||
- Could include various additional information about a request, such as various diagnostics about created data structures, the state of caches or similar, which are created in order to serve a request.
|
||||
- Could include "begin operation" and "end operation" logging statements.
|
||||
- Could include "begin operation" and "end operation" logs.
|
||||
- However, please consider using [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing) to instrument "begin" and "end" events, as tracing can be more efficient than logging. Logging may need to create string representations of the durations, log levels, and other fields.
|
||||
|
||||
`debug` level logging:
|
||||
|
||||
- May include a single log statement for opening a connection, accepting a request, and so on.
|
||||
- It can include a _high level_ overview of control flow in an operation. For example: "started work, processing step X, made X decision, finished work X, result code 200". This overview may consist of high cardinality structured data.
|
||||
|
||||
> You may also want to consider using [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing) to instrument "begin" and "end" events, as tracing may give you additional insights into your system behavior you would have missed with just manually analysing log statements.
|
||||
- It can include a _high level_ overview of what is going on in the library. For example: "started work, processing step X, finished work X, result code 200".
|
||||
|
||||
### Log levels to avoid
|
||||
|
||||
@@ -110,11 +104,11 @@ And a minor yet important hint: avoid inserting newlines and other control chara
|
||||
|
||||
#### Structured Logging (Semantic Logging)
|
||||
|
||||
Libraries may want to embrace the structured logging style, which renders logs in a [semi-structured data format](https://en.wikipedia.org/wiki/Semi-structured_data).
|
||||
Libraries may want to embrace the structured logging style.
|
||||
|
||||
It is a fantastic pattern which makes it easier and more reliable for automated code to process logged information.
|
||||
It is a fantastic pattern which makes consuming logs in automated systems, and through grepping and other means much easier and future proof.
|
||||
|
||||
Consider the following "not structured" log statement:
|
||||
Consider the following not structured log statement:
|
||||
|
||||
```swift
|
||||
// NOT structured logging style
|
||||
@@ -143,7 +137,7 @@ log.info("Accepted connection", metadata: [
|
||||
// <date> info [connection.id:?,connection.peer:?, connections.total:?] Accepted connection
|
||||
```
|
||||
|
||||
This structured log can be formatted, depending on the logging backend, slightly differently on various systems. Even in the simple string representation of such a log, we'd be able to grep for `connections.total: 100` rather than having to guess the correct string.
|
||||
This structured log can be formatted, depending on the logging backend, slightly differently on various systems. Even in the simple string representation of such log, we'd be able to grep for `connections.total: 100` rather than having to guess the correct string.
|
||||
|
||||
Also, since the message now does not contain all that much "human readable wording", it is less prone to randomly change from "Accepted" to "We have accepted" or vice versa. This kind of change could break alerting systems which are set up to parse and alert on specific log messages.
|
||||
|
||||
@@ -208,8 +202,8 @@ These are only general guidelines, and there always will be exceptions to these
|
||||
|
||||
Here are a few examples of situations when logging a message on a relatively high level might still be tolerable for a library.
|
||||
|
||||
It's permissible for a library to log at `critical` level right before a _hard_ crash of the process, as a last resort of informing the log collection systems or end-user about additional information detailing the reason for the crash. This should be _in addition to_ the message from a `fatalError` and can lead to an improved diagnosis/debugging experience for end users.
|
||||
It's permissible for a library to log at `critical` level right before a _hard crash_, as a last resort of informing the log collection systems or end-user about additional information detailing the reason for the crash. This should be _in addition to_ the message from a `fatalError` and can lead to an improved diagnosis/debugging experience for end users.
|
||||
|
||||
Sometimes libraries may be able to detect a harmful misconfiguration of the library. For example, selecting deprecated protocol versions. In such situations it may be useful to inform users in production by issuing a `warning`. However you should ensure that the warning is not logged repeatedly! For example, it is not acceptable for an HTTP client to log a warning on every single http request using some misconfiguration of the client. It _may_ be acceptable however for the client to log such a warning, for example, _once_ at configuration time, if the library has a good way to do this.
|
||||
Sometimes libraries may be able to detect a harmful misconfiguration of the library. For example, selecting deprecated protocol versions. In such situations it may be useful to inform users in production by issuing a `warning`. However you should ensure that the warning is not logged repeatedly! For example, it is not acceptable for an HTTP client to log a warning on every single http request using some misconfiguration of the client. It _may_ be acceptable however for the client to log such warning, for example, _once_ at configuration time, if the library has a good way to do this.
|
||||
|
||||
Some libraries may implement a "log this warning only once", "log this warning only at startup", "log this error only once an hour", or similar tricks to keep the noise level low but still informative enough to not be missed. This is, however, usually a pattern reserved for stateful long running libraries, rather than clients of databases and related persistent stores.
|
||||
@@ -12,7 +12,7 @@ Here is an example `Dockerfile` that builds and packages the application on top
|
||||
|
||||
```Dockerfile
|
||||
#------- build -------
|
||||
FROM swift:centos8 as builder
|
||||
FROM swiftlang/swift:nightly-centos8 as builder
|
||||
|
||||
# set up the workspace
|
||||
RUN mkdir /workspace
|
||||
@@ -21,12 +21,14 @@ WORKDIR /workspace
|
||||
# copy the source to the docker image
|
||||
COPY . /workspace
|
||||
|
||||
RUN swift build -c release --static-swift-stdlib
|
||||
RUN swift build -c release
|
||||
|
||||
#------- package -------
|
||||
FROM centos
|
||||
FROM centos:8
|
||||
# copy executables
|
||||
COPY --from=builder /workspace/.build/release/<executable-name> /
|
||||
# copy Swift's dynamic libraries dependencies
|
||||
COPY --from=builder /usr/lib/swift/linux/lib*so* /
|
||||
|
||||
# set the entry point (application name)
|
||||
CMD ["<executable-name>"]
|
||||
@@ -64,7 +66,7 @@ Since distroless supports Docker and is based on Debian, packaging a Swift appli
|
||||
```Dockerfile
|
||||
#------- build -------
|
||||
# Building using Ubuntu Bionic since its compatible with Debian runtime
|
||||
FROM swift:bionic as builder
|
||||
FROM swiftlang/swift:nightly-bionic as builder
|
||||
|
||||
# set up the workspace
|
||||
RUN mkdir /workspace
|
||||
@@ -73,7 +75,7 @@ WORKDIR /workspace
|
||||
# copy the source to the docker image
|
||||
COPY . /workspace
|
||||
|
||||
RUN swift build -c release --static-swift-stdlib
|
||||
RUN swift build -c release
|
||||
|
||||
#------- package -------
|
||||
# Running on distroless C++ since it includes
|
||||
@@ -81,6 +83,8 @@ RUN swift build -c release --static-swift-stdlib
|
||||
FROM gcr.io/distroless/cc-debian10
|
||||
# copy executables
|
||||
COPY --from=builder /workspace/.build/release/<executable-name> /
|
||||
# copy Swift's dynamic libraries dependencies
|
||||
COPY --from=builder /usr/lib/swift/linux/lib*so* /
|
||||
|
||||
# set the entry point (application name)
|
||||
CMD ["<executable-name>"]
|
||||
@@ -102,22 +106,23 @@ First, use the `docker run` command from the application's source location to bu
|
||||
$ docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-w /workspace \
|
||||
swift:bionic \
|
||||
/bin/bash -cl "swift build -c release --static-swift-stdlib"
|
||||
swift:5.2-bionic \
|
||||
/bin/bash -cl "swift build -c release"
|
||||
```
|
||||
|
||||
Note we are bind mounting the source directory so that the build writes the build artifacts to the local drive from which we will package them later.
|
||||
|
||||
Next we can create a staging area with the application's executable:
|
||||
Next we can create a staging area with the application's executables and Swift's dynamic libraries dependencies:
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-w /workspace \
|
||||
swift:bionic \
|
||||
swift:5.2-bionic \
|
||||
/bin/bash -cl ' \
|
||||
rm -rf .build/install && mkdir -p .build/install && \
|
||||
cp -P .build/release/<executable-name> .build/install/'
|
||||
cp -P .build/release/<executable-name> .build/install/ && \
|
||||
cp -P /usr/lib/swift/linux/lib*so* .build/install/'
|
||||
```
|
||||
|
||||
Note this command could be combined with the build command above--we separated them to make the example more readable.
|
||||
@@ -132,7 +137,7 @@ We can test the integrity of the tarball by extracting it to a directory and run
|
||||
|
||||
```bash
|
||||
$ cd <extracted directory>
|
||||
$ docker run -v "$PWD:/app" -w /app bionic ./<executable-name>
|
||||
$ docker run -v "$PWD:/app" -w /app swift:5.2-bionic-slim ./<executable-name>
|
||||
```
|
||||
|
||||
Deploying the application's tarball to the target server can be done using utilities like `scp`, or in a more sophisticated setup using configuration management system like `chef`, `puppet`, `ansible`, etc.
|
||||
@@ -149,7 +154,3 @@ The main advantage of this approach is that it is easy. Additional advantage is
|
||||
The main disadvantage of this approach that the server has the full toolchain (e.g. compiler) which means a sophisticated attacker can potentially find ways to execute code. They can also potentially gain access to the source code which might be sensitive. If the application code needs to be cloned from a private or protected repository, the server needs access to credentials which adds additional attack surface area.
|
||||
|
||||
In most cases, source distribution is not advised due to these security concerns.
|
||||
|
||||
## Static linking and Curl/XML
|
||||
|
||||
**Note:** if you are compiling with `-static-stdlib` and using Curl with FoundationNetworking or XML with FoundationXML you must have libcurl and/or libxml2 installed on the target system for it to work.
|
||||
@@ -103,8 +103,6 @@ for f in 0..<2_000 {
|
||||
|
||||
The above program contains the `TerribleArray` data structure which has _O(n)_ appends and not the amortised _O(1)_ that users are used to from `Array`.
|
||||
|
||||
We will assume, that you have Linux's `perf` installed and configured, documentation on how to install `perf` can be found in [this guide](linux-perf.md).
|
||||
|
||||
Let's assume we have compiled the above code using `swift build -c release` into a binary called `./slow`. We also assume that the `https://github.com/brendangregg/FlameGraph` repository is cloned in `~/FlameGraph`:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
## Installing Swift
|
||||
## Setting up
|
||||
|
||||
The [supported platforms](https://swift.org/platform-support/) for running Swift on the server and the [ready-built tools packages](https://swift.org/download/) are all hosted on swift.org together with installation instructions. There you also can find the [language reference documentation](https://swift.org/documentation/).
|
||||
|
||||
## IDEs/Editors with Swift Support
|
||||
## IDE and Swift-aware editor alternatives
|
||||
|
||||
A number of editors you may already be familiar with have support for writing Swift code. Here we provide a non-exhaustive list of such editors and relevant plugins/extensions, sorted alphabetically.
|
||||
There are a few different options for doing the actual coding with completions etc depending on what platform you are using, a non-exhastive list:
|
||||
|
||||
* [Atom IDE support](https://atom.io/packages/ide-swift)
|
||||
* [Atomic Blonde](https://atom.io/packages/atomic-blonde) a SourceKit based syntax highlighter for Atom.
|
||||
[Atom IDE support](https://atom.io/packages/ide-swift)
|
||||
|
||||
* [CLion](https://www.jetbrains.com/help/clion/swift.html)
|
||||
[CLion](https://www.jetbrains.com/help/clion/swift.html)
|
||||
|
||||
* [Emacs plugin](https://github.com/swift-emacs/swift-mode)
|
||||
[Emacs plugin](https://github.com/swift-emacs/swift-mode)
|
||||
|
||||
* [VIM plugin](https://github.com/keith/swift.vim)
|
||||
[VIM plugin](https://github.com/keith/swift.vim)
|
||||
|
||||
* [Visual Studio Code](https://code.visualstudio.com)
|
||||
* [Swift for Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)
|
||||
[Visual Studio Code](https://code.visualstudio.com)
|
||||
|
||||
* [Xcode](https://developer.apple.com/xcode/ide/)
|
||||
[Visual Studio Code optional code completion plugin](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swift-development-environment)
|
||||
|
||||
## Language Server Protocol (LSP) Support
|
||||
[Xcode](https://developer.apple.com/xcode/ide/)
|
||||
|
||||
The [SourceKit-LSP project](https://github.com/apple/sourcekit-lsp) provides a Swift implementation of the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/), which provides features such as code completion and jump-to-definition in supported editors.
|
||||
## Support for other editors using LSP
|
||||
|
||||
The project has both an [extensive list of editors that support it](https://github.com/apple/sourcekit-lsp/tree/main/Editors) and setup instructions for those editors, including many of those listed above.
|
||||
Additionally, the [SourceKit-LSP project](https://github.com/apple/sourcekit-lsp) has both an [extensive list of editors that supports it](https://github.com/apple/sourcekit-lsp/tree/main/Editors) and instructions on how to get completion in your favourite editor.
|
||||
|
||||
_Do you know about another IDE or IDE plugin that is missing? Please submit a PR to add them here!_
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Testing
|
||||
|
||||
SwiftPM is integrated with [XCTest, Apple’s unit test framework](https://developer.apple.com/documentation/xctest). Running `swift test` from the terminal, or triggering the test action in your IDE (Xcode or similar), will run all of your XCTest test cases. Test results will be displayed in your IDE or printed out to the terminal.
|
||||
Running `swift test` from the terminal will trigger the unit tests (or triggering the test action in Xcode).
|
||||
|
||||
A convenient way to test on Linux is using Docker. For example:
|
||||
SwiftPM is integrated with [XCTest, Apple’s unit test framework](https://developer.apple.com/documentation/xctest). Test results will be displayed in Xcode or printed out to the terminal.
|
||||
|
||||
Like building on Linux, testing on Linux requires the use of Docker. For example:
|
||||
|
||||
`$ docker run -v "$PWD:/code" -w /code swift:latest swift test`
|
||||
|
||||
@@ -12,7 +14,7 @@ Swift supports architecture-specific code. By default, Foundation imports archit
|
||||
|
||||
A historically important detail about testing for Linux is the `Tests/LinuxMain.swift` file.
|
||||
|
||||
- In Swift versions 5.4 and newer tests are automatically discovered on all platforms, no special file or flag needed.
|
||||
- In Swift versions 5.4 and newer tests are automaticlaly discovered on all platforms, no special file or flag needed.
|
||||
- In Swift versions >= 5.1 < 5.4, tests can be automaticlaly discovered on Linux using `swift test --enable-test-discovery` flag.
|
||||
- In Swift versions older than 5.1 the `Tests/LinuxMain.swift` file provides SwiftPM an index of all the tests it needs to run on Linux and it is critical to keep this file up-to-date as you add more unit tests. To regenerate this file, run `swift test --generate-linuxmain` after adding tests. It is also a good idea to include this command as part of your continuous integration setup.
|
||||
|
||||
@@ -20,7 +22,7 @@ A historically important detail about testing for Linux is the `Tests/LinuxMain.
|
||||
|
||||
- For Swift versions between Swift 5.1 and 5.4, always test with `--enable-test-discovery` to avoid forgetting tests on Linux.
|
||||
|
||||
- Make use of the sanitizers. Before running code in production, and preferably as a regular part of your CI process, do the following:
|
||||
- Make use of the sanitizers. Before running code in production, do the following:
|
||||
* Run your test suite with TSan (thread sanitizer): `swift test --sanitize=thread`
|
||||
* Run your test suite with ASan (address sanitizer): `swift test --sanitize=address` and `swift test --sanitize=address -c release -Xswiftc -enable-testing`
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 998 KiB |
|
Before Width: | Height: | Size: 619 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 912 KiB |