diff --git a/olp-cpp-sdk-core/include/olp/core/client/ErrorCode.h b/olp-cpp-sdk-core/include/olp/core/client/ErrorCode.h index d552c4666..b6d3bdff9 100644 --- a/olp-cpp-sdk-core/include/olp/core/client/ErrorCode.h +++ b/olp-cpp-sdk-core/include/olp/core/client/ErrorCode.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2025 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,6 +101,11 @@ enum class ErrorCode { * Absence of network connectivity. */ Offline, + + /** + * The requested content does not exist in the requested resource. + */ + NoContent, }; } // namespace client diff --git a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp index e1d44e582..7b41ac79a 100644 --- a/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp +++ b/olp-cpp-sdk-dataservice-read/src/VersionedLayerClientImpl.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2025 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -341,8 +341,8 @@ client::CancellationToken VersionedLayerClientImpl::PrefetchPartitions( auto download = [=](std::string data_handle, client::CancellationContext inner_context) mutable { if (data_handle.empty()) { - return BlobApi::DataResponse( - client::ApiError(client::ErrorCode::NotFound, "Not found")); + return BlobApi::DataResponse(client::ApiError( + client::ErrorCode::NoContent, "Partition is empty")); } repository::DataCacheRepository data_cache_repository(catalog_, settings_.cache); @@ -533,7 +533,7 @@ client::CancellationToken VersionedLayerClientImpl::PrefetchTiles( client::CancellationContext inner_context) mutable { if (data_handle.empty()) { return BlobApi::DataResponse( - ApiError(ErrorCode::NotFound, "Not found")); + ApiError(ErrorCode::NoContent, "Partition is empty")); } repository::DataCacheRepository cache(catalog_, diff --git a/olp-cpp-sdk-dataservice-read/src/VolatileLayerClientImpl.cpp b/olp-cpp-sdk-dataservice-read/src/VolatileLayerClientImpl.cpp index 9508f0717..f7d864b24 100644 --- a/olp-cpp-sdk-dataservice-read/src/VolatileLayerClientImpl.cpp +++ b/olp-cpp-sdk-dataservice-read/src/VolatileLayerClientImpl.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -251,8 +251,8 @@ client::CancellationToken VolatileLayerClientImpl::PrefetchTiles( auto download = [=](std::string data_handle, client::CancellationContext inner_context) mutable { if (data_handle.empty()) { - return BlobApi::DataResponse( - client::ApiError(client::ErrorCode::NotFound, "Not found")); + return BlobApi::DataResponse(client::ApiError( + client::ErrorCode::NoContent, "Partition is empty")); } repository::DataCacheRepository data_cache_repository( catalog_, settings_.cache); diff --git a/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp b/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp index 9a0daf204..037fa3f4d 100644 --- a/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp +++ b/olp-cpp-sdk-dataservice-read/src/repositories/DataRepository.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -123,8 +123,9 @@ BlobApi::DataResponse DataRepository::GetVersionedData( catalog_.ToCatalogHRNString().c_str(), request.CreateKey(layer_id, version).c_str()); - return DataResponse(client::ApiError::NotFound("Partition not found"), - network_statistics); + return DataResponse( + client::ApiError(client::ErrorCode::NoContent, "Partition is empty"), + network_statistics); } partition = std::move(partitions.front()); @@ -281,7 +282,8 @@ BlobApi::DataResponse DataRepository::GetVolatileData( catalog_.ToCatalogHRNString().c_str(), request.CreateKey(layer_id, olp::porting::none).c_str()); - return client::ApiError::NotFound("Partition not found"); + return client::ApiError(client::ErrorCode::NoContent, + "Partition is empty"); } partition = std::move(partitions.front()); diff --git a/olp-cpp-sdk-dataservice-read/src/repositories/PartitionsRepository.cpp b/olp-cpp-sdk-dataservice-read/src/repositories/PartitionsRepository.cpp index d80c9f090..85e4549f3 100644 --- a/olp-cpp-sdk-dataservice-read/src/repositories/PartitionsRepository.cpp +++ b/olp-cpp-sdk-dataservice-read/src/repositories/PartitionsRepository.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ #include "PartitionsRepository.h" #include +#include #include #include @@ -169,6 +170,36 @@ bool CheckAdditionalFields( return true; } + +model::Partitions CreateNoContentPartitions( + const std::vector& partition_ids) { + model::Partitions result; + auto& partitions = result.GetMutablePartitions(); + partitions.reserve(partition_ids.size()); + + std::transform(partition_ids.cbegin(), partition_ids.cend(), + std::back_inserter(partitions), + [&](const std::string& partition_id) { + // Partition can't be empty, see `Partition::GetPartition`. + // Data handle is the only not optional field and actual data + // is requested by the data handle so not setting it is a + // sign that `Partition` object has no corresponding data. + model::Partition partition; + partition.SetPartition(partition_id); + return partition; + }); + + return result; +} + +bool HasNoContent(const model::Partition& partition) { + // There's client code that caches mocked partitions with zero size for not + // existing data so it's better to check all fields. + return partition.GetDataHandle().empty() && !partition.GetDataSize() && + !partition.GetCompressedDataSize() && !partition.GetChecksum() && + !partition.GetCrc() && !partition.GetVersion(); +} + } // namespace namespace olp { @@ -252,6 +283,16 @@ PartitionsRepository::GetPartitionsExtendedResponse( OLP_SDK_LOG_TRACE_F(kLogTag, "GetPartitions found in cache, hrn='%s', key='%s'", catalog_str.c_str(), key.c_str()); + + // Clear from cached NoContent partitions + auto& mutable_partitions = cached_partitions->GetMutablePartitions(); + mutable_partitions.erase( + std::remove_if(mutable_partitions.begin(), mutable_partitions.end(), + [](const model::Partition& partition) { + return HasNoContent(partition); + }), + mutable_partitions.end()); + return *cached_partitions; } else if (fetch_option == CacheOnly) { OLP_SDK_LOG_TRACE_F( @@ -302,8 +343,13 @@ PartitionsRepository::GetPartitionsExtendedResponse( OLP_SDK_LOG_TRACE_F(kLogTag, "GetPartitions put to cache, hrn='%s', key='%s'", catalog_str.c_str(), key.c_str()); + const auto put_result = - cache_.Put(response.GetResult(), version, expiry, is_layer_metadata); + cache_.Put(!response.GetResult().GetPartitions().empty() + ? response.GetResult() + : CreateNoContentPartitions(partition_ids), + version, expiry, is_layer_metadata); + if (!put_result.IsSuccessful() && fail_on_cache_error) { OLP_SDK_LOG_ERROR_F(kLogTag, "Failed to write data to cache, hrn='%s', key='%s'", @@ -356,6 +402,16 @@ PartitionsResponse PartitionsRepository::GetPartitionById( OLP_SDK_LOG_TRACE_F(kLogTag, "GetPartitionById found in cache, hrn='%s', key='%s'", catalog_.ToCatalogHRNString().c_str(), key.c_str()); + + // Clear from cached NoContent partitions + auto& mutable_partitions = cached_partitions.GetMutablePartitions(); + mutable_partitions.erase( + std::remove_if(mutable_partitions.begin(), mutable_partitions.end(), + [](const model::Partition& partition) { + return HasNoContent(partition); + }), + mutable_partitions.end()); + return cached_partitions; } else if (fetch_option == CacheOnly) { OLP_SDK_LOG_TRACE_F( @@ -383,8 +439,13 @@ PartitionsResponse PartitionsRepository::GetPartitionById( OLP_SDK_LOG_TRACE_F(kLogTag, "GetPartitionById put to cache, hrn='%s', key='%s'", catalog_.ToCatalogHRNString().c_str(), key.c_str()); + const auto put_result = - cache_.Put(query_response.GetResult(), version, olp::porting::none); + cache_.Put(!query_response.GetResult().GetPartitions().empty() + ? query_response.GetResult() + : CreateNoContentPartitions(partitions), + version, olp::porting::none); + if (!put_result.IsSuccessful()) { OLP_SDK_LOG_ERROR_F(kLogTag, "GetPartitionById failed to write data to cache, " diff --git a/olp-cpp-sdk-dataservice-read/tests/PartitionsRepositoryTest.cpp b/olp-cpp-sdk-dataservice-read/tests/PartitionsRepositoryTest.cpp index b728d2b05..00cf97b92 100644 --- a/olp-cpp-sdk-dataservice-read/tests/PartitionsRepositoryTest.cpp +++ b/olp-cpp-sdk-dataservice-read/tests/PartitionsRepositoryTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2025 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,6 +144,10 @@ const std::string kHttpResponseLookupQuery = const std::string kUrlQueryApi = R"(https://sab.query.data.api.platform.here.com/query/v1/catalogs/hrn:here:data::olp-here-test:hereos-internal-test-v2)"; +const std::string kUrlQueryVersionedPartition = + kUrlQueryApi + + R"(/layers/testlayer/partitions?partition=1111&version=100)"; + const std::string kQueryTreeIndexWithAdditionalFields = R"(https://sab.query.data.api.platform.here.com/query/v1/catalogs/hrn:here:data::olp-here-test:hereos-internal-test-v2/layers/testlayer/versions/100/quadkeys/23064/depths/4?additionalFields=)" + olp::utils::Url::Encode(R"(checksum,crc,dataSize,compressedDataSize)"); @@ -592,9 +596,6 @@ TEST_F(PartitionsRepositoryTest, GetPartitionById) { TEST_F(PartitionsRepositoryTest, GetVersionedPartitions) { using testing::Return; - std::shared_ptr default_cache = - client::OlpClientSettingsFactory::CreateDefaultCache({}); - auto mock_network = std::make_shared(); auto cache = std::make_shared>(); const auto catalog = HRN::FromString(kCatalog); @@ -641,6 +642,147 @@ TEST_F(PartitionsRepositoryTest, GetVersionedPartitions) { ASSERT_FALSE(response.IsSuccessful()); EXPECT_TRUE(response.GetResult().GetPartitions().empty()); } + { + SCOPED_TRACE( + "Succeeds the cache look up when one of the partitions has no content"); + OlpClientSettings settings; + settings.cache = cache; + settings.network_request_handler = mock_network; + settings.retry_settings.timeout = 1; + + const std::string cache_key_1 = + kCatalog + "::" + kVersionedLayerId + "::" + kPartitionId + + "::" + std::to_string(kVersion) + "::partition"; + + const std::string cache_key_2 = + kCatalog + "::" + kVersionedLayerId + "::" + kInvalidPartitionId + + "::" + std::to_string(kVersion) + "::partition"; + + const std::string query_cache_response = + R"jsonString({"version":100,"partition":"1111","layer":"testlayer","dataHandle":"qwerty"})jsonString"; + + const std::string no_content_cache_response = + R"jsonString({"partition":"2222"})jsonString"; + + auto response_data = std::make_shared( + query_cache_response.begin(), query_cache_response.end()); + + EXPECT_CALL(*cache, Read(cache_key_1)).WillOnce(Return(response_data)); + + EXPECT_CALL(*cache, Read(cache_key_2)) + .WillOnce(Return(std::make_shared( + no_content_cache_response.cbegin(), + no_content_cache_response.cend()))); + + client::CancellationContext context; + ApiLookupClient lookup_client(catalog, settings); + repository::PartitionsRepository repository(catalog, kVersionedLayerId, + settings, lookup_client); + + read::PartitionsRequest request; + request.WithPartitionIds({kPartitionId, kInvalidPartitionId}); + request.WithFetchOption(read::CacheOnly); + + auto response = repository.GetVersionedPartitionsExtendedResponse( + request, kVersion, context); + + ASSERT_TRUE(response.IsSuccessful()); + EXPECT_EQ(response.GetResult().GetPartitions().size(), 1); + } + { + SCOPED_TRACE("Cache utilised for not existing partitions"); + + OlpClientSettings settings; + settings.cache = cache; + settings.network_request_handler = mock_network; + settings.retry_settings.timeout = 1; + + const std::string cache_key_1 = + kCatalog + "::" + kVersionedLayerId + "::" + kPartitionId + + "::" + std::to_string(kVersion) + "::partition"; + + EXPECT_CALL(*cache, Read(cache_key_1)) + .WillOnce(Return(client::ApiError::NotFound())); + + EXPECT_CALL(*cache, Write(_, _, _)).WillOnce(Return(client::ApiNoResult())); + + EXPECT_CALL(*cache, Get(kCacheKeyMetadata, _)) + .WillOnce(Return(kUrlQueryApi)); + + EXPECT_CALL(*mock_network, + Send(IsGetRequest(kUrlQueryVersionedPartition), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + kOlpSdkHttpResponseEmptyPartitionList)); + + client::CancellationContext context; + ApiLookupClient lookup_client(catalog, settings); + repository::PartitionsRepository repository(catalog, kVersionedLayerId, + settings, lookup_client); + + read::PartitionsRequest request; + request.WithPartitionIds({kPartitionId}); + request.WithFetchOption(read::OnlineIfNotFound); + + auto response = repository.GetVersionedPartitionsExtendedResponse( + request, kVersion, context); + + ASSERT_TRUE(response.IsSuccessful()); + EXPECT_TRUE(response.GetResult().GetPartitions().empty()); + } + { + SCOPED_TRACE("Cache utilised for valid partition"); + + OlpClientSettings settings; + settings.cache = cache; + settings.network_request_handler = mock_network; + settings.retry_settings.timeout = 1; + + const std::string cache_key_1 = + kCatalog + "::" + kVersionedLayerId + "::" + kPartitionId + + "::" + std::to_string(kVersion) + "::partition"; + + EXPECT_CALL(*cache, Read(cache_key_1)) + .WillOnce(Return(client::ApiError::NotFound())); + + EXPECT_CALL(*cache, Write(_, _, _)) + .Times(4) + .WillRepeatedly(Return(client::ApiNoResult())); + + EXPECT_CALL(*cache, Get(kCacheKeyMetadata, _)) + .WillOnce(Return(kUrlQueryApi)); + + EXPECT_CALL(*mock_network, + Send(IsGetRequest(kUrlQueryVersionedPartition), _, _, _, _)) + .WillOnce(ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + kOlpSdkHttpResponsePartitions)); + + client::CancellationContext context; + ApiLookupClient lookup_client(catalog, settings); + repository::PartitionsRepository repository(catalog, kVersionedLayerId, + settings, lookup_client); + + read::PartitionsRequest request; + request.WithPartitionIds({kPartitionId}); + request.WithFetchOption(read::OnlineIfNotFound); + + auto response = repository.GetVersionedPartitionsExtendedResponse( + request, kVersion, context); + + ASSERT_TRUE(response.IsSuccessful()); + EXPECT_EQ(response.GetResult().GetPartitions().size(), 4); + } +} + +TEST_F(PartitionsRepositoryTest, GetVersionedPartitions_WithCache) { + using testing::Return; + + auto mock_network = std::make_shared(); + const auto catalog = HRN::FromString(kCatalog); + std::shared_ptr default_cache = + client::OlpClientSettingsFactory::CreateDefaultCache({}); + { SCOPED_TRACE("Successful fetch from network with a list of partitions"); diff --git a/olp-cpp-sdk-dataservice-read/tests/VolatileLayerClientImplTest.cpp b/olp-cpp-sdk-dataservice-read/tests/VolatileLayerClientImplTest.cpp index 08a49f47a..52d6424e5 100644 --- a/olp-cpp-sdk-dataservice-read/tests/VolatileLayerClientImplTest.cpp +++ b/olp-cpp-sdk-dataservice-read/tests/VolatileLayerClientImplTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ namespace read = olp::dataservice::read; namespace model = olp::dataservice::read::model; using ::testing::_; using ::testing::Mock; +using ::testing::Return; constexpr auto kUrlVolatileBlobData = R"(https://volatile-blob-ireland.data.api.platform.here.com/blobstore/v1/catalogs/hereos-internal-test-v2/layers/testlayer/data/4eed6ed1-0d32-43b9-ae79-043cb4256432)"; @@ -79,6 +80,12 @@ constexpr auto kUrlPrefetchBlobData1 = constexpr auto kUrlPrefetchBlobData2 = R"(https://volatile-blob-ireland.data.api.platform.here.com/blobstore/v1/catalogs/hereos-internal-test-v2/layers/testlayer/data/e83b397a-2be5-45a8-b7fb-ad4cb3ea13b1)"; +constexpr auto kCacheKeyPartition269 = + R"(hrn:here:data::olp-here-test:hereos-internal-test-v2::testlayer::269::partition)"; + +const std::string kNoContentPartition269Str = + R"({"dataHandle":"","partition":"269"})"; + const std::string kCatalog = "hrn:here:data::olp-here-test:hereos-internal-test-v2"; const std::string kLayerId = "testlayer"; @@ -109,13 +116,14 @@ TEST(VolatileLayerClientImplTest, GetData) { settings.cache = cache_mock; read::VolatileLayerClientImpl client(kHrn, kLayerId, settings); + EXPECT_CALL(*network_mock, Send(IsGetRequest(kUrlLookup), _, _, _, _)) + .WillRepeatedly( + ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( + olp::http::HttpStatusCode::OK), + kHttpResponseLookup)); + { SCOPED_TRACE("Get Data with DataHandle"); - EXPECT_CALL(*network_mock, Send(IsGetRequest(kUrlLookup), _, _, _, _)) - .WillRepeatedly( - ReturnHttpResponse(olp::http::NetworkResponse().WithStatus( - olp::http::HttpStatusCode::OK), - kHttpResponseLookup)); SetupNetworkExpectation(*network_mock, kUrlVolatileBlobData, "someData", olp::http::HttpStatusCode::OK); @@ -191,6 +199,40 @@ TEST(VolatileLayerClientImplTest, GetData) { kHttpResponseNoPartition, olp::http::HttpStatusCode::OK); + EXPECT_CALL(*cache_mock, Read(kCacheKeyPartition269)) + .WillOnce(Return(olp::client::ApiError::NotFound())); + + EXPECT_CALL(*cache_mock, Write(kCacheKeyPartition269, _, _)) + .WillOnce(Return(olp::client::ApiNoResult())); + + std::promise promise; + std::future future = promise.get_future(); + + auto token = client.GetData( + read::DataRequest().WithPartitionId(kPartitionId), + [&](read::DataResponse response) { promise.set_value(response); }); + + EXPECT_EQ(future.wait_for(kTimeout), std::future_status::ready); + + const auto& response = future.get(); + ASSERT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), + olp::client::ErrorCode::NoContent); + + Mock::VerifyAndClearExpectations(network_mock.get()); + } + + { + SCOPED_TRACE("Get Data from non existent partition utilizes cache"); + + auto expected_cached_value = + std::make_shared( + kNoContentPartition269Str.cbegin(), + kNoContentPartition269Str.cend()); + + EXPECT_CALL(*cache_mock, Read(kCacheKeyPartition269)) + .WillOnce(Return(expected_cached_value)); + std::promise promise; std::future future = promise.get_future(); @@ -203,7 +245,7 @@ TEST(VolatileLayerClientImplTest, GetData) { const auto& response = future.get(); ASSERT_FALSE(response.IsSuccessful()); EXPECT_EQ(response.GetError().GetErrorCode(), - olp::client::ErrorCode::NotFound); + olp::client::ErrorCode::NoContent); Mock::VerifyAndClearExpectations(network_mock.get()); } @@ -292,7 +334,7 @@ TEST(VolatileLayerClientImplTest, GetDataCancellableFuture) { const auto& response = future.get(); ASSERT_FALSE(response.IsSuccessful()); EXPECT_EQ(response.GetError().GetErrorCode(), - olp::client::ErrorCode::NotFound); + olp::client::ErrorCode::NoContent); Mock::VerifyAndClearExpectations(network_mock.get()); } @@ -677,7 +719,7 @@ TEST(VolatileLayerClientImplTest, PrefetchTiles) { for (auto& tile_result : result) { std::string str = tile_result->tile_key_.ToHereTile(); ASSERT_FALSE(tile_result->IsSuccessful()); - ASSERT_EQ(olp::client::ErrorCode::NotFound, + ASSERT_EQ(olp::client::ErrorCode::NoContent, tile_result->GetError().GetErrorCode()); ASSERT_TRUE(tile_result->tile_key_.IsValid()); } diff --git a/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp b/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp index 872e2ad7a..7f4637d89 100644 --- a/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp +++ b/tests/integration/olp-cpp-sdk-dataservice-read/VersionedLayerClientTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2024 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -2057,7 +2057,7 @@ TEST_F(DataserviceReadVersionedLayerClientTest, for (auto tile_result : response.GetResult()) { std::string str = tile_result->tile_key_.ToHereTile(); ASSERT_FALSE(tile_result->IsSuccessful()); - ASSERT_EQ(olp::client::ErrorCode::NotFound, + ASSERT_EQ(olp::client::ErrorCode::NoContent, tile_result->GetError().GetErrorCode()); ASSERT_TRUE(tile_result->tile_key_.IsValid()); }