287 lines
8.1 KiB
Markdown
287 lines
8.1 KiB
Markdown
Seastar
|
||
=======
|
||
|
||
Introduction
|
||
------------
|
||
|
||
SeaStar is an event-driven framework allowing you to write non-blocking,
|
||
asynchronous code in a relatively straightforward manner (once understood).
|
||
It is based on [futures](http://en.wikipedia.org/wiki/Futures_and_promises).
|
||
|
||
Building Seastar
|
||
--------------------
|
||
|
||
### Building seastar on Fedora 20
|
||
|
||
Installing GCC 4.9 for gnu++1y:
|
||
* Beware that this installation will replace your current GCC version.
|
||
```
|
||
yum install fedora-release-rawhide
|
||
yum --enablerepo rawhide update gcc-c++
|
||
yum --enablerepo rawhide install libubsan libasan
|
||
```
|
||
|
||
Installing required packages:
|
||
```
|
||
yum install libaio-devel ninja-build ragel hwloc-devel numactl-devel libpciaccess-devel cryptopp-devel
|
||
```
|
||
|
||
You then need to run the following to create the "build.ninja" file:
|
||
```
|
||
./configure.py
|
||
```
|
||
Note it is enough to run this once, and you don't need to repeat it before
|
||
every build. build.ninja includes a rule which will automatically re-run
|
||
./configure.py if it changes.
|
||
|
||
Then finally:
|
||
```
|
||
ninja-build
|
||
```
|
||
|
||
### Building seastar on Ubuntu 14.04
|
||
|
||
Installing required packages:
|
||
```
|
||
sudo apt-get install libaio-dev ninja-build ragel libhwloc-dev libnuma-dev libpciaccess-dev libcrypto++-dev libboost-all-dev
|
||
```
|
||
|
||
Installing GCC 4.9 for gnu++1y. Unlike the Fedora case above, this will
|
||
not harm the existing installation of GCC 4.8, and will install an
|
||
additional set of compilers, and additional commands named gcc-4.9,
|
||
g++-4.9, etc., that need to be used explictly, while the "gcc", "g++",
|
||
etc., commands continue to point to the 4.8 versions.
|
||
|
||
```
|
||
# Install add-apt-repository
|
||
sudo apt-get install software-properties-common python-software-properties
|
||
# Use it to add Ubuntu's testing compiler repository
|
||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
|
||
sudo apt-get update
|
||
# Install gcc 4.9 and relatives
|
||
sudo apt-get install g++-4.9
|
||
# Also set up necessary header file links and stuff (?)
|
||
sudo apt-get install gcc-4.9-multilib g++-4.9-multilib
|
||
```
|
||
|
||
To compile Seastar explicitly using gcc 4.9, use:
|
||
```
|
||
./configure.py --compiler=g++-4.9
|
||
```
|
||
|
||
To compile OSv explicitly using gcc 4.9, use:
|
||
```
|
||
make CC=gcc-4.9 CXX=g++-4.9 -j 24
|
||
```
|
||
|
||
### Building seastar in Docker container
|
||
|
||
To build a Docker image:
|
||
|
||
```
|
||
docker build -t seastar-dev .
|
||
```
|
||
|
||
To launch a container:
|
||
|
||
```
|
||
$ docker run -v $HOME/seastar/:/seastar -i -t seastar-dev /bin/bash
|
||
```
|
||
|
||
Finally, to build seastar inside the container:
|
||
|
||
```
|
||
cd /seastar
|
||
ninja-build
|
||
```
|
||
|
||
### Building with a DPDK network backend
|
||
|
||
1. Setup host to compile DPDK:
|
||
- Ubuntu
|
||
```
|
||
sudo apt-get install -y build-essential linux-image-extra-`uname -r`
|
||
```
|
||
2. Prepare a DPDK SDK:
|
||
- Download the latest DPDK release: `wget http://dpdk.org/browse/dpdk/snapshot/dpdk-1.7.1.tar.gz`
|
||
- Untar it.
|
||
- Start the tools/setup.sh script as root.
|
||
- Compile a linuxapp target (option 8).
|
||
- Install IGB_UIO module (option 10).
|
||
- Bind some physical port to IGB_UIO (option 16).
|
||
- Configure hugepage mappings (option 13/14).
|
||
3. Run a configure.py: `./configure.py --dpdk-target <Path to untared dpdk-1.7.1 above>/x86_64-native-linuxapp-gcc --compiler=g++-4.9`.
|
||
4. Run `ninja-build`.
|
||
|
||
To run with the DPDK backend for a native stack give the seastar application `--dpdk-pmd 1` parameter.
|
||
|
||
Futures and promises
|
||
--------------------
|
||
|
||
A *future* is a result of a computation that may not be available yet.
|
||
Examples include:
|
||
|
||
* a data buffer that we are reading from the network
|
||
* the expiration of a timer
|
||
* the completion of a disk write
|
||
* the result computation that requires the values from
|
||
one or more other futures.
|
||
|
||
a *promise* is an object or function that provides you with a future,
|
||
with the expectation that it will fulfill the future.
|
||
|
||
Promises and futures simplify asynchronous programming since they decouple
|
||
the event producer (the promise) and the event consumer (whoever uses the
|
||
future). Whether the promise is fulfilled before the future is consumed,
|
||
or vice versa, does not change the outcome of the code.
|
||
|
||
Consuming a future
|
||
------------------
|
||
|
||
You consume a future by using its *then()* method, providing it with a
|
||
callback (typically a lambda). For example, consider the following
|
||
operation:
|
||
|
||
```C++
|
||
future<int> get(); // promises an int will be produced eventually
|
||
future<> put(int) // promises to store an int
|
||
|
||
void f() {
|
||
get().then([] (int value) {
|
||
put(value + 1).then([] {
|
||
std::cout << "value stored successfully\n";
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
Here, we initate a *get()* operation, requesting that when it completes, a
|
||
*put()* operation will be scheduled with an incremented value. We also
|
||
request that when the *put()* completes, some text will be printed out.
|
||
|
||
Chaining futures
|
||
----------------
|
||
|
||
If a *then()* lambda returns a future (call it x), then that *then()*
|
||
will return a future (call it y) that will receive the same value. This
|
||
removes the need for nesting lambda blocks; for example the code above
|
||
could be rewritten as:
|
||
|
||
```C++
|
||
future<int> get(); // promises an int will be produced eventually
|
||
future<> put(int) // promises to store an int
|
||
|
||
void f() {
|
||
get().then([] (int value) {
|
||
return put(value + 1);
|
||
}).then([] {
|
||
std::cout << "value stored successfully\n";
|
||
});
|
||
}
|
||
```
|
||
|
||
Loops
|
||
-----
|
||
|
||
Loops are achieved with a tail call; for example:
|
||
|
||
```C++
|
||
future<int> get(); // promises an int will be produced eventually
|
||
future<> put(int) // promises to store an int
|
||
|
||
future<> loop_to(int end) {
|
||
if (value == end) {
|
||
return make_ready_future<>();
|
||
}
|
||
get().then([end] (int value) {
|
||
return put(value + 1);
|
||
}).then([end] {
|
||
return loop_to(end);
|
||
});
|
||
}
|
||
```
|
||
|
||
The *make_ready_future()* function returns a future that is already
|
||
available --- corresponding to the loop termination condition, where
|
||
no further I/O needs to take place.
|
||
|
||
Under the hood
|
||
--------------
|
||
|
||
When the loop above runs, both *then* method calls execute immediately
|
||
--- but without executing the bodies. What happens is the following:
|
||
|
||
1. `get()` is called, initiates the I/O operation, and allocates a
|
||
temporary structure (call it `f1`).
|
||
2. The first `then()` call chains its body to `f1` and allocates
|
||
another temporary structure, `f2`.
|
||
3. The second `then()` call chains its body to `f2`.
|
||
|
||
Again, all this runs immediately without waiting for anything.
|
||
|
||
After the I/O operation initiated by `get()` completes, it calls the
|
||
continuation stored in `f1`, calls it, and frees `f1`. The continuation
|
||
calls `put()`, which initiates the I/O operation required to perform
|
||
the store, and allocates a temporary object `f12`, and chains some glue
|
||
code to it.
|
||
|
||
After the I/O operation initiated by `put()` completes, it calls the
|
||
continuation associated with `f12`, which simply tells it to call the
|
||
continuation assoicated with `f2`. This continuation simply calls
|
||
`loop_to()`. Both `f12` and `f2` are freed. `loop_to()` then calls
|
||
`get()`, which starts the process all over again, allocating new versions
|
||
of `f1` and `f2`.
|
||
|
||
Handling exceptions
|
||
-------------------
|
||
|
||
If a `.then()` clause throws an exception, the scheduler will catch it
|
||
and cancel any dependent `.then()` clauses. If you want to trap the
|
||
exception, add a `.rescue()` clause at the end:
|
||
|
||
```C++
|
||
future<buffer> receive();
|
||
request parse(buffer buf);
|
||
future<response> process(request req);
|
||
future<> send(response resp);
|
||
|
||
void f() {
|
||
receive().then([] (buffer buf) {
|
||
return process(parse(std::move(buf));
|
||
}).then([] (response resp) {
|
||
return send(std::move(resp));
|
||
}).then([] {
|
||
f();
|
||
}).rescue([] (auto get_ex) {
|
||
try {
|
||
get_ex();
|
||
} (catch std::exception& e) {
|
||
// your handler goes here
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
When the `get_ex` variable is called as a function, it will rethrow
|
||
the exception that aborted processing, and you can then apply any
|
||
needed error handling. It is essentially a transformation of
|
||
|
||
```C++
|
||
buffer receive();
|
||
request parse(buffer buf);
|
||
response process(request req);
|
||
void send(response resp);
|
||
|
||
void f() {
|
||
try {
|
||
while (true) {
|
||
auto req = parse(receive());
|
||
auto resp = process(std::move(req));
|
||
send(std::move(resp));
|
||
}
|
||
} catch (std::exception& e) {
|
||
// your handler goes here
|
||
}
|
||
}
|
||
```
|