From 8067e11dfade2497b8c57bef727807f789f1d6f5 Mon Sep 17 00:00:00 2001 From: Bulat Gayazov Date: Mon, 23 Sep 2024 02:33:09 +0000 Subject: [PATCH 1/6] Moved fixes from ydb repo --- examples/secondary_index/secondary_index_generate.cpp | 1 + src/client/topic/ut/ut_utils/topic_sdk_test_setup.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/secondary_index/secondary_index_generate.cpp b/examples/secondary_index/secondary_index_generate.cpp index 56084fc713..4307f112c9 100644 --- a/examples/secondary_index/secondary_index_generate.cpp +++ b/examples/secondary_index/secondary_index_generate.cpp @@ -2,6 +2,7 @@ #include +#include #include using namespace NLastGetopt; diff --git a/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h b/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h index 349b5ab655..3b151d09f2 100644 --- a/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h +++ b/src/client/topic/ut/ut_utils/topic_sdk_test_setup.h @@ -21,7 +21,7 @@ class TTopicSdkTestSetup { void CreateTopicWithAutoscale(const TString& path = TString{TEST_TOPIC}, const TString& consumer = TEST_CONSUMER, size_t partitionCount = 1, size_t maxPartitionCount = 100); - void DescribeTopic(const TString& path = TEST_TOPIC); + void DescribeTopic(const TString& path = TString{TEST_TOPIC}); TString GetEndpoint() const; TString GetTopicPath(const TString& name = TString{TEST_TOPIC}) const; From 636214144b9b34a72b1b215471024462c05e2b19 Mon Sep 17 00:00:00 2001 From: Vasily Gerasimov Date: Mon, 23 Sep 2024 02:40:14 +0000 Subject: [PATCH 2/6] Moved commit "Support cancel after in rate limiter" from ydb repo --- include/ydb-cpp-sdk/client/rate_limiter/rate_limiter.h | 5 +++++ src/api/protos/ydb_rate_limiter.proto | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/include/ydb-cpp-sdk/client/rate_limiter/rate_limiter.h b/include/ydb-cpp-sdk/client/rate_limiter/rate_limiter.h index aac78b0136..406776c092 100644 --- a/include/ydb-cpp-sdk/client/rate_limiter/rate_limiter.h +++ b/include/ydb-cpp-sdk/client/rate_limiter/rate_limiter.h @@ -159,6 +159,11 @@ class TRateLimiterClient { TAsyncDescribeResourceResult DescribeResource(const std::string& coordinationNodePath, const std::string& resourcePath, const TDescribeResourceSettings& = {}); // Acquire resources's units inside a coordination node. + // If CancelAfter is set greater than zero and less than OperationTimeout + // and resource is not ready after CancelAfter time, + // the result code of this operation will be CANCELLED and resource will not be spent. + // It is recommended to specify both OperationTimeout and CancelAfter. + // CancelAfter should be less than OperationTimeout. TAsyncStatus AcquireResource(const std::string& coordinationNodePath, const std::string& resourcePath, const TAcquireResourceSettings& = {}); private: diff --git a/src/api/protos/ydb_rate_limiter.proto b/src/api/protos/ydb_rate_limiter.proto index eba14010cf..74e05772ea 100644 --- a/src/api/protos/ydb_rate_limiter.proto +++ b/src/api/protos/ydb_rate_limiter.proto @@ -266,6 +266,11 @@ message DescribeResourceResult { // message AcquireResourceRequest { + // If cancel_after is set greater than zero and less than operation_timeout + // and resource is not ready after cancel_after time, + // the result code of this operation will be CANCELLED and resource will not be spent. + // It is recommended to specify both operation_timeout and cancel_after. + // cancel_after should be less than operation_timeout and non zero. Ydb.Operations.OperationParams operation_params = 1; // Path of a coordination node. From 2e6c8835c509b22b7ad714375d2c1db91fd0650a Mon Sep 17 00:00:00 2001 From: Golear Dimitris Date: Mon, 23 Sep 2024 02:45:22 +0000 Subject: [PATCH 3/6] Moved commit "Remove implicit casts from nullptr to TString" from ydb repo --- src/client/topic/ut/local_partition_ut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/topic/ut/local_partition_ut.cpp b/src/client/topic/ut/local_partition_ut.cpp index 94165c9b38..4ff64776ba 100644 --- a/src/client/topic/ut/local_partition_ut.cpp +++ b/src/client/topic/ut/local_partition_ut.cpp @@ -202,7 +202,7 @@ namespace NYdb::NTopic::NTests { private: Ydb::Discovery::ListEndpointsResult MockResults; - TString DiscoveryAddr = 0; + TString DiscoveryAddr; std::unique_ptr Server; TAdaptiveLock Lock; From 0e04d7c19b631333c8b429f2011865e62972c50a Mon Sep 17 00:00:00 2001 From: Alek5andr-Kotov Date: Mon, 23 Sep 2024 15:55:19 +0000 Subject: [PATCH 4/6] Moved commit "The consumer's generation number is not stored in the transaction" from ydb repo --- src/client/topic/ut/topic_to_table_ut.cpp | 117 +++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/client/topic/ut/topic_to_table_ut.cpp b/src/client/topic/ut/topic_to_table_ut.cpp index d323d3c1fe..c16201066b 100644 --- a/src/client/topic/ut/topic_to_table_ut.cpp +++ b/src/client/topic/ut/topic_to_table_ut.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -42,8 +43,14 @@ class TFixture : public NUnitTest::TBaseFixture { void WaitForEvent(); }; + struct TFeatureFlags { + bool EnablePQConfigTransactionsAtSchemeShard = true; + }; + void SetUp(NUnitTest::TTestContext&) override; + void NotifySchemeShard(const TFeatureFlags& flags); + NTable::TSession CreateTableSession(); NTable::TTransaction BeginTx(NTable::TSession& session); void CommitTx(NTable::TTransaction& tx, EStatus status = EStatus::SUCCESS); @@ -65,9 +72,10 @@ class TFixture : public NUnitTest::TBaseFixture { const TString& consumer = TEST_CONSUMER, size_t partitionCount = 1, std::optional maxPartitionCount = std::nullopt); - void DescribeTopic(const TString& path); + void AddConsumer(const TString& topic, const TVector& consumers); + void WriteToTopicWithInvalidTxId(bool invalidTxId); TTopicWriteSessionPtr CreateTopicWriteSession(const TString& topicPath, @@ -102,6 +110,8 @@ class TFixture : public NUnitTest::TBaseFixture { NYdb::EStatus status); void CloseTopicWriteSession(const TString& topicPath, const TString& messageGroupId); + void CloseTopicReadSession(const TString& topicPath, + const TString& consumerName); enum EEndOfTransaction { Commit, @@ -182,6 +192,8 @@ class TFixture : public NUnitTest::TBaseFixture { ui64 tabletId, const NPQ::TWriteId& writeId); + ui64 GetSchemeShardTabletId(const TActorId& actorId); + std::unique_ptr Setup; std::unique_ptr Driver; @@ -199,11 +211,27 @@ void TFixture::SetUp(NUnitTest::TTestContext&) { NKikimr::Tests::TServerSettings settings = TTopicSdkTestSetup::MakeServerSettings(); settings.SetEnableTopicServiceTx(true); + Setup = std::make_unique(TEST_CASE_NAME, settings); Driver = std::make_unique(Setup->MakeDriver()); } +void TFixture::NotifySchemeShard(const TFeatureFlags& flags) +{ + auto request = std::make_unique(); + *request->Record.MutableConfig() = *Setup->GetServer().ServerSettings.AppConfig; + request->Record.MutableConfig()->MutableFeatureFlags()->SetEnablePQConfigTransactionsAtSchemeShard(flags.EnablePQConfigTransactionsAtSchemeShard); + + auto& runtime = Setup->GetRuntime(); + auto actorId = runtime.AllocateEdgeActor(); + + ui64 ssId = GetSchemeShardTabletId(actorId); + + runtime.SendToPipe(ssId, actorId, request.release()); + runtime.GrabEdgeEvent(); +} + NTable::TSession TFixture::CreateTableSession() { NTable::TTableClient client(GetDriver()); @@ -330,6 +358,20 @@ void TFixture::CreateTopic(const TString& path, Setup->CreateTopic(path, consumer, partitionCount, maxPartitionCount); } +void TFixture::AddConsumer(const TString& path, + const TVector& consumers) +{ + NTopic::TTopicClient client(GetDriver()); + NTopic::TAlterTopicSettings settings; + + for (const auto& consumer : consumers) { + settings.BeginAddConsumer(consumer); + } + + auto result = client.AlterTopic(path, settings).GetValueSync(); + UNIT_ASSERT_C(result.IsSuccess(), result.GetIssues().ToString()); +} + void TFixture::DescribeTopic(const TString& path) { Setup->DescribeTopic(path); @@ -664,6 +706,13 @@ void TFixture::CloseTopicWriteSession(const TString& topicPath, TopicWriteSessions.erase(key); } +void TFixture::CloseTopicReadSession(const TString& topicPath, + const TString& consumerName) +{ + Y_UNUSED(consumerName); + TopicReadSessions.erase(topicPath); +} + void TFixture::WriteToTopic(const TString& topicPath, const TString& messageGroupId, const TString& message, @@ -780,6 +829,37 @@ void TFixture::WaitForSessionClose(const TString& topicPath, UNIT_ASSERT(context.AckCount() <= context.WriteCount); } +ui64 TFixture::GetSchemeShardTabletId(const TActorId& actorId) +{ + auto navigate = std::make_unique(); + navigate->DatabaseName = "/Root"; + + NSchemeCache::TSchemeCacheNavigate::TEntry entry; + entry.Path = SplitPath("/Root"); + entry.SyncVersion = true; + entry.ShowPrivatePath = true; + entry.Operation = NSchemeCache::TSchemeCacheNavigate::OpList; + + navigate->ResultSet.push_back(std::move(entry)); + //navigate->UserToken = "root@builtin"; + navigate->Cookie = 12345; + + auto& runtime = Setup->GetRuntime(); + + runtime.Send(MakeSchemeCacheID(), actorId, + new TEvTxProxySchemeCache::TEvNavigateKeySet(navigate.release()), + 0, + true); + auto response = runtime.GrabEdgeEvent(); + + UNIT_ASSERT_VALUES_EQUAL(response->Request->Cookie, 12345); + UNIT_ASSERT_VALUES_EQUAL(response->Request->ErrorCount, 0); + + auto& front = response->Request->ResultSet.front(); + + return front.Self->Info.GetSchemeshardId(); +} + ui64 TFixture::GetTopicTabletId(const TActorId& actorId, const TString& topicPath, ui32 partition) { auto navigate = std::make_unique(); @@ -2017,6 +2097,41 @@ Y_UNIT_TEST_F(WriteToTopic_Demo_38, TFixture) WriteMessagesInTx(0, 1); } +Y_UNIT_TEST_F(ReadRuleGeneration, TFixture) +{ + // There was a server + NotifySchemeShard({.EnablePQConfigTransactionsAtSchemeShard = false}); + + // Users have created their own topic on it + CreateTopic(TEST_TOPIC); + + // And they wrote their messages into it + WriteToTopic(TEST_TOPIC, TEST_MESSAGE_GROUP_ID, "message-1"); + WriteToTopic(TEST_TOPIC, TEST_MESSAGE_GROUP_ID, "message-2"); + WriteToTopic(TEST_TOPIC, TEST_MESSAGE_GROUP_ID, "message-3"); + + // And he had a consumer + AddConsumer(TEST_TOPIC, {"consumer-1"}); + + // We read messages from the topic and committed offsets + auto messages = ReadFromTopic(TEST_TOPIC, "consumer-1", TDuration::Seconds(2)); + UNIT_ASSERT_VALUES_EQUAL(messages.size(), 3); + CloseTopicReadSession(TEST_TOPIC, "consumer-1"); + + // And then the Logbroker team turned on the feature flag + NotifySchemeShard({.EnablePQConfigTransactionsAtSchemeShard = true}); + + // Users continued to write to the topic + WriteToTopic(TEST_TOPIC, TEST_MESSAGE_GROUP_ID, "message-4"); + + // Users have added new consumers + AddConsumer(TEST_TOPIC, {"consumer-2"}); + + // And they wanted to continue reading their messages + messages = ReadFromTopic(TEST_TOPIC, "consumer-1", TDuration::Seconds(2)); + UNIT_ASSERT_VALUES_EQUAL(messages.size(), 1); +} + } } From 25ef694553ff6de174e8e15053558f723f68e3df Mon Sep 17 00:00:00 2001 From: Nikolay Shumkov Date: Mon, 23 Sep 2024 16:20:25 +0000 Subject: [PATCH 5/6] Moved commit "Support ydb dump for tables with serial types" from ydb repo --- include/ydb-cpp-sdk/client/table/table.h | 20 ++++++++- src/api/protos/ydb_table.proto | 2 + src/client/table/impl/table_client.cpp | 4 ++ src/client/table/table.cpp | 57 +++++++++++++++++++----- 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/include/ydb-cpp-sdk/client/table/table.h b/include/ydb-cpp-sdk/client/table/table.h index 82ae8c7291..7f98bc6a2c 100644 --- a/include/ydb-cpp-sdk/client/table/table.h +++ b/include/ydb-cpp-sdk/client/table/table.h @@ -98,19 +98,33 @@ class TKeyRange { std::optional To_; }; +struct TSequenceDescription { + struct TSetVal { + int64_t NextValue; + bool NextUsed; + }; + std::optional SetVal; +}; + struct TTableColumn { std::string Name; TType Type; std::string Family; std::optional NotNull; + std::optional SequenceDescription; TTableColumn() = default; - TTableColumn(std::string name, TType type, std::string family = std::string(), std::optional notNull = std::nullopt) + TTableColumn(std::string name, + TType type, + std::string family = std::string(), + std::optional notNull = std::nullopt, + std::optional sequenceDescription = std::nullopt) : Name(std::move(name)) , Type(std::move(type)) , Family(std::move(family)) , NotNull(std::move(notNull)) + , SequenceDescription(std::move(sequenceDescription)) { } // Conversion from TColumn for API compatibility @@ -634,7 +648,7 @@ class TTableDescription { TTableDescription(); explicit TTableDescription(const Ydb::Table::CreateTableRequest& request); - void AddColumn(const std::string& name, const Ydb::Type& type, const std::string& family, std::optional notNull); + void AddColumn(const std::string& name, const Ydb::Type& type, const std::string& family, std::optional notNull, std::optional sequenceDescription); void SetPrimaryKeyColumns(const std::vector& primaryKeyColumns); // common @@ -852,6 +866,7 @@ class TTableBuilder { TTableBuilder& AddNonNullableColumn(const std::string& name, const TPgType& type, const std::string& family = std::string()); TTableBuilder& SetPrimaryKeyColumns(const std::vector& primaryKeyColumns); TTableBuilder& SetPrimaryKeyColumn(const std::string& primaryKeyColumn); + TTableBuilder& AddSerialColumn(const std::string& name, const EPrimitiveType& type, TSequenceDescription sequenceDescription, const std::string& family = std::string()); // common TTableBuilder& AddSecondaryIndex(const TIndexDescription& indexDescription); @@ -1627,6 +1642,7 @@ struct TDescribeTableSettings : public TOperationRequestSettings { diff --git a/src/api/protos/ydb_table.proto b/src/api/protos/ydb_table.proto index 7635437b32..9f0da934cd 100644 --- a/src/api/protos/ydb_table.proto +++ b/src/api/protos/ydb_table.proto @@ -779,6 +779,8 @@ message DescribeTableRequest { bool include_table_stats = 6; // Includes partition statistics (required include_table_statistics) bool include_partition_stats = 7; + // Includes set_val settings for sequences + bool include_set_val = 8; } message DescribeTableResponse { diff --git a/src/client/table/impl/table_client.cpp b/src/client/table/impl/table_client.cpp index 68734dd928..b5863abf8f 100644 --- a/src/client/table/impl/table_client.cpp +++ b/src/client/table/impl/table_client.cpp @@ -525,6 +525,10 @@ TAsyncDescribeTableResult TTableClient::TImpl::DescribeTable(const std::string& request.set_include_partition_stats(true); } + if (settings.WithSetVal_) { + request.set_include_set_val(true); + } + auto promise = NewPromise(); auto extractor = [promise, settings] diff --git a/src/client/table/table.cpp b/src/client/table/table.cpp index 67df1cf1cc..1d13e5dddf 100644 --- a/src/client/table/table.cpp +++ b/src/client/table/table.cpp @@ -291,7 +291,24 @@ class TTableDescription::TImpl { if (col.has_not_null()) { not_null = col.not_null(); } - Columns_.emplace_back(col.name(), col.type(), col.family(), not_null); + std::optional sequenceDescription; + switch (col.default_value_case()) { + case Ydb::Table::ColumnMeta::kFromSequence: { + if (col.from_sequence().name() == "_serial_column_" + col.name()) { + TSequenceDescription currentSequenceDescription; + if (col.from_sequence().has_set_val()) { + TSequenceDescription::TSetVal setVal; + setVal.NextUsed = col.from_sequence().set_val().next_used(); + setVal.NextValue = col.from_sequence().set_val().next_value(); + currentSequenceDescription.SetVal = std::move(setVal); + } + sequenceDescription = std::move(currentSequenceDescription); + } + break; + } + default: break; + } + Columns_.emplace_back(col.name(), col.type(), col.family(), not_null, std::move(sequenceDescription)); } // indexes @@ -453,8 +470,8 @@ class TTableDescription::TImpl { return Proto_; } - void AddColumn(const std::string& name, const Ydb::Type& type, const std::string& family, std::optional notNull) { - Columns_.emplace_back(name, type, family, notNull); + void AddColumn(const std::string& name, const Ydb::Type& type, const std::string& family, std::optional notNull, std::optional sequenceDescription) { + Columns_.emplace_back(name, type, family, notNull, std::move(sequenceDescription)); } void SetPrimaryKeyColumns(const std::vector& primaryKeyColumns) { @@ -738,8 +755,8 @@ const std::vector& TTableDescription::GetKeyRanges() const { return Impl_->GetKeyRanges(); } -void TTableDescription::AddColumn(const std::string& name, const Ydb::Type& type, const std::string& family, std::optional notNull) { - Impl_->AddColumn(name, type, family, notNull); +void TTableDescription::AddColumn(const std::string& name, const Ydb::Type& type, const std::string& family, std::optional notNull, std::optional sequenceDescription) { + Impl_->AddColumn(name, type, family, notNull, std::move(sequenceDescription)); } void TTableDescription::SetPrimaryKeyColumns(const std::vector& primaryKeyColumns) { @@ -915,6 +932,15 @@ void TTableDescription::SerializeTo(Ydb::Table::CreateTableRequest& request) con if (column.NotNull.has_value()) { protoColumn.set_not_null(column.NotNull.value()); } + if (column.SequenceDescription.has_value()) { + auto* fromSequence = protoColumn.mutable_from_sequence(); + if (column.SequenceDescription->SetVal.has_value()) { + auto* setVal = fromSequence->mutable_set_val(); + setVal->set_next_value(column.SequenceDescription->SetVal->NextValue); + setVal->set_next_used(column.SequenceDescription->SetVal->NextUsed); + } + fromSequence->set_name("_serial_column_" + column.Name); + } } for (const auto& pk : Impl_->GetPrimaryKeyColumns()) { @@ -1122,7 +1148,7 @@ TTableBuilder& TTableBuilder::AddNullableColumn(const std::string& name, const E .EndOptional() .Build(); - TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, false); + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, false, std::nullopt); return *this; } @@ -1132,7 +1158,7 @@ TTableBuilder& TTableBuilder::AddNullableColumn(const std::string& name, const T .Decimal(type) .EndOptional() .Build(); - TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, false); + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, false, std::nullopt); return *this; } @@ -1141,7 +1167,7 @@ TTableBuilder& TTableBuilder::AddNullableColumn(const std::string& name, const T .Pg(type) .Build(); - TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, false); + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, false, std::nullopt); return *this; } @@ -1150,7 +1176,7 @@ TTableBuilder& TTableBuilder::AddNonNullableColumn(const std::string& name, cons .Primitive(type) .Build(); - TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true); + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true, std::nullopt); return *this; } @@ -1159,7 +1185,7 @@ TTableBuilder& TTableBuilder::AddNonNullableColumn(const std::string& name, cons .Decimal(type) .Build(); - TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true); + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true, std::nullopt); return *this; } @@ -1168,7 +1194,16 @@ TTableBuilder& TTableBuilder::AddNonNullableColumn(const std::string& name, cons .Pg(type) .Build(); - TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true); + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true, std::nullopt); + return *this; +} + +TTableBuilder& TTableBuilder::AddSerialColumn(const std::string& name, const EPrimitiveType& type, TSequenceDescription sequenceDescription, const std::string& family) { + auto columnType = TTypeBuilder() + .Primitive(type) + .Build(); + + TableDescription_.AddColumn(name, TProtoAccessor::GetProto(columnType), family, true, std::move(sequenceDescription)); return *this; } From 51d9070cffd91da4cec5ad32954101ae0e4a1036 Mon Sep 17 00:00:00 2001 From: Alek5andr-Kotov Date: Mon, 23 Sep 2024 16:22:34 +0000 Subject: [PATCH 6/6] Moved commit "The TEvProposePartitionConfig message is sent only to the main partitions" from ydb repo --- src/client/topic/ut/topic_to_table_ut.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/client/topic/ut/topic_to_table_ut.cpp b/src/client/topic/ut/topic_to_table_ut.cpp index c16201066b..55d4ffb6e0 100644 --- a/src/client/topic/ut/topic_to_table_ut.cpp +++ b/src/client/topic/ut/topic_to_table_ut.cpp @@ -211,6 +211,7 @@ void TFixture::SetUp(NUnitTest::TTestContext&) { NKikimr::Tests::TServerSettings settings = TTopicSdkTestSetup::MakeServerSettings(); settings.SetEnableTopicServiceTx(true); + settings.SetEnablePQConfigTransactionsAtSchemeShard(true); Setup = std::make_unique(TEST_CASE_NAME, settings); @@ -2097,6 +2098,26 @@ Y_UNIT_TEST_F(WriteToTopic_Demo_38, TFixture) WriteMessagesInTx(0, 1); } +Y_UNIT_TEST_F(WriteToTopic_Demo_39, TFixture) +{ + CreateTopic("topic_A", TEST_CONSUMER); + + NTable::TSession tableSession = CreateTableSession(); + NTable::TTransaction tx = BeginTx(tableSession); + + WriteToTopic("topic_A", TEST_MESSAGE_GROUP_ID, "message #1", &tx); + WriteToTopic("topic_A", TEST_MESSAGE_GROUP_ID, "message #2", &tx); + + WaitForAcks("topic_A", TEST_MESSAGE_GROUP_ID); + + AddConsumer("topic_A", {"consumer"}); + + CommitTx(tx, EStatus::SUCCESS); + + auto messages = ReadFromTopic("topic_A", "consumer", TDuration::Seconds(2)); + UNIT_ASSERT_VALUES_EQUAL(messages.size(), 2); +} + Y_UNIT_TEST_F(ReadRuleGeneration, TFixture) { // There was a server