Crates.io | pyportus |
lib.rs | pyportus |
version | 0.6.0 |
source | src |
created_at | 2018-10-30 19:54:58.014863 |
updated_at | 2022-11-09 20:10:17.449407 |
description | A Congestion Control Plane |
homepage | https://ccp-project.github.io |
repository | https://github.com/ccp-project/portus |
max_upload_size | |
id | 93665 |
size | 32,725 |
This module provides a python interface to the Portus CCP implementation.
These python bindings are available on pyPI, pip install pyportus
.
To build manually, this project uses maturin.
To test:
maturin develop
To install:
maturin build
pip install
the resulting .whlTo then run/test, cd examples && ./<venv>/python3 aimd.py
(may need to run as root)
An algorithm in portus is represented by a Python class and an instance of this class represents a single TCP flow. A new instance is created for each flow.
This class must be a subclass of portus.AlgBase
and must implement the two
following method signatures:
on_create(self)
on_report(self, r)
r
is a Report object containing all the fields defined in your datapath program, as well as the current Cwnd
and Rate
. Suppose your program defines just a single variable: (def (acked 0))
, where acked
adds up the total bytes acked since the last report. This value can be accessed as r.acked
. Similarly, you can access the cwnd or rate as r.Cwnd
and r.Rate
(captialization important!).Each instantiation of the class will automatically have two fields inside self:
self.datapath
is a pointer to the datapath object that can be used to install
new datapath programs. It has two available methods:
datapath.install( str )
, which takes a datapath program as a string. It compiles the program and installs it in the datapath. It does not return anything, though it may raise an exception if your program fails to compile.datapath.update_field(field, val)
, which takes a variable in the Report
scope of your datapath program and sets the value to val
. For example, to update just the cwnd, you could use datapath.update_field("Cwnd", 10000)
(note: cwnd is denoted in bytes, not packets).self.datapath_info
is a struct containing fields about this particular flow from the datapath (this could be used, for example, in on_create
to set an initial cwnd based on the datapath's mss
)
sock_id
: unique id of this flow in the datapath
init_cwnd
: the initial congestion window this flow will have until you set it
src_ip
, src_port
, dst_ip
, dst_port
: the ip address and port of the source and destination for the flow
Datapath programs are used to (1) define which statistics to send back to your usespace program and how often and (2) set the congestion window and/or pacing rate. A datapath program is written in a very simple lisp-like dialect and consists of a single variable definition line followed by any number of when clauses:
(def ( ... ) ( ... ))
(when (event) (
do_stuff ...
)
(when (other_event) (
do_other_stuff ...
)
updated
Example: (def (Report.acked 0) (Report.rtt 0) (Report.timeout false))
This line defines the names and initial values of variables in the report scope. Calling (report)
in your datapath program results in a call to your algorithm's on_report
function with the current value of these variables. After the call these variables are reset back to their initial value.
NOTE: Variables in datapath programs are written as {scope}.{name}
. For example, the acked
variable in the Report
scope is written as Report.acked
. Therefore, all variables defined in this line must start with Report.
However, when you access them in on_report
, you just provide the variable name. In our example above, Report.rtt
defines the variable rtt
in the Report
scope. If we want to access this value in on_report(r)
, we'd use r.rtt
(i.e. not r.Report.rtt
).
When clauses consist of a boolean expression and a set of instructions. On each ack, the datapath checks the boolean expression, and if it evaluates to true
, it runs the set of instructions. For example, the following when clause sends a report (i.e. calls the on_report
function) once every rtt:
(when (> Micros Flow.rtt_sample_us)
(report)
)
A sample algorithm definition showing the full API:
import portus
# Class must sublcass portus.AlgBase
class SampleCCAlg(portus.AlgBase):
# Init must take exactly these parameters
def __init__(self, datapath, datapath_info):
# Store a copy of the datapath and info for later
self.datapath = datapath
self.datapath_info = datapath_info
# Internally store an initial cwnd value
self.cwnd = 10 * self.datapath_info.mss
# Install an initial datapath program to keep track of the RTT and report it once per RTT
# The first when clause is true on every single ack,
# which means the 'Report.rtt' field will always keep the latest rtt sample
# The second when clause is true once one rtt's worth of time has passed,
# at which point it will trigger on_report, and Micros (and Report.rtt) will be reset to 0
self.datapath.install("""\
(def
(Report.rtt 0)
)
(when true
(:= Report.rtt Flow.rtt_sample_us)
(fallthrough)
)
(when (> Micros Flow.rtt_sample_us)
(report)
)
""")
# This function will be called once per RTT, and the report struct `r` will contain:
# "rtt", "Cwnd", and "Rate"
def on_report(self, r):
# Compute new cwnd internally
# If the rtt has decreased, increase the cwnd by 1 packet, else decrease by 1 packet
if self.last_rtt < r.rtt:
self.cwnd += self.datapath_info.mss
else:
self.cwnd -= self.datapath_info.mss
self.last_rtt = r.rtt
# Send this new value of cwnd to the datapath
self.datapath.update_field("Cwnd", self.cwnd)
You should install an initial datapath program in your __init__
implementation, otherwise you will not receive any reports and nothing else will happen. You can always install a different datapath program later when handling on_report
.
If you want to print anything, you should use sys.stderr.write()
(note that you need to import sys
and that it doesn't automatically add new lines for you like print
does).
You must store a reference to datapath
in self
called "datapath" (i.e. self.datapath = datapath
), because the library internally uses this to access the datapath struct as well.
The CCP entry point is portus.connect(ipc_type, class, debug, blocking)
:
ipc_type (string)
: (netlink | unix | char) on linux or (unix) on macclass
: your algorithm class, e.g. SampleCCAlg
debug (bool)
: if true, the CCP will log all messages passed between the ccp and datapathblocking (bool)
: if true, use blocking ipc reads, otherwise use non-blockingFor example: portus.connect("netlink", SampleCCAlg, debug=True, blocking=True)
.
Regardless of whether you use blocking or non-blocking sockets, connect
will block forever (to stop the CCP just send ctrl+c or kill the process).
For a full working example of both defining an algorithm and running the CCP, see the simple AIMD scheme in ./aimd.py
and try running it: sudo python aimd.py
.