blob: b7d6bc79cd77205df727bf94a89e36b60f42e953 [file] [log] [blame]
.. _module-pw_transfer:
===========
pw_transfer
===========
``pw_transfer`` is a reliable data transfer protocol which runs on top of
Pigweed RPC.
.. attention::
``pw_transfer`` is under construction and so is its documentation.
-----
Usage
-----
C++
===
Transfer thread
---------------
To run transfers as either a client or server (or both), a dedicated thread is
required. The transfer thread is used to process all transfer-related events
safely. The same transfer thread can be shared by a transfer client and service
running on the same system.
.. note::
All user-defined transfer callbacks (i.e. the virtual interface of a
``Handler`` or completion function in a transfer client) will be
invoked from the transfer thread's context.
In order to operate, a transfer thread requires two buffers:
- The first is a *chunk buffer*. This is used to stage transfer packets received
by the RPC system to be processed by the transfer thread. It must be large
enough to store the largest possible chunk the system supports.
- The second is an *encode buffer*. This is used by the transfer thread to
encode outgoing RPC packets. It is necessarily larger than the chunk buffer.
Typically, this is sized to the system's maximum transmission unit at the
transport layer.
A transfer thread is created by instantiating a ``pw::transfer::Thread``. This
class derives from ``pw::thread::ThreadCore``, allowing it to directly be used
when creating a system thread. Refer to :ref:`module-pw_thread-thread-creation`
for additional information.
**Example thread configuration**
.. code-block:: cpp
#include "pw_transfer/transfer_thread.h"
namespace {
// The maximum number of concurrent transfers the thread should support as
// either a client or a server. These can be set to 0 (if only using one or
// the other).
constexpr size_t kMaxConcurrentClientTransfers = 5;
constexpr size_t kMaxConcurrentServerTransfers = 3;
// The maximum payload size that can be transmitted by the system's
// transport stack. This would typically be defined within some transport
// header.
constexpr size_t kMaxTransmissionUnit = 512;
// The maximum amount of data that should be sent within a single transfer
// packet. By necessity, this should be less than the max transmission unit.
//
// pw_transfer requires some additional per-packet overhead, so the actual
// amount of data it sends may be lower than this.
constexpr size_t kMaxTransferChunkSizeBytes = 480;
// Buffers for storing and encoding chunks (see documentation above).
std::array<std::byte, kMaxTransferChunkSizeBytes> chunk_buffer;
std::array<std::byte, kMaxTransmissionUnit> encode_buffer;
pw::transfer::Thread<kMaxConcurrentClientTransfers,
kMaxConcurrentServerTransfers>
transfer_thread(chunk_buffer, encode_buffer);
} // namespace
// pw::transfer::TransferThread is the generic, non-templated version of the
// Thread class. A Thread can implicitly convert to a TransferThread.
pw::transfer::TransferThread& GetSystemTransferThread() {
return transfer_thread;
}
.. _pw_transfer-transfer-server:
Transfer server
---------------
``pw_transfer`` provides an RPC service for running transfers through an RPC
server.
To know how to read data from or write data to device, a ``Handler`` interface
is defined (``pw_transfer/public/pw_transfer/handler.h``). Transfer handlers
represent a transferable resource, wrapping a stream reader and/or writer with
initialization and completion code. Custom transfer handler implementations
should derive from ``ReadOnlyHandler``, ``WriteOnlyHandler``, or
``ReadWriteHandler`` as appropriate and override Prepare and Finalize methods
if necessary.
A transfer handler should be implemented and instantiated for each unique
resource that can be transferred to or from a device. Each instantiated handler
must have a globally-unique integer ID used to identify the resource.
Handlers are registered with the transfer service. This may be done during
system initialization (for static resources), or dynamically at runtime to
support ephemeral transfer resources.
**Example transfer handler implementation**
.. code-block:: cpp
#include "pw_stream/memory_stream.h"
#include "pw_transfer/transfer.h"
// A simple transfer handler which reads data from an in-memory buffer.
class SimpleBufferReadHandler : public pw::transfer::ReadOnlyHandler {
public:
SimpleReadTransfer(uint32_t resource_id, pw::ConstByteSpan data)
: ReadOnlyHandler(resource_id), reader_(data) {
set_reader(reader_);
}
private:
pw::stream::MemoryReader reader_;
};
Handlers may optionally implement a `GetStatus` method, which allows clients to
query the status of a resource with a handler registered. The application layer
above transfer can choose how to fill and interpret this information. The status
information is `readable_offset`, `writeable_offset`, `read_checksum`, and
`write_checksum`.
**Example GetStatus implementation**
.. code-block:: cpp
Status GetStatus(uint64_t& readable_offset,
uint64_t& writeable_offset,
uint64_t& read_checksum,
uint64_t& write_checksum) {
readable_offset = resource.get_size();
writeable_offset = resource.get_writeable_offset();
read_checksum = resource.get_crc();
write_checksum = resource.calculate_crc(0, writeable_offset);
return pw::OkStatus();
}
The transfer service is instantiated with a reference to the system's transfer
thread and registered with the system's RPC server.
**Example transfer service initialization**
.. code-block:: cpp
#include "pw_transfer/transfer.h"
namespace {
// In a write transfer, the maximum number of bytes to receive at one time
// (potentially across multiple chunks), unless specified otherwise by the
// transfer handler's stream::Writer. Should be set reasonably high; the
// transfer will attempt to determine an optimal window size based on the
// link.
constexpr size_t kDefaultMaxBytesToReceive = 16384;
pw::transfer::TransferService transfer_service(
GetSystemTransferThread(), kDefaultMaxBytesToReceive);
// Instantiate a handler for the data to be transferred. The resource ID will
// be used by the transfer client and server to identify the handler.
constexpr uint32_t kMagicBufferResourceId = 1;
char magic_buffer_to_transfer[256] = { /* ... */ };
SimpleBufferReadHandler magic_buffer_handler(
kMagicBufferResourceId, magic_buffer_to_transfer);
} // namespace
void InitTransferService() {
// Register the handler with the transfer service, then the transfer service
// with an RPC server.
transfer_service.RegisterHandler(magic_buffer_handler);
GetSystemRpcServer().RegisterService(transfer_service);
}
Transfer client
---------------
``pw_transfer`` provides a transfer client capable of running transfers through
an RPC client.
.. note::
Currently, a transfer client is only capable of running transfers on a single
RPC channel. This may be expanded in the future.
The transfer client provides the following APIs for managing data transfers:
.. cpp:function:: Result<pw::Transfer::Client::Handle> pw::transfer::Client::Read(uint32_t resource_id, pw::stream::Writer& output, CompletionFunc&& on_completion, pw::transfer::ProtocolVersion version = kDefaultProtocolVersion, pw::chrono::SystemClock::duration timeout = cfg::kDefaultClientTimeout, pw::chrono::SystemClock::duration initial_chunk_timeout = cfg::kDefaultInitialChunkTimeout, uint32_t initial_offset = 0u)
Reads data from a transfer server to the specified ``pw::stream::Writer``.
Invokes the provided callback function with the overall status of the
transfer.
Due to the asynchronous nature of transfer operations, this function will only
return a non-OK status if it is called with bad arguments. Otherwise, it will
return OK and errors will be reported through the completion callback.
For using the offset parameter, please see :ref:`pw_transfer-nonzero-transfers`.
.. cpp:function:: Result<pw::Transfer::Client::Handle> pw::transfer::Client::Write(uint32_t resource_id, pw::stream::Reader& input, CompletionFunc&& on_completion, pw::transfer::ProtocolVersion version = kDefaultProtocolVersion, pw::chrono::SystemClock::duration timeout = cfg::kDefaultClientTimeout, pw::chrono::SystemClock::duration initial_chunk_timeout = cfg::kDefaultInitialChunkTimeout, uint32_t initial_offset = 0u)
Writes data from a source ``pw::stream::Reader`` to a transfer server.
Invokes the provided callback function with the overall status of the
transfer.
Due to the asynchronous nature of transfer operations, this function will only
return a non-OK status if it is called with bad arguments. Otherwise, it will
return OK and errors will be reported through the completion callback.
For using the offset parameter, please see :ref:`pw_transfer-nonzero-transfers`.
Transfer handles
^^^^^^^^^^^^^^^^
Each transfer session initiated by a client returns a ``Handle`` object which
is used to manage the transfer. These handles support the following operations:
.. cpp:function:: pw::Transfer::Client::Handle::Cancel()
Terminates the ongoing transfer.
.. cpp:function:: pw::Transfer::Client::Handle::SetTransferSize(size_t size_bytes)
In a write transfer, indicates the total size of the transfer resource.
**Example client setup**
.. code-block:: cpp
#include "pw_transfer/client.h"
namespace {
// RPC channel on which transfers should be run.
constexpr uint32_t kChannelId = 42;
// In a read transfer, the maximum number of bytes to receive at one time
// (potentially across multiple chunks), unless specified otherwise by the
// transfer's stream. Should be set reasonably high; the transfer will
// attempt to determine an optimal window size based on the link.
constexpr size_t kDefaultMaxBytesToReceive = 16384;
pw::transfer::Client transfer_client(GetSystemRpcClient(),
kChannelId,
GetSystemTransferThread(),
kDefaultMaxBytesToReceive);
} // namespace
Status ReadMagicBufferSync(pw::ByteSpan sink) {
pw::stream::Writer writer(sink);
struct {
pw::sync::ThreadNotification notification;
pw::Status status;
} transfer_state;
Result<pw::transfer::Client::Handle> handle = transfer_client.Read(
kMagicBufferResourceId,
writer,
[&transfer_state](pw::Status status) {
transfer_state.status = status;
transfer_state.notification.release();
});
if (!handle.ok()) {
return handle.status();
}
// Block until the transfer completes.
transfer_state.notification.acquire();
return transfer_state.status;
}
Specifying Resource Sizes
-------------------------
Transfer data is sent and received through the ``pw::Stream`` interface, which
does not have a concept of overall stream size. Users of transfers that are
fixed-size may optionally indicate this to the transfer client and server,
which will be shared with the transfer peer to enable features such as progress
reporting.
The transfer size can only be set on the transmitting side of the transfer;
that is, the client in a ``Write`` transfer or the server in a ``Read``
transfer.
If the specified resource size is smaller than the available transferrable data,
only a slice of the data up to the resource size will be transferred. If the
specified size is equal to or larger than the data size, all of the data will
be sent.
**Setting a transfer size from a transmitting client**
.. code-block:: c++
Result<pw::transfer::Client::Handle> handle = client.Write(...);
if (handle.ok()) {
handle->SetTransferSize(kMyResourceSize);
}
**Setting a transfer size on a server resource**
The ``TransferHandler`` interface allows overriding its ``ResourceSize``
function to return the size of its transfer resource.
.. code-block:: c++
class MyResourceHandler : public pw::transfer::ReadOnlyHandler {
public:
Status PrepareRead() final;
virtual size_t ResourceSize() const final {
return kMyResourceSize;
}
};
Atomic File Transfer Handler
----------------------------
Transfers are handled using the generic `Handler` interface. A specialized
`Handler`, `AtomicFileTransferHandler` is available to handle file transfers
with atomic semantics. It guarantees that the target file of the transfer is
always in a correct state. A temporary file is written to prior to updating the
target file. If any transfer failure occurs, the transfer is aborted and the
target file is either not created or not updated.
.. _module-pw_transfer-config:
Module Configuration Options
----------------------------
The following configurations can be adjusted via compile-time configuration of
this module, see the
:ref:`module documentation <module-structure-compile-time-configuration>` for
more details.
.. c:macro:: PW_TRANSFER_DEFAULT_MAX_CLIENT_RETRIES
The default maximum number of times a transfer client should retry sending a
chunk when no response is received. Can later be configured per-transfer when
starting one.
.. c:macro:: PW_TRANSFER_DEFAULT_MAX_SERVER_RETRIES
The default maximum number of times a transfer server should retry sending a
chunk when no response is received.
In typical setups, retries are driven by the client, and timeouts on the
server are used only to clean up resources, so this defaults to 0.
.. c:macro:: PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES
The default maximum number of times a transfer should retry sending any chunk
over the course of its entire lifetime.
This number should be high, particularly if long-running transfers are
expected. Its purpose is to prevent transfers from getting stuck in an
infinite loop.
.. c:macro:: PW_TRANSFER_DEFAULT_CLIENT_TIMEOUT_MS
The default amount of time, in milliseconds, to wait for a chunk to arrive
in a transfer client before retrying. This can later be configured
per-transfer.
.. c:macro:: PW_TRANSFER_DEFAULT_SERVER_TIMEOUT_MS
The default amount of time, in milliseconds, to wait for a chunk to arrive
on the server before retrying. This can later be configured per-transfer.
.. c:macro:: PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS
The default amount of time, in milliseconds, to wait for an initial server
response to a transfer before retrying. This can later be configured
per-transfer.
This is set separately to PW_TRANSFER_DEFAULT_TIMEOUT_MS as transfers may
require additional time for resource initialization (e.g. erasing a flash
region before writing to it).
.. c:macro:: PW_TRANSFER_DEFAULT_EXTEND_WINDOW_DIVISOR
The fractional position within a window at which a receive transfer should
extend its window size to minimize the amount of time the transmitter
spends blocked.
For example, a divisor of 2 will extend the window when half of the
requested data has been received, a divisor of three will extend at a third
of the window, and so on.
.. c:macro:: PW_TRANSFER_LOG_DEFAULT_CHUNKS_BEFORE_RATE_LIMIT
Number of chunks to send repetitive logs at full rate before reducing to
rate_limit. Retransmit parameter chunks will restart at this chunk count
limit.
Default is first 10 parameter logs will be sent, then reduced to one log
every ``PW_TRANSFER_RATE_PERIOD_MS``
.. c:macro:: PW_TRANSFER_LOG_DEFAULT_RATE_PERIOD_MS
The minimum time between repetative logs after the rate limit has been
applied (after CHUNKS_BEFORE_RATE_LIMIT parameter chunks).
Default is to reduce repetative logs to once every 10 seconds after
CHUNKS_BEFORE_RATE_LIMIT parameter chunks have been sent.
.. c:macro:: PW_TRANSFER_CONFIG_LOG_LEVEL
Configurable log level for the entire transfer module.
.. c:macro:: PW_TRANSFER_CONFIG_DEBUG_CHUNKS
Turns on logging of individual non-data or non-parameter chunks. Default is
false, to disable logging.
.. c:macro:: PW_TRANSFER_CONFIG_DEBUG_DATA_CHUNKS
Turns on logging of individual data and parameter chunks. Default is false to
disable logging. These chunks are moderated (rate-limited) by the same
``PW_TRANSFER_RATE_PERIOD_MS`` as other repetitive logs.
.. _pw_transfer-nonzero-transfers:
Non-zero Starting Offset Transfers
----------------------------------
``pw_transfer`` provides for transfers which read from or
write to a server resource starting from a point after the beginning.
Handling of read/write/erase boundaries of the resource storage backend must
be handled by the user through the transfer handler interfaces of `GetStatus`
and `PrepareRead/Write(uint32_t offset)`.
A resource can be read or written from a non-zero starting offset simply by
having the transfer client calling `read()` or `write()` with an offset
parameter. The offset gets included in the starting handshake.
.. note::
The data or stream passed to `read()` or `write()` will be used as-is. I.e.
no seeking will be applied; the user is expected to seek to the desired
location.
On the server side, the offset is accepted, and passed to the transfer
handler's `Prepare(uint32_t)` method. This method must be implemented
specifically by the handler in order to support the offset transfer. The
transfer handler confirms that the start offset is valid for the read/write
operation, and the server responds with the offset to confirm the non-zero
transfer operation. Older server sw will ignore the offset, so the clients
check that the server has accepted the non-zero offset during the handshake, so
users may elect to catch such errors. Clients return `Status.UNIMPLEMENTED` in
such cases.
Due to the need to seek streams by the handler to support the non-zero offset,
it is recommended to return `Status.RESOURCE_EXHAUSTED` if a seek is requested
past the end of the stream.
See the :ref:`transfer handler <pw_transfer-transfer-server>` documentation for
further information about configuring resources for non-zero transfers and the
interface documentation in
``pw/transfer/public/pw_transfer/handler.h``
Python
======
.. automodule:: pw_transfer
:members: ProgressStats, ProtocolVersion, Manager, Error
**Example**
.. code-block:: python
import pw_transfer
# Initialize a Pigweed RPC client; see pw_rpc docs for more info.
rpc_client = CustomRpcClient()
rpcs = rpc_client.channel(1).rpcs
transfer_service = rpcs.pw.transfer.Transfer
transfer_manager = pw_transfer.Manager(transfer_service)
try:
# Read the transfer resource with ID 3 from the server.
data = transfer_manager.read(3)
except pw_transfer.Error as err:
print('Failed to read:', err.status)
try:
# Send some data to the server. The transfer manager does not have to be
# reinitialized.
transfer_manager.write(2, b'hello, world')
except pw_transfer.Error as err:
print('Failed to write:', err.status)
Typescript
==========
Provides a simple interface for transferring bulk data over pw_rpc.
**Example**
.. code-block:: typescript
import { pw_transfer } from 'pigweedjs';
const { Manager } from pw_transfer;
const client = new CustomRpcClient();
service = client.channel()!.service('pw.transfer.Transfer')!;
const manager = new Manager(service, DEFAULT_TIMEOUT_S);
manager.read(3, (stats: ProgressStats) => {
console.log(`Progress Update: ${stats}`);
}).then((data: Uint8Array) => {
console.log(`Completed read: ${data}`);
}).catch(error => {
console.log(`Failed to read: ${error.status}`);
});
manager.write(2, textEncoder.encode('hello world'))
.catch(error => {
console.log(`Failed to read: ${error.status}`);
});
Java
====
pw_transfer provides a Java client. The transfer client returns a
`ListenableFuture <https://guava.dev/releases/21.0/api/docs/com/google/common/util/concurrent/ListenableFuture>`_
to represent the results of a read or write transfer.
.. code-block:: java
import dev.pigweed.pw_transfer.TransferClient;
public class TheClass {
public void DoTransfer(MethodClient transferReadMethodClient,
MethodClient transferWriteMethodClient) {
// Create a new transfer client.
TransferClient client = new TransferClient(
transferReadMethodClient,
transferWriteMethodClient,
TransferTimeoutSettings.builder()
.setTimeoutMillis(TRANSFER_TIMEOUT_MS)
.setMaxRetries(MAX_RETRIES)
.build());
// Start a read transfer.
ListenableFuture<byte[]> readTransfer = client.read(123);
// Start a write transfer.
ListenableFuture<Void> writeTransfer = client.write(123, dataToWrite);
// Get the data from the read transfer.
byte[] readData = readTransfer.get();
// Wait for the write transfer to complete.
writeTransfer.get();
}
}
--------
Protocol
--------
Chunks
======
Transfers run as a series of *chunks* exchanged over an RPC stream. Chunks can
contain transferable data, metadata, and control parameters. Each chunk has an
associated type, which determines what information it holds and the semantics of
its fields.
The chunk is a protobuf message, whose definition can be found
:ref:`here <module-pw_transfer-proto-definition>`.
Resources and sessions
======================
Transfers are run for a specific *resource* --- a stream of data which can be
read from or written to. Resources have a system-specific integral identifier
defined by the implementers of the server-side transfer node.
The series of chunks exchanged in an individual transfer operation for a
resource constitute a transfer *session*. The session runs from its opening
chunk until either a terminating chunk is received or the transfer times out.
Sessions are assigned IDs by the client that starts them, which are unique over
the RPC channel between the client and server, allowing the server to identify
transfers across multiple clients.
Reliability
===========
``pw_transfer`` attempts to be a reliable data transfer protocol.
As Pigweed RPC is considered an unreliable communications system,
``pw_transfer`` implements its own mechanisms for reliability. These include
timeouts, data retransmissions, and handshakes.
.. note::
A transfer can only be reliable if its underlying data stream is seekable.
A non-seekable stream could prematurely terminate a transfer following a
packet drop.
Opening handshake
=================
Transfers begin with a three-way handshake, whose purpose is to identify the
resource being transferred, assign a session ID, and synchronize the protocol
version to use.
A read or write transfer for a resource is initiated by a transfer client. The
client sends the ID of the resource to the server alongside a unique session ID
in a ``START`` chunk, indicating that it wishes to begin a new transfer. This
chunk additionally encodes the protocol version which the client is configured
to use.
Upon receiving a ``START`` chunk, the transfer server checks whether the
requested resource is available. If so, it prepares the resource for the
operation, which typically involves opening a data stream, alongside any
additional user-specified setup. The server accepts the client's session ID,
then responds to the client with a ``START_ACK`` chunk containing the resource,
session, and configured protocol version for the transfer.
.. _module-pw_transfer-windowing:
Windowing
=========
Throughout a transfer, the receiver maintains a window of how much data it can
receive at a given time. This window is a multiple of the maximum size of a
single data chunk, and is adjusted dynamically in response to the ongoing status
of the transfer.
pw_transfer uses a congestion control algorithm similar to that of TCP
`(RFC 5681 §3.1) <https://datatracker.ietf.org/doc/html/rfc5681#section-3.1>`_,
adapted to pw_transfer's mode of operation that tunes parameters per window.
Once a portion of a window has successfully been received, it is acknowledged by
the reciever and the window size is extended. Transfers begin in a "slow start"
phase, during which the window is doubled on each ACK. This continues until the
transfer detects a packet loss. Once this occurs, the window size is halved and
the transfer enters a "congestion avoidance" phase for the remainder of its run.
During this phase, successful ACKs increase the window size by a single chunk,
whereas packet loss continues to half it.
Transfer completion
===================
Either side of a transfer can terminate the operation at any time by sending a
``COMPLETION`` chunk containing the final status of the transfer. When a
``COMPLETION`` chunk is sent, the terminator of the transfer performs local
cleanup, then waits for its peer to acknowledge the completion.
Upon receving a ``COMPLETION`` chunk, the transfer peer cancels any pending
operations, runs its set of cleanups, and responds with a ``COMPLETION_ACK``,
fully ending the session from the peer's side.
The terminator's session remains active waiting for a ``COMPLETION_ACK``. If not
received after a timeout, it re-sends its ``COMPLETION`` chunk. The session ends
either following receipt of the acknowledgement or if a maximum number of
retries is hit.
.. _module-pw_transfer-proto-definition:
Server to client transfer (read)
================================
.. image:: read.svg
Client to server transfer (write)
=================================
.. image:: write.svg
Protocol buffer definition
==========================
.. literalinclude:: transfer.proto
:language: protobuf
:lines: 14-
Errors
======
Protocol errors
---------------
The following table describes the meaning of each status code when sent by the
sender or the receiver (see `Transfer roles`_).
.. cpp:namespace-push:: pw::stream
+-------------------------+-------------------------+-------------------------+
| Status | Sent by sender | Sent by receiver |
+=========================+=========================+=========================+
| ``OK`` | (not sent) | All data was received |
| | | and handled |
| | | successfully. |
+-------------------------+-------------------------+-------------------------+
| ``ABORTED`` | The service aborted the transfer because the |
| | client restarted it. This status is passed to the |
| | transfer handler, but not sent to the client |
| | because it restarted the transfer. |
+-------------------------+---------------------------------------------------+
| ``CANCELLED`` | The client cancelled the transfer. |
+-------------------------+-------------------------+-------------------------+
| ``DATA_LOSS`` | Failed to read the data | Failed to write the |
| | to send. The | received data. The |
| | :cpp:class:`Reader` | :cpp:class:`Writer` |
| | returned an error. | returned an error. |
+-------------------------+-------------------------+-------------------------+
| ``FAILED_PRECONDITION`` | Received chunk for transfer that is not active. |
+-------------------------+-------------------------+-------------------------+
| ``INVALID_ARGUMENT`` | Received a malformed packet. |
+-------------------------+-------------------------+-------------------------+
| ``INTERNAL`` | An assumption of the protocol was violated. |
| | Encountering ``INTERNAL`` indicates that there is |
| | a bug in the service or client implementation. |
+-------------------------+-------------------------+-------------------------+
| ``PERMISSION_DENIED`` | The transfer does not support the requested |
| | operation (either reading or writing). |
+-------------------------+-------------------------+-------------------------+
| ``RESOURCE_EXHAUSTED`` | The receiver requested | Storage is full. |
| | zero bytes, indicating | |
| | their storage is full, | |
| | but there is still data | |
| | to send. | |
+-------------------------+-------------------------+-------------------------+
| ``UNAVAILABLE`` | The service is busy with other transfers and |
| | cannot begin a new transfer at this time. |
+-------------------------+-------------------------+-------------------------+
| ``UNIMPLEMENTED`` | Out-of-order chunk was | (not sent) |
| | requested, but seeking | |
| | is not supported. | |
+-------------------------+-------------------------+-------------------------+
.. cpp:namespace-pop::
Transfer roles
==============
Every transfer has two participants: the sender and the receiver. The sender
transmits data to the receiver. The receiver controls how the data is
transferred and sends the final status when the transfer is complete.
In read transfers, the client is the receiver and the service is the sender. In
write transfers, the client is the sender and the service is the receiver.
Sender flow
-----------
.. mermaid::
graph TD
start([Client initiates<br>transfer]) -->data_request
data_request[Receive transfer<br>parameters]-->send_chunk
send_chunk[Send chunk]-->sent_all
sent_all{Sent final<br>chunk?} -->|yes|wait
sent_all-->|no|sent_requested
sent_requested{Sent all<br>pending?}-->|yes|data_request
sent_requested-->|no|send_chunk
wait[Wait for receiver]-->is_done
is_done{Received<br>final chunk?}-->|yes|done
is_done-->|no|data_request
done([Transfer complete])
Receiver flow
-------------
.. mermaid::
graph TD
start([Client initiates<br>transfer]) -->request_bytes
request_bytes[Set transfer<br>parameters]-->wait
wait[Wait for chunk]-->received_chunk
received_chunk{Received<br>chunk by<br>deadline?}-->|no|request_bytes
received_chunk-->|yes|check_chunk
check_chunk{Correct<br>offset?} -->|yes|process_chunk
check_chunk --> |no|request_bytes
process_chunk[Process chunk]-->final_chunk
final_chunk{Final<br>chunk?}-->|yes|signal_completion
final_chunk{Final<br>chunk?}-->|no|received_requested
received_requested{Received all<br>pending?}-->|yes|request_bytes
received_requested-->|no|wait
signal_completion[Signal completion]-->done
done([Transfer complete])
Legacy protocol
===============
``pw_transfer`` was initially released into production prior to several of the
reliability improvements of its modern protocol. As a result of this, transfer
implementations support a "legacy" protocol mode, in which transfers run without
utilizing these features.
The primary differences between the legacy and modern protocols are listed
below.
- There is no distinction between a transfer resource and session --- a single
``transfer_id`` field represents both. Only one transfer for a given resource
can run at a time, and it is not possible to determine where one transfer for
a resource ends and the next begins.
- The legacy protocol has no opening handshake phase. The client initiates with
a transfer ID and starting transfer parameters (during a read), and the data
transfer phase begins immediately.
- The legacy protocol has no terminating handshake phase. When either end
completes a transfer by sending a status chunk, it does not wait for the peer
to acknowledge. Resources used by the transfer are immediately freed, and
there is no guarantee that the peer is notified of completion.
Transfer clients request the latest transfer protocol version by default, but
may be configured to request the legacy protocol. Transfer server and client
implementations detect if their transfer peer is running the legacy protocol and
automatically switch to it if required, even if they requested a newer protocol
version. It is **strongly** unadvised to use the legacy protocol in new code.
.. _module-pw_transfer-integration-tests:
-----------------
Integration tests
-----------------
The ``pw_transfer`` module has a set of integration tests that verify the
correctness of implementations in different languages.
`Test source code <https://cs.pigweed.dev/pigweed/+/main:pw_transfer/integration_test/>`_.
To run the tests on your machine, run
.. code-block:: bash
$ bazel test \
pw_transfer/integration_test:cross_language_small_test \
pw_transfer/integration_test:cross_language_medium_test
.. note:: There is a large test that tests transfers that are megabytes in size.
These are not run automatically, but can be run manually via the
``pw_transfer/integration_test:cross_language_large_test`` test. These are
VERY slow, but exist for manual validation of real-world use cases.
The integration tests permit injection of client/server/proxy binaries to use
when running the tests. This allows manual testing of older versions of
pw_transfer against newer versions.
.. code-block:: bash
# Test a newer version of pw_transfer against an old C++ client that was
# backed up to another directory.
$ bazel run pw_transfer/integration_test:cross_language_medium_test -- \
--cpp-client-binary ../old_pw_transfer_version/cpp_client
Backwards compatibility tests
=============================
``pw_transfer`` includes a `suite of backwards-compatibility tests
<https://cs.pigweed.dev/pigweed/+/main:pw_transfer/integration_test/legacy_binaries_test.py>`_
that are intended to continuously validate a degree of backwards-compatibility
with older pw_transfer servers and clients. This is done by retrieving older
binaries hosted in CIPD and running tests between the older client/server
binaries and the latest binaries.
The CIPD package contents can be created with this command:
.. code-block::bash
$ bazel build pw_transfer/integration_test:server \
pw_transfer/integration_test:cpp_client
$ mkdir pw_transfer_test_binaries
$ cp bazel-bin/pw_transfer/integration_test/server \
pw_transfer_test_binaries
$ cp bazel-bin/pw_transfer/integration_test/cpp_client \
pw_transfer_test_binaries
To update the CIPD package itself, follow the `internal documentation for
updating a CIPD package <go/pigweed-cipd#installing-packages-into-cipd>`_.
CI/CQ integration
=================
`Current status of the test in CI <https://ci.chromium.org/ui/p/pigweed/builders/luci.pigweed.pigweed.ci/pigweed-linux-bzl-integration>`_.
By default, these tests are not run in CQ (on presubmit) because they are too
slow. However, you can request that the tests be run in presubmit on your
change by adding to following line to the commit message footer:
.. code-block::
Cq-Include-Trybots: luci.pigweed.try:pigweed-linux-bzl-integration
.. _module-pw_transfer-parallel-tests:
Running the tests many times
============================
Because the tests bind to network ports, you cannot run more than one instance
of each test in parallel. However, you might want to do so, e.g. to debug
flakes. This section describes a manual process that makes this possible.
Linux
-----
On Linux, you can add the ``"block-network"`` tag to the tests (`example
<https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/181297>`_). This
enables network isolation for the tests, allowing you to run them in parallel
via,
.. code-block::
bazel test --runs_per_test=10 //pw_transfer/integration_tests/...
MacOS
-----
Network isolation is not supported on MacOS because the OS doesn't support
network virtualization (`gh#2669
<https://github.com/bazelbuild/bazel/issues/2669>`_). The best you can do is to
tag the tests ``"exclusive"``. This allows you to use ``--runs_per_test``, but
will force each test to run by itself, with no parallelism.
Why is this manual?
-------------------
Ideally, we would apply either the ``"block-network"`` or ``"exclusive"`` tag
to the tests depending on the OS. But this is not supported, `gh#2971
<https://github.com/bazelbuild/bazel/issues/2971>`_.
We don't want to tag the tests ``"exclusive"`` by default because that will
prevent *different* tests from running in parallel, significantly slowing them
down.
.. toctree::
:hidden:
API reference <api>