userver: YDB topic writer service
Loading...
Searching...
No Matches
YDB topic writer service

Before you start

Make sure that you can compile and run core tests and read a basic example Writing your first HTTP server.

It is recommended to read YDB service first, as it covers YDB table operations and topic consumption.

Step by step guide

In this tutorial we will write a service that publishes messages to a YDB topic using ydb::TopicWriter. The service would have the following REST API:

  • HTTP POST by path /write with the message body in the request body writes the message to the configured YDB topic.

WriteHandler declaration

Like in Writing your first HTTP server we create a component for handling HTTP requests. The component holds references to ydb::TopicWriterManager and ydb::TopicWriter obtained from ydb::TopicWriterComponent:

class WriteHandler final : public server::handlers::HttpHandlerBase {
public:
static constexpr std::string_view kName{"handler-write"};
WriteHandler(const components::ComponentConfig& config, const components::ComponentContext& context);
std::string HandleRequest(server::http::HttpRequest& request, server::request::RequestContext& context)
const override;
private:
ydb::TopicWriterManager& writer_manager_;
ydb::TopicWriter& writer_;
};

WriteHandler constructor

The topic writer is looked up by name configured in static config (messages in this sample):

WriteHandler::WriteHandler(const components::ComponentConfig& config, const components::ComponentContext& context)
: server::handlers::HttpHandlerBase(config, context),
writer_manager_{context.FindComponent<ydb::TopicWriterComponent>().GetTopicWriterManager()},
writer_(writer_manager_.GetTopicWriter(kWriterName))
{}

WriteHandler::HandleRequest

To publish a message, call ydb::TopicWriter::WriteMessage(). The method returns ydb::TopicWriteResult with a status that indicates whether the message was accepted into the writer queue:

std::string WriteHandler::HandleRequest(server::http::HttpRequest& request, server::request::RequestContext&) const {
request.GetHttpResponse().SetContentType(http::content_type::kTextPlain);
const auto result = writer_.WriteMessage(request.RequestBody(), {});
switch (result.GetStatus()) {
return "Message was written to topic\n";
request.SetResponseStatus(server::http::HttpStatus::kTooManyRequests);
return "Topic writer queue is full\n";
request.SetResponseStatus(server::http::HttpStatus::kInternalServerError);
return "Failed to write message to topic\n";
}
request.SetResponseStatus(server::http::HttpStatus::kInternalServerError);
return "Unexpected topic writer status\n";
}

ydb::TopicWriteStatus::kResourceExhausted means the internal queue is full and the caller should retry later. ydb::TopicWriteStatus::kOk means the message was accepted for delivery.

Static config

Static configuration of the service is quite close to the configuration from Writing your first HTTP server except for the YDB component, topic writer and handler:

# yaml
ydb:
operation-settings:
client-timeout: 1100ms
retries: 3
databases:
sampledb:
database: sampledb
max_pool_size: 10
min_pool_size: 5

Topic writer settings:

# yaml
ydb-topic-writer:
topics:
messages:
topic: messages
database: sampledb

HTTP handler:

# yaml
handler-write:
path: /write
method: POST
task_processor: main-task-processor

There are more static options for the topic writer component configuration, all of them are described at ydb::TopicWriterComponent.

See YDB for YDB hints and more usage samples.

int main()

Add the handler, ydb::YdbComponent and ydb::TopicWriterComponent to the components::MinimalServerComponentList() and run utils::DaemonRun:

int main(int argc, char* argv[]) {
const auto component_list =
.AppendComponentList(USERVER_NAMESPACE::dynamic_config::updater::ComponentList())
.Append<server::handlers::TestsControl>()
.AppendComponentList(clients::http::ComponentList())
.Append<components::DefaultSecdistProvider>()
.Append<samples::ydb_topic_writer::WriteHandler>()
.Append<ydb::YdbComponent>()
.Append<ydb::TopicWriterComponent>();
return utils::DaemonMain(argc, argv, component_list);
}

Build and Run

To build the sample, execute the following build steps at the userver root directory:

mkdir build_release
cd build_release
cmake -DCMAKE_BUILD_TYPE=Release ..
make userver-samples-ydb_topic_writer_service

The sample could be started by running make start-userver-samples-ydb_topic_writer_service. The command would invoke testsuite start target that sets proper paths in the configuration files, prepares and starts YDB, and starts the service.

To start the service manually start the YDB server and run ./samples/ydb_topic_writer_service/userver-samples-ydb_topic_writer_service -c </path/to/static_config.yaml>.

Now you can send a request to your service from another terminal:

bash
$ curl -X POST 'http://localhost:8080/write' -d 'hello topic writer'
Message was written to topic

Functional testing

Functional tests for the service could be implemented using the testsuite. To do that you have to:

  • Turn on the pytest_userver.plugins.ydb plugin and create the topic before each test:
    import pytest
    pytest_plugins = ['pytest_userver.plugins.core', 'pytest_userver.plugins.ydb']
    @pytest.fixture(scope='session')
    def ydb_topic_path() -> str:
    return 'messages'
    @pytest.fixture(scope='session')
    def ydb_topic_consumer_name() -> str:
    return 'test-consumer'
    @pytest.fixture(autouse=True, scope='function')
    def ydb_create_topic(ydb, ydb_topic_path, ydb_topic_consumer_name):
    topic_client = ydb.topic_client
    try:
    describe_result = topic_client.describe_topic(ydb_topic_path)
    if hasattr(describe_result, 'result'):
    describe_result = describe_result.result()
    _ = describe_result
    except Exception:
    create_result = topic_client.create_topic(
    ydb_topic_path,
    consumers=[ydb_topic_consumer_name],
    )
    if hasattr(create_result, 'result'):
    create_result.result()
  • Write the test that sends a message via HTTP and reads it back from the topic using the YDB topic client:
    async def test_topic_writer_cycle(service_client, ydb, ydb_topic_path, ydb_topic_consumer_name):
    message = 'hello topic writer'
    response = await service_client.post('/write', data=message)
    assert response.status == 200
    assert response.text == 'Message was written to topic\n'
    topic_client = ydb.topic_client
    reader = topic_client.reader(
    ydb_topic_path,
    consumer=ydb_topic_consumer_name,
    )
    deadline = asyncio.get_running_loop().time() + 10.0
    received_messages = []
    while asyncio.get_running_loop().time() < deadline:
    message_batch = reader.receive_message(timeout=1.0)
    if hasattr(message_batch, 'result'):
    message_batch = message_batch.result()
    if not message_batch:
    await asyncio.sleep(0.2)
    continue
    if isinstance(message_batch, (list, tuple)):
    received_messages.extend(message_batch)
    else:
    received_messages.append(message_batch)
    for received in received_messages:
    data = getattr(received, 'data', None)
    if callable(data):
    data = data()
    if data == message.encode():
    if hasattr(reader, 'commit'):
    commit_result = reader.commit(received)
    if hasattr(commit_result, 'result'):
    commit_result.result()
    if hasattr(reader, 'close'):
    close_result = reader.close()
    if hasattr(close_result, 'result'):
    close_result.result()
    return
    if hasattr(reader, 'close'):
    close_result = reader.close()
    if hasattr(close_result, 'result'):
    close_result.result()
    rendered = []
    for received in received_messages:
    data = getattr(received, 'data', None)
    if callable(data):
    data = data()
    rendered.append(data)
    raise AssertionError(f'Message `{message}` was not found in topic messages: {rendered}')

Full sources

See the full example: