From 9a60180f9d6f52a4ca805e5463ecc9e5e80e88f9 Mon Sep 17 00:00:00 2001 From: Privacy Sandbox Team Date: Fri, 5 Apr 2024 19:08:18 +0000 Subject: [PATCH] Release 0.16.0 (2024-04-05) ### Features * Add cache hit or miss metrics * Add coorindator specific terraform parameters * Add data loading prefix allowlist parameter * Add default PAS UDF * Add E2E latency for GetKeyValues and GetKeyValueSet in sharded lookup * Add file groups and file group reader logic * Add go fmt to pre-commit * Add key prefix support to blob storage client * Add LogContext and ConsentedDebugConfiguration proto to v2 API and internal lookup API * Add prod and nonprod build flag * Add request context to wrap metrics context * Add support for configuring directory allowlist * Add wiring for prefix allowlist (actual impl in follow up cl) * Allow overrides for coordinators endpoints in nonprod mode * Allow to disable v1 key not found entry in response * Create separate metrics context map for internal lookup server * Deprecate metrics recorder for internal lookup * Deprecate metrics recorder for internal server * Deprecate metrics recorder for sharded lookup * deprecate metrics recorder for V1 server and handler * Deprecate metrics recorder from cache * Enable simulation system send realtime udpates * Enable TCMalloc for KV Server and benchmarks * Explicitly enable core dumps * Implement deletion cutoff max timestamp per directory * Load data files and allow notifications from configured prefix * Load prefix files on startup and handle prefix blob notifications * Log common request metrics * Migrate from glog to absl log * Partition data loading metrics by delta file name * Pass request context from hooks to downstream components * Pass request context to udf hooks * Read telemetry config from cloud parameter * Revamp AWS metrics dashboard * Revamp GCP metrics dashboard * Set udf_min_log_level from parameter store. * Support content type proto for v2 api * Support content type proto for v2 api response * Update cache interface and blob data location to pass prefix * Update start_after to use a map from prefix to start_after * Use file groups for loading snapshots * Write logs to an Otel endpoint ### Bug Fixes * Actually load all files in a snapshot file group * **AWS:** Filter out unavailable zones. * Correct an error in kokoro_release. * Correct format for image tag. * Correct typo for internal dev's service_mesh_address. * Correct typos in GCP deployment guide. * Crash server if default UDF fails to load. * Delete non-active certificate before creating a new one. * Fix filtering logic for prefixed blobs * Fix permissions for data-loading-blob-prefix-allowlist * Make GCP nat optional. * Parse delta filename from notification before validating it * Remove glog dependency for record_utils * Remove temp dir only if it's successfully created. * Rename class to ThreadManager * Set retain_initial_value_of_delta_metric flag for aws metrics exporter * Update a outdated hyperlink. * Update common repo to pick up the AWS metrics dimension fix * Update GCP Terraform with ability to delete unhealthy instance. * Update tf variables to use gorekore instead of kelvingorekore * Use blob key instead of prefixed basename ### GCP: Fixes * **GCP:** Make sure server is connected to otel collector before reaching to ready state ### GCP: Features * **GCP:** Applying Terraform pulls docker image with new tag. * **GCP:** Make service mesh address configurable. * **GCP:** Make subnet ip cidr configurable. * **GCP:** Make xlb/envoy optional. ### Documentation * Add ad retrieval explainer. * Add docs for directory support * Add PA and PAS folders * Add PAS developer guide * Add public docs for file groups * Ads retreival explainer update. ### Dependencies * **deps:** Add clang-tidy bazel config * **deps:** Add cpp_nowarn bazel config * **deps:** Upgrade bazel to 6.5.0 * **deps:** Upgrade build-system to 0.55.1 * **deps:** Upgrade build-system to 0.55.2 * **deps:** Upgrade build-system to 0.57.0 * **deps:** Upgrade data-plane-shared repo * **deps:** Upgrade data-plane-shared repo to 1684674 2024-02-09 * **deps:** Upgrade data-plane-shared-libraries to 1fbac46 * **deps:** Upgrade pre-commit hooks Bug: N/A Change-Id: If188118c8459f412bcedaa2e2ee670f8c0045727 GitOrigin-RevId: 7e6c7c71d308a2c0f6401af2b96c5acedfd39f58 --- .bazelrc | 27 +- .bazelversion | 2 +- .pre-commit-config.yaml | 24 +- .precommit_configs/markdown-link-check.json | 27 + BUILD.bazel | 64 + CHANGELOG.md | 110 ++ README.md | 16 +- WORKSPACE | 43 +- builders/CHANGELOG.md | 108 ++ builders/etc/.clang-format | 4 +- builders/etc/.clang-tidy | 8 + builders/etc/.pylintrc | 118 ++ .../images/build-amazonlinux2/install_apps | 6 + .../images/build-amazonlinux2023/install_apps | 6 + builders/images/build-debian/install_apps | 25 +- builders/images/install_golang_apps | 8 +- builders/images/presubmit/install_apps | 15 +- builders/images/test-tools/Dockerfile | 31 +- builders/images/test-tools/build_wrk | 27 + builders/tests/data/hashes/build-amazonlinux2 | 2 +- .../tests/data/hashes/build-amazonlinux2023 | 2 +- builders/tests/data/hashes/build-debian | 2 +- builders/tests/data/hashes/presubmit | 2 +- builders/tests/data/hashes/test-tools | 2 +- builders/tests/run-tests | 4 +- builders/tools/awscurl | 17 +- builders/tools/cbuild | 39 +- builders/tools/collect-coverage | 50 +- builders/tools/normalize-bazel-symlinks | 30 +- builders/tools/pre-commit | 5 +- builders/tools/wrk2 | 1 + builders/version.txt | 2 +- components/cloud_config/BUILD.bazel | 33 +- .../cloud_config/instance_client_aws.cc | 18 +- .../cloud_config/instance_client_gcp.cc | 10 +- .../cloud_config/instance_client_local.cc | 2 +- .../cloud_config/parameter_client_aws.cc | 2 +- .../cloud_config/parameter_client_gcp.cc | 11 +- .../cloud_config/parameter_client_gcp_test.cc | 6 +- .../cloud_config/parameter_client_local.cc | 24 +- .../parameter_client_local_test.cc | 24 + components/data/blob_storage/BUILD.bazel | 44 +- .../blob_storage/blob_prefix_allowlist.cc | 68 + .../data/blob_storage/blob_prefix_allowlist.h | 66 + .../blob_prefix_allowlist_test.cc | 62 + .../blob_storage_change_notifier_s3.cc | 7 +- .../blob_storage_change_notifier_s3_test.cc | 2 +- .../data/blob_storage/blob_storage_client.h | 11 +- .../blob_storage/blob_storage_client_gcp.cc | 42 +- .../blob_storage_client_gcp_test.cc | 28 +- .../blob_storage/blob_storage_client_local.cc | 15 +- .../blob_storage_client_local_test.cc | 41 + .../blob_storage/blob_storage_client_s3.cc | 28 +- .../blob_storage_client_s3_test.cc | 64 +- .../data/blob_storage/delta_file_notifier.cc | 275 ++-- .../data/blob_storage/delta_file_notifier.h | 15 +- .../blob_storage/delta_file_notifier_test.cc | 135 +- .../blob_storage/seeking_input_streambuf.cc | 35 +- .../blob_storage/seeking_input_streambuf.h | 2 +- .../seeking_input_streambuf_test.cc | 1 - components/data/common/BUILD.bazel | 14 +- components/data/common/change_notifier_aws.cc | 36 +- .../data/common/change_notifier_aws_test.cc | 2 +- components/data/common/change_notifier_gcp.cc | 2 +- .../data/common/change_notifier_gcp_test.cc | 3 +- .../data/common/change_notifier_local.cc | 2 +- .../data/common/change_notifier_local_test.cc | 3 +- components/data/common/mocks.h | 9 +- components/data/common/msg_svc_gcp.cc | 2 +- components/data/common/thread_manager.cc | 14 +- components/data/common/thread_manager.h | 8 +- components/data/file_group/BUILD.bazel | 73 + components/data/file_group/file_group.cc | 141 ++ components/data/file_group/file_group.h | 88 + .../file_group/file_group_search_utils.cc | 100 ++ .../data/file_group/file_group_search_utils.h | 66 + .../file_group_search_utils_test.cc | 152 ++ components/data/file_group/file_group_test.cc | 122 ++ components/data/realtime/BUILD.bazel | 13 +- .../delta_file_record_change_notifier.h | 2 +- .../delta_file_record_change_notifier_aws.cc | 11 +- ...ta_file_record_change_notifier_aws_test.cc | 1 - ...delta_file_record_change_notifier_local.cc | 2 +- ..._file_record_change_notifier_local_test.cc | 1 - .../data/realtime/realtime_notifier_aws.cc | 58 +- .../realtime/realtime_notifier_aws_test.cc | 1 - .../data/realtime/realtime_notifier_gcp.cc | 26 +- .../realtime/realtime_notifier_gcp_test.cc | 1 - .../realtime/realtime_thread_pool_manager.h | 2 +- .../realtime_thread_pool_manager_aws_test.cc | 1 - .../realtime_thread_pool_manager_gcp_test.cc | 1 - components/data_server/cache/BUILD.bazel | 7 +- components/data_server/cache/cache.h | 34 +- .../data_server/cache/key_value_cache.cc | 193 ++- .../data_server/cache/key_value_cache.h | 93 +- .../data_server/cache/key_value_cache_test.cc | 741 +++++---- components/data_server/cache/mocks.h | 20 +- .../data_server/cache/noop_key_value_cache.h | 17 +- .../data_server/data_loading/BUILD.bazel | 10 +- .../data_loading/data_orchestrator.cc | 329 ++-- .../data_loading/data_orchestrator.h | 2 + .../data_loading/data_orchestrator_test.cc | 77 +- .../data_server/request_handler/BUILD.bazel | 41 +- .../request_handler/compression.cc | 2 +- .../request_handler/compression_brotli.cc | 2 +- .../compression_brotli_test.cc | 2 +- .../request_handler/get_values_adapter.cc | 4 +- .../get_values_adapter_test.cc | 34 +- .../request_handler/get_values_handler.cc | 59 +- .../request_handler/get_values_handler.h | 17 +- .../get_values_handler_test.cc | 71 +- .../request_handler/get_values_v2_handler.cc | 80 +- .../request_handler/get_values_v2_handler.h | 33 +- .../get_values_v2_handler_test.cc | 165 +- .../request_handler/ohttp_client_encryptor.cc | 8 +- .../request_handler/ohttp_client_encryptor.h | 9 +- .../request_handler/ohttp_encryptor_test.cc | 6 +- .../request_handler/ohttp_server_encryptor.cc | 2 +- .../request_handler/ohttp_server_encryptor.h | 2 +- .../request_handler/uncompressed.cc | 2 +- components/data_server/server/BUILD.bazel | 92 +- .../data_server/server/key_fetcher_factory.h | 6 +- .../server/key_fetcher_factory_cloud.cc | 65 +- .../server/key_fetcher_factory_gcp.cc | 33 +- .../server/key_fetcher_factory_local.cc | 2 +- .../server/key_fetcher_utils_gcp.cc | 50 + .../server/key_fetcher_utils_gcp.h | 44 + .../server/key_value_service_impl.cc | 28 +- .../server/key_value_service_impl.h | 13 +- .../server/key_value_service_v2_impl.cc | 6 +- .../server/key_value_service_v2_impl.h | 8 +- .../data_server/server/lifecycle_heartbeat.cc | 2 +- .../server/lifecycle_heartbeat_test.cc | 1 - components/data_server/server/main.cc | 10 +- components/data_server/server/mocks.h | 1 - .../server/nonprod_key_fetcher_factory_aws.cc | 21 + .../nonprod_key_fetcher_factory_cloud.cc | 48 + .../nonprod_key_fetcher_factory_cloud.h | 46 + .../server/nonprod_key_fetcher_factory_gcp.cc | 53 + .../server/parameter_fetcher_aws.cc | 2 +- .../server/parameter_fetcher_gcp.cc | 2 +- .../server/parameter_fetcher_gcp_test.cc | 1 - .../server/parameter_fetcher_local.cc | 2 +- .../server/parameter_fetcher_local_test.cc | 1 - components/data_server/server/server.cc | 171 +- components/data_server/server/server.h | 13 +- .../data_server/server/server_initializer.cc | 44 +- .../data_server/server/server_initializer.h | 5 +- .../data_server/server/server_local_test.cc | 57 +- components/errors/BUILD.bazel | 18 +- components/errors/error_tag.h | 45 + components/errors/retry.h | 14 +- components/errors/retry_test.cc | 1 - components/internal_server/BUILD.bazel | 29 +- components/internal_server/local_lookup.cc | 62 +- components/internal_server/local_lookup.h | 5 +- .../internal_server/local_lookup_test.cc | 70 +- components/internal_server/lookup.h | 5 +- components/internal_server/lookup.proto | 9 + .../internal_server/lookup_server_impl.cc | 72 +- .../internal_server/lookup_server_impl.h | 30 +- .../lookup_server_impl_test.cc | 17 +- components/internal_server/mocks.h | 11 +- .../internal_server/remote_lookup_client.h | 12 +- .../remote_lookup_client_impl.cc | 56 +- .../remote_lookup_client_impl_test.cc | 36 +- components/internal_server/sharded_lookup.cc | 163 +- components/internal_server/sharded_lookup.h | 11 +- .../internal_server/sharded_lookup_test.cc | 187 ++- components/internal_server/string_padder.cc | 2 +- components/sharding/BUILD.bazel | 8 +- .../sharding/cluster_mappings_manager.cc | 2 +- .../sharding/cluster_mappings_manager.h | 2 +- .../cluster_mappings_manager_aws_test.cc | 13 +- .../cluster_mappings_manager_gcp_test.cc | 11 +- components/sharding/shard_manager.cc | 8 +- components/sharding/shard_manager.h | 5 +- components/sharding/shard_manager_test.cc | 43 +- components/telemetry/BUILD.bazel | 20 +- components/telemetry/error_code.h | 99 +- components/telemetry/init_aws.cc | 4 +- components/telemetry/init_local_ostream.cc | 2 +- components/telemetry/init_local_otlp.cc | 4 +- .../telemetry/local_otlp_config/README.md | 4 +- components/telemetry/open_telemetry_sink.cc | 28 + components/telemetry/open_telemetry_sink.h | 38 + components/telemetry/server_definition.h | 560 +++++-- components/tools/BUILD.bazel | 27 +- components/tools/benchmarks/BUILD.bazel | 17 +- .../tools/benchmarks/cache_benchmark.cc | 59 +- .../benchmarks/data_loading_benchmark.cc | 19 +- .../tools/blob_storage_change_watcher_aws.cc | 2 +- components/tools/blob_storage_commands.cc | 24 +- components/tools/blob_storage_commands.h | 8 +- ...orage_util_aws.cc => blob_storage_util.cc} | 13 +- .../tools/concurrent_publishing_engine.cc | 36 +- .../tools/concurrent_publishing_engine.h | 9 +- components/tools/data_loading_analyzer.cc | 11 +- .../tools/delta_file_record_change_watcher.cc | 2 +- components/tools/delta_file_watcher_aws.cc | 5 +- components/tools/get_region_aws.cc | 2 +- components/tools/get_region_local.cc | 2 +- components/tools/publisher_service_local.cc | 48 + components/tools/realtime_notifier.cc | 6 +- .../tools/realtime_updates_publisher.cc | 6 +- .../BUILD.bazel | 4 +- .../validator.cc | 18 +- components/udf/BUILD.bazel | 17 +- components/udf/hooks/BUILD.bazel | 19 +- components/udf/hooks/get_values_hook.cc | 9 +- components/udf/hooks/get_values_hook.h | 8 +- components/udf/hooks/get_values_hook_test.cc | 51 +- components/udf/hooks/logging_hook.h | 15 +- components/udf/hooks/run_query_hook.cc | 6 +- components/udf/hooks/run_query_hook.h | 5 +- components/udf/hooks/run_query_hook_test.cc | 27 +- components/udf/mocks.h | 6 +- components/udf/noop_udf_client.cc | 7 +- components/udf/udf_client.cc | 75 +- components/udf/udf_client.h | 15 +- components/udf/udf_client_test.cc | 366 ++++- components/udf/udf_config_builder.cc | 35 +- components/udf/udf_config_builder.h | 8 +- components/util/BUILD.bazel | 29 +- components/util/build_info.cc | 2 +- components/util/platform_initializer_aws.cc | 4 +- components/util/platform_initializer_gcp.cc | 9 +- components/util/request_context.cc | 31 + components/util/request_context.h | 52 + components/util/sleepfor.cc | 2 +- docs/AWS_Terraform_vars.md | 42 +- docs/GCP_Terraform_vars.md | 61 +- docs/ad_retrieval_overview.md | 4 + docs/assets/ad_retrieval_filter_funnel.png | Bin 0 -> 40841 bytes docs/assets/ad_retrieval_udf.png | Bin 0 -> 272341 bytes .../assets/ad_retrieval_use_case_overview.png | Bin 0 -> 251156 bytes docs/assets/ad_retrieval_walkthrough.png | Bin 0 -> 262447 bytes .../data_loading/data_format_specification.md | 78 + .../data_loading_capabilities.md | 0 docs/data_loading/file_groups.md | 44 + docs/{ => data_loading}/loading_data.md | 165 +- .../realtime_updates_capabilities.md | 13 +- docs/deployment/deploying_locally.md | 9 +- docs/deployment/deploying_on_aws.md | 16 +- docs/deployment/deploying_on_gcp.md | 39 +- .../deployment/working_with_terraform.md | 8 +- docs/developing_the_server.md | 65 +- docs/generating_udf_files.md | 9 +- docs/inline_wasm_udfs.md | 2 +- docs/profiling_the_server.md | 12 +- .../ad_retrieval_overview.md | 407 +++++ .../examples/BUILD.bazel | 63 + .../examples/ad_retrieval.csv | 2 + .../examples/ad_retrieval_udf.js | 30 + .../onboarding_dev_guide.md | 210 +++ .../integrating_with_fledge.md | 4 +- docs/working_with_terraform.md | 1 - .../examples/sample_word2vec/README.md | 3 +- getting_started/quick_start.md | 18 +- .../quick_start_assets/docker-compose.yaml | 4 +- infrastructure/testing/BUILD.bazel | 2 +- .../testing/protocol_testing_helper_server.cc | 2 +- production/packaging/aws/build_and_test | 10 +- .../packaging/aws/data_server/BUILD.bazel | 88 +- .../packaging/aws/data_server/ami/BUILD.bazel | 2 +- .../packaging/aws/data_server/bin/BUILD.bazel | 1 + .../aws/data_server/nitro-pcr0/amd64.json | 2 +- .../otel_collector/otel_collector_config.yaml | 11 +- .../packaging/build_and_test_all_in_docker | 11 +- production/packaging/gcp/build_and_test | 10 +- .../packaging/gcp/data_server/BUILD.bazel | 13 +- .../packaging/gcp/data_server/bin/BUILD.bazel | 1 + .../gcp/data_server/bin/init_server_main.cc | 71 +- .../packaging/local/data_server/BUILD.bazel | 4 +- .../bin/start_request_simulation_system | 2 +- .../environments/demo/us-east-1.tfvars.json | 10 + .../environments/demo/us-west-1.tfvars.json | 7 + .../terraform/aws/environments/kv_server.tf | 26 +- .../aws/environments/kv_server_variables.tf | 53 + .../terraform/aws/modules/kv_server/main.tf | 79 +- .../aws/modules/kv_server/variables.tf | 54 +- .../terraform/aws/services/dashboard/main.tf | 492 ++++-- .../aws/services/dashboard/variables.tf | 5 + .../aws/services/iam_role_policies/main.tf | 2 +- .../terraform/aws/services/parameter/main.tf | 75 + .../aws/services/parameter/outputs.tf | 40 + .../aws/services/parameter/variables.tf | 50 + .../aws/services/security_group_rules/main.tf | 9 + .../terraform/gcp/environments/README.md | 23 + .../environments/demo/us-east1.tfvars.json | 12 + .../terraform/gcp/environments/kv_server.tf | 69 +- .../gcp/environments/kv_server_variables.tf | 90 +- .../terraform/gcp/environments/terraform.tf | 2 +- .../terraform/gcp/modules/kv_server/main.tf | 27 +- .../gcp/modules/kv_server/variables.tf | 22 + .../gcp/services/autoscaling/main.tf | 14 +- .../gcp/services/autoscaling/variables.tf | 6 + .../terraform/gcp/services/dashboards/main.tf | 1426 ++++++++++++++++- .../terraform/gcp/services/networking/main.tf | 8 +- .../gcp/services/networking/outputs.tf | 2 +- .../gcp/services/networking/variables.tf | 16 + .../gcp/services/service_mesh/variables.tf | 6 + public/applications/pa/BUILD.bazel | 2 +- public/applications/pa/response_utils.cc | 2 +- .../pas/retrieval_request_builder.cc | 27 +- .../pas/retrieval_request_builder.h | 3 + public/constants.cc | 7 + public/constants.h | 32 +- public/data_loading/BUILD.bazel | 7 +- public/data_loading/aggregation/BUILD.bazel | 3 +- .../aggregation/record_aggregator.cc | 2 +- public/data_loading/csv/BUILD.bazel | 5 +- .../csv/csv_delta_record_stream_reader.cc | 2 +- .../csv/csv_delta_record_stream_reader.h | 2 +- .../csv_delta_record_stream_reader_test.cc | 2 +- .../csv/csv_delta_record_stream_writer.cc | 2 +- public/data_loading/filename_utils.cc | 29 +- public/data_loading/filename_utils.h | 14 + public/data_loading/filename_utils_test.cc | 36 + public/data_loading/readers/BUILD.bazel | 16 +- public/data_loading/readers/avro_stream_io.cc | 46 +- public/data_loading/readers/avro_stream_io.h | 4 +- .../readers/avro_stream_io_test.cc | 44 +- .../data_loading/readers/riegeli_stream_io.h | 40 +- .../readers/riegeli_stream_io_test.cc | 3 +- .../readers/stream_record_reader_factory.h | 2 +- public/data_loading/record_utils.cc | 2 +- public/data_loading/records_utils.cc | 2 +- public/data_loading/writers/BUILD.bazel | 9 +- .../writers/avro_delta_record_stream_writer.h | 2 +- .../delta_record_limiting_file_writer.cc | 2 +- .../writers/delta_record_stream_writer.h | 2 +- .../delta_record_stream_writer_test.cc | 1 - public/query/cpp/BUILD.bazel | 2 +- public/query/cpp/client_utils.cc | 2 +- public/query/v2/BUILD.bazel | 1 + public/query/v2/get_values_v2.proto | 5 + public/test_util/BUILD.bazel | 1 - public/test_util/mocks.h | 1 - public/udf/BUILD.bazel | 16 +- public/udf/constants.h | 29 +- .../latency_benchmark_requirements.txt | 6 + third_party_deps/python_deps.bzl | 5 + .../rules_closure_repositories.bzl | 33 - .../BUILD.bazel | 7 +- .../bidding_auction_data_cli.cc | 2 +- .../delta_key_value_writer_test.cc | 1 - .../http_url_fetch_client.cc | 2 +- .../http_value_retriever.cc | 2 +- tools/data_cli/BUILD.bazel | 3 + tools/data_cli/commands/BUILD.bazel | 4 +- .../data_cli/commands/format_data_command.cc | 2 +- .../commands/generate_snapshot_command.cc | 2 +- tools/data_cli/data_cli.cc | 11 +- tools/latency_benchmarking/BUILD.bazel | 23 + tools/latency_benchmarking/README.md | 405 +++++ .../create_csv_summary.py | 78 + .../latency_benchmarking/deploy_and_benchmark | 393 +++++ .../example/aws_tf_overrides.txt | 6 + .../example/gcp_tf_overrides.txt | 2 + .../example/kv_data/BUILD.bazel | 52 + .../example/request_metadata.jsonl | 4 + .../example/request_metadata_run_query.jsonl | 2 + .../example/udf_code/BUILD.bazel | 44 + .../udf_code/benchmark_cpp_wasm_udf.cc | 290 ++++ .../udf_code/benchmark_cpp_wasm_udf.js | 27 + .../example/udf_code/benchmark_udf.js | 236 +++ .../example/udf_code/externs.js | 34 + .../latency_benchmarking/generate_requests.py | 160 ++ tools/latency_benchmarking/merge_csvs.py | 41 + tools/latency_benchmarking/run_benchmarks | 197 +++ tools/request_simulation/BUILD.bazel | 37 +- tools/request_simulation/client_worker.h | 6 +- .../request_simulation/client_worker_test.cc | 4 +- .../delta_based_request_generator.cc | 5 +- .../delta_based_request_generator.h | 6 +- .../delta_based_request_generator_test.cc | 13 +- .../detla_based_realtime_updates_publisher.cc | 39 +- .../detla_based_realtime_updates_publisher.h | 12 +- tools/request_simulation/grpc_client.h | 2 +- tools/request_simulation/main.cc | 10 +- tools/request_simulation/metrics_collector.cc | 4 +- tools/request_simulation/metrics_collector.h | 4 +- .../metrics_collector_test.cc | 2 +- tools/request_simulation/rate_limiter.h | 2 +- .../realtime_message_batcher.cc | 9 +- .../realtime_message_batcher_test.cc | 5 +- .../request_generation_util.cc | 3 +- .../request_simulation_parameter_fetcher.h | 1 + ...equest_simulation_parameter_fetcher_aws.cc | 13 +- ...equest_simulation_parameter_fetcher_gcp.cc | 11 +- ...uest_simulation_parameter_fetcher_local.cc | 11 +- .../request_simulation_system.cc | 67 +- .../request_simulation_system.h | 15 +- .../request_simulation_system_local_test.cc | 9 +- .../synthetic_request_generator.h | 4 +- .../helloworld_server/BUILD.bazel | 2 +- .../helloworld_server/helloworld.cc | 2 +- tools/serving_data_generator/BUILD.bazel | 8 +- .../test_serving_data_generator.cc | 95 +- tools/udf/closure_js/closure_to_delta.bzl | 11 +- .../examples/get_values_binary/udf.js | 35 +- .../get_values_binary_proto/my_udf.js | 4 +- .../examples/hello_world/BUILD.bazel | 2 +- .../examples/hello_world/my_udf.js | 4 +- .../inline_wasm/examples/js_call/my_udf.js | 6 +- .../inline_wasm/examples/protobuf/my_udf.js | 4 +- tools/udf/inline_wasm/wasm.bzl | 23 +- tools/udf/sample_udf/udf.js | 26 +- tools/udf/udf_generator/BUILD.bazel | 3 +- .../udf_generator/udf_delta_file_generator.cc | 6 +- tools/udf/udf_tester/BUILD.bazel | 4 +- tools/udf/udf_tester/README.md | 3 +- tools/udf/udf_tester/udf_delta_file_tester.cc | 26 +- version.txt | 2 +- 415 files changed, 12518 insertions(+), 3255 deletions(-) create mode 100644 .precommit_configs/markdown-link-check.json create mode 100644 builders/etc/.clang-tidy create mode 100644 builders/etc/.pylintrc create mode 100755 builders/images/test-tools/build_wrk create mode 120000 builders/tools/wrk2 create mode 100644 components/data/blob_storage/blob_prefix_allowlist.cc create mode 100644 components/data/blob_storage/blob_prefix_allowlist.h create mode 100644 components/data/blob_storage/blob_prefix_allowlist_test.cc create mode 100644 components/data/file_group/BUILD.bazel create mode 100644 components/data/file_group/file_group.cc create mode 100644 components/data/file_group/file_group.h create mode 100644 components/data/file_group/file_group_search_utils.cc create mode 100644 components/data/file_group/file_group_search_utils.h create mode 100644 components/data/file_group/file_group_search_utils_test.cc create mode 100644 components/data/file_group/file_group_test.cc create mode 100644 components/data_server/server/key_fetcher_utils_gcp.cc create mode 100644 components/data_server/server/key_fetcher_utils_gcp.h create mode 100644 components/data_server/server/nonprod_key_fetcher_factory_aws.cc create mode 100644 components/data_server/server/nonprod_key_fetcher_factory_cloud.cc create mode 100644 components/data_server/server/nonprod_key_fetcher_factory_cloud.h create mode 100644 components/data_server/server/nonprod_key_fetcher_factory_gcp.cc create mode 100644 components/errors/error_tag.h create mode 100644 components/telemetry/open_telemetry_sink.cc create mode 100644 components/telemetry/open_telemetry_sink.h rename components/tools/{blob_storage_util_aws.cc => blob_storage_util.cc} (91%) create mode 100644 components/tools/publisher_service_local.cc create mode 100644 components/util/request_context.cc create mode 100644 components/util/request_context.h create mode 100644 docs/ad_retrieval_overview.md create mode 100644 docs/assets/ad_retrieval_filter_funnel.png create mode 100644 docs/assets/ad_retrieval_udf.png create mode 100644 docs/assets/ad_retrieval_use_case_overview.png create mode 100644 docs/assets/ad_retrieval_walkthrough.png create mode 100644 docs/data_loading/data_format_specification.md rename docs/{ => data_loading}/data_loading_capabilities.md (100%) create mode 100644 docs/data_loading/file_groups.md rename docs/{ => data_loading}/loading_data.md (67%) rename docs/{ => data_loading}/realtime_updates_capabilities.md (95%) rename production/terraform/README.md => docs/deployment/working_with_terraform.md (71%) create mode 100644 docs/protected_app_signals/ad_retrieval_overview.md create mode 100644 docs/protected_app_signals/examples/BUILD.bazel create mode 100644 docs/protected_app_signals/examples/ad_retrieval.csv create mode 100644 docs/protected_app_signals/examples/ad_retrieval_udf.js create mode 100644 docs/protected_app_signals/onboarding_dev_guide.md rename docs/{ => protected_audience}/integrating_with_fledge.md (97%) delete mode 120000 docs/working_with_terraform.md create mode 100644 production/terraform/gcp/environments/README.md create mode 100644 third_party_deps/latency_benchmark_requirements.txt delete mode 100644 third_party_deps/rules_closure_repositories.bzl create mode 100644 tools/latency_benchmarking/BUILD.bazel create mode 100644 tools/latency_benchmarking/README.md create mode 100644 tools/latency_benchmarking/create_csv_summary.py create mode 100755 tools/latency_benchmarking/deploy_and_benchmark create mode 100644 tools/latency_benchmarking/example/aws_tf_overrides.txt create mode 100644 tools/latency_benchmarking/example/gcp_tf_overrides.txt create mode 100644 tools/latency_benchmarking/example/kv_data/BUILD.bazel create mode 100644 tools/latency_benchmarking/example/request_metadata.jsonl create mode 100644 tools/latency_benchmarking/example/request_metadata_run_query.jsonl create mode 100644 tools/latency_benchmarking/example/udf_code/BUILD.bazel create mode 100644 tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.cc create mode 100644 tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.js create mode 100644 tools/latency_benchmarking/example/udf_code/benchmark_udf.js create mode 100644 tools/latency_benchmarking/example/udf_code/externs.js create mode 100644 tools/latency_benchmarking/generate_requests.py create mode 100644 tools/latency_benchmarking/merge_csvs.py create mode 100755 tools/latency_benchmarking/run_benchmarks diff --git a/.bazelrc b/.bazelrc index 0ea65008..fdae62c2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -19,12 +19,12 @@ test:run_all_tests --test_verbose_timeout_warnings build:noexcept --copt=-fno-exceptions # Grant exceptions to some dependencies so they can use exceptions build:noexcept --per_file_copt=.*boost.*@-fexceptions -build:noexcept --per_file_copt=.*cc/aws/proxy.*@-fexceptions -build:noexcept --per_file_copt=.*cc/roma.*@-fexceptions +build:noexcept --per_file_copt=.*src/aws/proxy.*@-fexceptions +build:noexcept --per_file_copt=.*src/roma.*@-fexceptions build:noexcept --per_file_copt=.*oneTBB.*@-fexceptions build:noexcept --per_file_copt=.*com_github_nghttp2_nghttp2.*@-fexceptions -build:noexcept --per_file_copt=.*cc/core.*@-fexceptions -build:noexcept --per_file_copt=.*cc/cpio.*@-fexceptions +build:noexcept --per_file_copt=.*src/core.*@-fexceptions +build:noexcept --per_file_copt=.*src/cpio.*@-fexceptions test --test_output=errors # Disable ICU linking for googleurl. @@ -40,9 +40,17 @@ build:clang --host_cxxopt=-std=c++17 build:clang --client_env=BAZEL_CXXOPTS=-std=c++17 build:clang --per_file_copt=external/nitrokmscli_.*\.c@-Wno-int-conversion +build:cpp_nowarn --copt=-Werror +build:cpp_nowarn --per_file_copt=external/.*@-Wno-error + +build:clang-tidy --aspects @bazel_clang_tidy//clang_tidy:clang_tidy.bzl%clang_tidy_aspect +build:clang-tidy --output_groups=report +build:clang-tidy --@bazel_clang_tidy//:clang_tidy_config=//:clang_tidy_config + # Required to use protos in wasm_cc_binary/inline_wasm_cc_binary build:emscripten --per_file_copt=.*zlib.*@-Wno-deprecated-non-prototype build:emscripten --per_file_copt=.*utf8_range.*@-Wno-unused-function +build:emscripten --per_file_copt=.*protobuf.*@-Wno-deprecated-declarations # Address sanitizer, set action_env to segregate cache entries build:asan --action_env=PRIVACY_SANDBOX_SERVERS_ASAN=1 @@ -106,9 +114,14 @@ build:aws_platform --@google_privacysandbox_servers_common//:platform=aws build:gcp_platform --//:platform=gcp build:gcp_platform --@google_privacysandbox_servers_common//:platform=gcp +# --config prod_mode: builds the service in prod mode +build:prod_mode --//:mode=prod +build:prod_mode --@google_privacysandbox_servers_common//:build_flavor=prod + +# --config prod_mode: builds the service in prod mode +build:nonprod_mode --//:mode=nonprod +build:nonprod_mode --@google_privacysandbox_servers_common//:build_flavor=non_prod + try-import %workspace%/builders/.coverage.bazelrc coverage --test_tag_filters=-nocoverage coverage --test_size_filters=-enormous - -build:non_prod --@google_privacysandbox_servers_common//:build_flavor=non_prod -build:prod --@google_privacysandbox_servers_common//:build_flavor=prod diff --git a/.bazelversion b/.bazelversion index 91e4a9f2..f22d756d 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -6.3.2 +6.5.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2e6ace2..fe88324b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ exclude: (?x)^( fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: end-of-file-fixer - id: fix-byte-order-marker @@ -43,6 +43,12 @@ repos: - id: check-executables-have-shebangs - id: detect-private-key +- repo: https://github.com/tcort/markdown-link-check + rev: v3.11.2 + hooks: + - id: markdown-link-check + args: [-c .precommit_configs/markdown-link-check.json] + - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 3.0.0 hooks: @@ -54,12 +60,12 @@ repos: exclude: ^(google_internal|builders/images)/.*$ - repo: https://github.com/bufbuild/buf - rev: v1.26.1 + rev: v1.29.0 hooks: - id: buf-format - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v16.0.6 + rev: v17.0.6 hooks: - id: clang-format types_or: @@ -100,7 +106,7 @@ repos: - terraform - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v3.1.0 hooks: - id: prettier types_or: @@ -113,7 +119,7 @@ repos: )$ - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.9.2 + rev: v0.12.1 hooks: - id: markdownlint-cli2 name: lint markdown @@ -148,7 +154,13 @@ repos: - --quiet - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 24.2.0 hooks: - id: black name: black python formatter + +- repo: https://github.com/tekwizely/pre-commit-golang + rev: v1.0.0-rc.1 + hooks: + - id: go-fmt + name: go format diff --git a/.precommit_configs/markdown-link-check.json b/.precommit_configs/markdown-link-check.json new file mode 100644 index 00000000..98b5f35e --- /dev/null +++ b/.precommit_configs/markdown-link-check.json @@ -0,0 +1,27 @@ +{ + "ignorePatterns": [ + { + "pattern": "^tg/" + }, + { + "pattern": "^http://localhost" + }, + { + "pattern": "^https://demo.kv-server.your-domain.example/" + }, + { + "pattern": "^demo.kv-server.your-domain.example:8443" + } + ], + "replacementPatterns": [ + { + "pattern": "^/", + "replacement": "{{BASEURL}}/" + } + ], + "timeout": "20s", + "retryOn429": true, + "retryCount": 5, + "fallbackRetryDelay": "30s", + "aliveStatusCodes": [200, 206] +} diff --git a/BUILD.bazel b/BUILD.bazel index 51c4a0a9..6c8c9cb4 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@io_bazel_rules_go//go:def.bzl", "nogo") @@ -107,6 +108,69 @@ config_setting( ], ) +string_flag( + name = "mode", + build_setting_default = "prod", + values = [ + "prod", + "nonprod", + ], +) + +config_setting( + name = "prod_mode", + flag_values = { + ":mode": "prod", + }, + visibility = [ + "//components:__subpackages__", + "//tools:__subpackages__", + ], +) + +config_setting( + name = "nonprod_mode", + flag_values = { + ":mode": "nonprod", + }, + visibility = [ + "//components:__subpackages__", + "//tools:__subpackages__", + ], +) + +selects.config_setting_group( + name = "aws_prod", + match_all = [ + "//:aws_platform", + "//:prod_mode", + ], +) + +selects.config_setting_group( + name = "aws_nonprod", + match_all = [ + "//:aws_platform", + "//:nonprod_mode", + ], +) + +selects.config_setting_group( + name = "gcp_prod", + match_all = [ + "//:gcp_platform", + "//:prod_mode", + ], +) + +selects.config_setting_group( + name = "gcp_nonprod", + match_all = [ + "//:gcp_platform", + "//:nonprod_mode", + ], +) + exports_files( [".bazelversion"], ) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e93e13d..6699b468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,116 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## 0.16.0 (2024-04-05) + + +### Features + +* Add cache hit or miss metrics +* Add coorindator specific terraform parameters +* Add data loading prefix allowlist parameter +* Add default PAS UDF +* Add E2E latency for GetKeyValues and GetKeyValueSet in sharded lookup +* Add file groups and file group reader logic +* Add go fmt to pre-commit +* Add key prefix support to blob storage client +* Add LogContext and ConsentedDebugConfiguration proto to v2 API and internal lookup API +* Add prod and nonprod build flag +* Add request context to wrap metrics context +* Add support for configuring directory allowlist +* Add wiring for prefix allowlist (actual impl in follow up cl) +* Allow overrides for coordinators endpoints in nonprod mode +* Allow to disable v1 key not found entry in response +* Create separate metrics context map for internal lookup server +* Deprecate metrics recorder for internal lookup +* Deprecate metrics recorder for internal server +* Deprecate metrics recorder for sharded lookup +* deprecate metrics recorder for V1 server and handler +* Deprecate metrics recorder from cache +* Enable simulation system send realtime udpates +* Enable TCMalloc for KV Server and benchmarks +* Explicitly enable core dumps +* Implement deletion cutoff max timestamp per directory +* Load data files and allow notifications from configured prefix +* Load prefix files on startup and handle prefix blob notifications +* Log common request metrics +* Migrate from glog to absl log +* Partition data loading metrics by delta file name +* Pass request context from hooks to downstream components +* Pass request context to udf hooks +* Read telemetry config from cloud parameter +* Revamp AWS metrics dashboard +* Revamp GCP metrics dashboard +* Set udf_min_log_level from parameter store. +* Support content type proto for v2 api +* Support content type proto for v2 api response +* Update cache interface and blob data location to pass prefix +* Update start_after to use a map from prefix to start_after +* Use file groups for loading snapshots +* Write logs to an Otel endpoint + + +### Bug Fixes + +* Actually load all files in a snapshot file group +* **AWS:** Filter out unavailable zones. +* Correct an error in kokoro_release. +* Correct format for image tag. +* Correct typo for internal dev's service_mesh_address. +* Correct typos in GCP deployment guide. +* Crash server if default UDF fails to load. +* Delete non-active certificate before creating a new one. +* Fix filtering logic for prefixed blobs +* Fix permissions for data-loading-blob-prefix-allowlist +* Make GCP nat optional. +* Parse delta filename from notification before validating it +* Remove glog dependency for record_utils +* Remove temp dir only if it's successfully created. +* Rename class to ThreadManager +* Set retain_initial_value_of_delta_metric flag for aws metrics exporter +* Update a outdated hyperlink. +* Update common repo to pick up the AWS metrics dimension fix +* Update GCP Terraform with ability to delete unhealthy instance. +* Update tf variables to use gorekore instead of kelvingorekore +* Use blob key instead of prefixed basename + + +### GCP: Fixes + +* **GCP:** Make sure server is connected to otel collector before reaching to ready state + + +### GCP: Features + +* **GCP:** Applying Terraform pulls docker image with new tag. +* **GCP:** Make service mesh address configurable. +* **GCP:** Make subnet ip cidr configurable. +* **GCP:** Make xlb/envoy optional. + + +### Documentation + +* Add ad retrieval explainer. +* Add docs for directory support +* Add PA and PAS folders +* Add PAS developer guide +* Add public docs for file groups +* Ads retreival explainer update. + + +### Dependencies + +* **deps:** Add clang-tidy bazel config +* **deps:** Add cpp_nowarn bazel config +* **deps:** Upgrade bazel to 6.5.0 +* **deps:** Upgrade build-system to 0.55.1 +* **deps:** Upgrade build-system to 0.55.2 +* **deps:** Upgrade build-system to 0.57.0 +* **deps:** Upgrade data-plane-shared repo +* **deps:** Upgrade data-plane-shared repo to 1684674 2024-02-09 +* **deps:** Upgrade data-plane-shared-libraries to 1fbac46 +* **deps:** Upgrade pre-commit hooks + ## 0.15.0 (2024-01-23) diff --git a/README.md b/README.md index 80d8e09f..d1632784 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ versions are released. The query API conforms to the [API explainer](https://github.com/WICG/turtledove/blob/main/FLEDGE_Key_Value_Server_API.md). At the moment, to load data, instead of calling the mutation API, you would place the data as files into a location that can be directly read by the server. See more details in the -[data loading guide](/docs/loading_data.md). +[data loading guide](/docs/data_loading/loading_data.md). Currently, this service can be deployed to 1 region of your choice with more regions to be added soon. Monitoring and alerts are currently unavailable. @@ -72,18 +72,18 @@ changes. - [FLEDGE K/V server API explainer](https://github.com/WICG/turtledove/blob/main/FLEDGE_Key_Value_Server_API.md) - [FLEDGE K/V server trust model](https://github.com/privacysandbox/fledge-docs/blob/main/key_value_service_trust_model.md) - [Local server quickstart guide](/docs/developing_the_server.md) -- [AWS server user deployment documentation](/docs/deploying_on_aws.md) -- [GCP server user deployment documentation](/docs/deploying_on_gcp.md) -- [Integrating the K/V server with FLEDGE](/docs/integrating_with_fledge.md) -- [FLEDGE K/V server sharding explainer](https://github.com/privacysandbox/fledge-docs/blob/main/key_value_sharding.md) +- [AWS server user deployment documentation](/docs/deployment/deploying_on_aws.md) +- [GCP server user deployment documentation](/docs/deployment/deploying_on_gcp.md) +- [Integrating the K/V server with FLEDGE](/docs/protected_audience/integrating_with_fledge.md) +- [FLEDGE K/V server sharding explainer](https://github.com/privacysandbox/protected-auction-services-docs/blob/main/key_value_service_sharding.md) - Operating documentation - - [Data loading API and operations](/docs/loading_data.md) + - [Data loading API and operations](/docs/data_loading/loading_data.md) - [Generating and loading UDF files](/docs/generating_udf_files.md) - Error handling explainer (_to be published_) - Developer guide - [Codebase structure](/docs/repo_layout.md) - - [Working with Terraform](/production/terraform/README.md) - - [Contributing to the codebase](/docs/CONTRIBUTING.md) + - [Working with Terraform](/docs/deployment/working_with_terraform.md) + - [Contributing to the codebase](/docs/contributing.md) - [Code of conduct](/docs/CODE_OF_CONDUCT.md) - [Change log](/CHANGELOG.md) diff --git a/WORKSPACE b/WORKSPACE index 4deae703..da073316 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -13,11 +13,11 @@ python_deps("//builders/bazel") http_archive( name = "google_privacysandbox_servers_common", - # commit f198e86 2024-01-16 - sha256 = "979d467165bef950ed69bc913e9ea743bfc068714fb43cf61e02b52b910a5561", - strip_prefix = "data-plane-shared-libraries-f198e86307028ad98c38a8ed1c72189eefa97334", + # commit b34fe82 2024-04-03 + sha256 = "2afc7017723efb9d34b6ed713be03dbdf9b45de8ba585d2ea314eb3a52903d0a", + strip_prefix = "data-plane-shared-libraries-b34fe821b982e06446df617edb7a6e3041c8b0db", urls = [ - "https://github.com/privacysandbox/data-plane-shared-libraries/archive/f198e86307028ad98c38a8ed1c72189eefa97334.zip", + "https://github.com/privacysandbox/data-plane-shared-libraries/archive/b34fe821b982e06446df617edb7a6e3041c8b0db.zip", ], ) @@ -51,6 +51,20 @@ load( cpp_repositories() +http_archive( + name = "io_bazel_rules_docker", + sha256 = "b1e80761a8a8243d03ebca8845e9cc1ba6c82ce7c5179ce2b295cd36f7e394bf", + urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.25.0/rules_docker-v0.25.0.tar.gz"], +) + +load("@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories") + +container_repositories() + +load("@io_bazel_rules_docker//repositories:deps.bzl", io_bazel_rules_docker_deps = "deps") + +io_bazel_rules_docker_deps() + load("//third_party_deps:container_deps.bzl", "container_deps") container_deps() @@ -110,14 +124,13 @@ load("//third_party_deps:python_deps.bzl", "python_repositories") python_repositories() # Load the starlark macro, which will define your dependencies. -load("@word2vec//:requirements.bzl", "install_deps") +load("@latency_benchmark//:requirements.bzl", latency_benchmark_install_deps = "install_deps") +load("@word2vec//:requirements.bzl", word2vec_install_deps = "install_deps") # Call it to define repos for your requirements. -install_deps() +latency_benchmark_install_deps() -load("//third_party_deps:rules_closure_repositories.bzl", "rules_closure_repositories") - -rules_closure_repositories() +word2vec_install_deps() # Use nogo to run `go vet` with bazel load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") @@ -125,3 +138,15 @@ load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_depe go_rules_dependencies() go_register_toolchains(nogo = "@//:kv_nogo") + +# setup container_structure_test +http_archive( + name = "container_structure_test", + sha256 = "2da13da4c4fec9d4627d4084b122be0f4d118bd02dfa52857ff118fde88e4faa", + strip_prefix = "container-structure-test-1.16.0", + urls = ["https://github.com/GoogleContainerTools/container-structure-test/archive/v1.16.0.zip"], +) + +load("@container_structure_test//:repositories.bzl", "container_structure_test_register_toolchain") + +container_structure_test_register_toolchain(name = "cst") diff --git a/builders/CHANGELOG.md b/builders/CHANGELOG.md index ecf0d25b..e323d8a3 100644 --- a/builders/CHANGELOG.md +++ b/builders/CHANGELOG.md @@ -2,6 +2,114 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## 0.57.0 (2024-03-10) + + +### Features + +* Add a generic pylintrc +* Add clang-tidy to build-debian + +## 0.56.0 (2024-02-29) + + +### Features + +* Add pylint to presubmit + + +### Bug Fixes + +* Clean bazel build and mod caches +* Pin okigan/awscurl to v0.29 + +## 0.55.2 (2024-02-23) + + +### Bug Fixes + +* Add gmock to .clang-format + +## 0.55.1 (2024-02-22) + + +### Bug Fixes + +* Do not invoke normalize-bazel-symlinks for cbuild --cmd + +## 0.55.0 (2024-02-22) + + +### Bug Fixes + +* Normalize bazel symlinks to a resolved path +* Pass the correct path for normalize-bazel-symlinks + +## 0.54.0 (2024-02-09) + + +### Features + +* Set cbuild workdir to pwd relative to root workspace + +## 0.53.0 (2024-01-25) + + +### Features + +* Add support to collect-coverage tool for custom lcov report + + +### Bug Fixes + +* Improve --cmd-profiler support + +## 0.52.0 (2023-12-02) + + +### Features + +* add python3.9 dev to bazel-debian + +## 0.51.0 (2023-11-30) + + +### Bug Fixes + +* Clean go build cache at the end of image build script + + +### Dependencies + +* **deps:** Upgrade bazelisk to 1.19.0 + +## 0.50.0 (2023-11-06) + + +### Features + +* Add openssh-client to build-debian image + +## 0.49.1 (2023-10-30) + + +### Bug Fixes + +* Add tools/wrk2 wrapper script + +## 0.49.0 (2023-10-27) + + +### Features + +* Add wrk2 to test-tools image +* Extend normalize-bazel-symlink to normalize within containers + + +### Dependencies + +* **deps:** Update versions in test-tools image + ## 0.48.0 (2023-10-11) diff --git a/builders/etc/.clang-format b/builders/etc/.clang-format index 8c45f0ce..3232a3be 100644 --- a/builders/etc/.clang-format +++ b/builders/etc/.clang-format @@ -4,8 +4,8 @@ DerivePointerAlignment: false SortIncludes: true IncludeBlocks: Regroup IncludeCategories: - # gtest, this should be put first in tests. - - Regex: '^ from OS. - Regex: '^<[_A-Za-z0-9-]+\.h>' diff --git a/builders/etc/.clang-tidy b/builders/etc/.clang-tidy new file mode 100644 index 00000000..2e9829d0 --- /dev/null +++ b/builders/etc/.clang-tidy @@ -0,0 +1,8 @@ +Checks: > + -*, + bugprone-*, + -bugprone-narrowing-conversions, + performance-*, + +WarningsAsErrors: '*' +... diff --git a/builders/etc/.pylintrc b/builders/etc/.pylintrc new file mode 100644 index 00000000..967bd3ee --- /dev/null +++ b/builders/etc/.pylintrc @@ -0,0 +1,118 @@ +[MESSAGES CONTROL] + +# List of checkers and warnings to enable. +enable= + indexing-exception, + old-raise-syntax, + +# List of checkers and warnings to disable. +# TODO: Shrink this list to as small as possible. +disable= + attribute-defined-outside-init, + bad-option-value, + bare-except, + broad-except, + c-extension-no-member, + design, + file-ignored, + fixme, + global-statement, + import-error, + import-outside-toplevel, + locally-disabled, + misplaced-comparison-constant, + multiple-imports, + no-self-use, + relative-import, + similarities, + suppressed-message, + ungrouped-imports, + unsubscriptable-object, + useless-object-inheritance, # Remove once all bots are on Python 3. + useless-suppression, + wrong-import-order, + wrong-import-position, + # FIXME: To be removed. Leftovers from Python 3 migration. + consider-using-with, + raise-missing-from, + super-with-arguments, + use-a-generator, + consider-using-generator, + consider-using-f-string, # Too much legacy usages. + unspecified-encoding, # Too much legacy usage. + broad-exception-raised, + +[BASIC] + +# Regular expression which should only match the name +# of functions or classes which do not require a docstring. +no-docstring-rgx=(__.*__|main) + +# Min length in lines of a function that requires a docstring. +docstring-min-length=10 + +# Regular expression which should only match correct module names. The +# leading underscore is sanctioned for private modules by Google's style +# guide. +# +# There are exceptions to the basic rule (_?[a-z][a-z0-9_]*) to cover +# requirements of Python's module system and of the presubmit framework. +module-rgx=^(_?[a-z][a-z0-9_]*)|__init__|__main__|PRESUBMIT|PRESUBMIT_unittest$ + +# Regular expression which should only match correct module level names. +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression which should only match correct class attribute. +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression which should only match correct class names. +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression which should only match correct function names. +# 'camel_case' and 'snake_case' group names are used for consistency of naming +# styles across functions and methods. +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression which should only match correct method names. +# 'camel_case' and 'snake_case' group names are used for consistency of naming +# styles across functions and methods. 'exempt' indicates a name which is +# consistent with all naming styles. +method-rgx=(?x) + ^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase + |tearDownTestCase|setupSelf|tearDownClass|setUpClass + |(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next) + |(?P_{0,2}[A-Z][a-zA-Z0-9_]*) + |(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match correct instance attribute names. +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression which should only match correct argument names. +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression which should only match correct variable names. +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names. +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma. +good-names=main,_,maxDiff + +# Bad variable names which should always be refused, separated by a comma. +bad-names= + +# FIXME: Renable this. +# List of builtins function names that should not be used, separated by a comma. +#bad-builtin=input + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=90 + +# Maximum number of lines in a module +max-module-lines=99999 + +[TYPECHECK] diff --git a/builders/images/build-amazonlinux2/install_apps b/builders/images/build-amazonlinux2/install_apps index e0f46ccc..a5e6ff35 100755 --- a/builders/images/build-amazonlinux2/install_apps +++ b/builders/images/build-amazonlinux2/install_apps @@ -86,6 +86,11 @@ function install_clang() { clang --version } +function cleanup() { + cd / + go clean -cache -modcache +} + if [[ ${VERBOSE} -eq 1 ]]; then printf "=== SHELL ENVIRONMENT ===\n" env @@ -99,3 +104,4 @@ install_golang "${BUILD_ARCH}" install_gcc install_packer install_python +cleanup diff --git a/builders/images/build-amazonlinux2023/install_apps b/builders/images/build-amazonlinux2023/install_apps index 722e89f6..51c64377 100755 --- a/builders/images/build-amazonlinux2023/install_apps +++ b/builders/images/build-amazonlinux2023/install_apps @@ -85,6 +85,11 @@ function install_clang() { clang --version } +function cleanup() { + cd / + go clean -cache -modcache +} + if [[ ${VERBOSE} -eq 1 ]]; then printf "=== SHELL ENVIRONMENT ===\n" env @@ -98,3 +103,4 @@ install_clang install_golang "${BUILD_ARCH}" install_gcc install_packer +cleanup diff --git a/builders/images/build-debian/install_apps b/builders/images/build-debian/install_apps index c7d8e4a6..fba47ca1 100755 --- a/builders/images/build-debian/install_apps +++ b/builders/images/build-debian/install_apps @@ -44,8 +44,8 @@ function apt_update() { } function install_python() { - DEBIAN_FRONTEND=noninteractive apt-get --quiet install -y --no-install-recommends \ - python3.9-venv="3.9.*" + apt-get --quiet install -y --no-install-recommends \ + python3.9-venv="3.9.*" python3.9-dev mkdir -p /opt/bin update-alternatives \ --force \ @@ -63,19 +63,21 @@ function install_python() { } function install_misc() { - DEBIAN_FRONTEND=noninteractive apt-get --quiet install -y --no-install-recommends \ + apt-get --quiet install -y --no-install-recommends \ apt-transport-https="2.0.*" \ bsdmainutils \ ca-certificates \ chrpath="0.16-*" \ libcurl4="7.68.*" \ curl="7.68.*" \ - file="1:5*" \ + file="1:5.*" \ gettext="0.19.*" \ git="1:2.25.*" \ gnupg="2.2.*" \ + google-perftools="2.*" \ locales="2.31-*" \ lsb-release="11.1.*" \ + openssh-client="1:8.2*" \ patch="2.7.*" \ rename="1.10-*" \ software-properties-common="0.99.*" \ @@ -121,11 +123,19 @@ function install_docker() { apt-get --quiet install -y --no-install-recommends docker-ce docker-ce-cli containerd.io } -function clean_debian() { +function install_clang_tidy() { + apt-get --quiet install -y clang-tidy + printf "clang-tidy version: %s\n" "$(clang-tidy --version)" + printf "clang-tidy config: %s\n" "$(clang-tidy -dump-config)" +} + +function cleanup() { apt-get --quiet autoremove -y apt-get autoclean apt-get clean rm -rf /var/lib/apt/lists + cd / + go clean -cache -modcache } if [[ ${VERBOSE} -eq 1 ]]; then @@ -133,10 +143,13 @@ if [[ ${VERBOSE} -eq 1 ]]; then env fi +declare -x -r DEBIAN_FRONTEND=noninteractive + apt_update install_misc install_clang +install_clang_tidy install_golang "${BUILD_ARCH}" install_docker "${BUILD_ARCH}" install_python # should run after other install_* -clean_debian +cleanup diff --git a/builders/images/install_golang_apps b/builders/images/install_golang_apps index a10d33e5..9d75eb94 100755 --- a/builders/images/install_golang_apps +++ b/builders/images/install_golang_apps @@ -31,7 +31,7 @@ while [[ $# -gt 0 ]]; do done function install_bazelisk() { - go install github.com/bazelbuild/bazelisk@v1.13.2 + go install github.com/bazelbuild/bazelisk@v1.19.0 BAZELISK="$(go env GOPATH)"/bin/bazelisk if [[ -n ${BAZEL_PATH} ]] && [[ -d ${BAZEL_PATH} ]]; then ln -s "${BAZELISK}" "${BAZEL_PATH}"/bazel @@ -43,4 +43,10 @@ function install_bazelisk() { rm -rf /bazel_root/* } +function cleanup() { + cd / + go clean -cache -modcache +} + install_bazelisk +cleanup diff --git a/builders/images/presubmit/install_apps b/builders/images/presubmit/install_apps index 64808cc1..c0d8d0d8 100755 --- a/builders/images/presubmit/install_apps +++ b/builders/images/presubmit/install_apps @@ -37,10 +37,7 @@ while [[ $# -gt 0 ]]; do set -o xtrace shift ;; - -h | --help) - usage 0 - break - ;; + -h | --help) usage 0 ;; *) usage 0 ;; esac done @@ -85,7 +82,9 @@ function install_docker() { function install_precommit() { /usr/bin/python3.9 -m venv "${PRE_COMMIT_VENV_DIR}" - "${PRE_COMMIT_VENV_DIR}"/bin/pip install pre-commit~=3.1 + "${PRE_COMMIT_VENV_DIR}"/bin/pip install \ + pre-commit~=3.1 \ + pylint~=3.1.0 "${PRE_COMMIT_TOOL}" --version # initialize pre-commit cache, which needs a git repo (a temporary will suffice) @@ -102,11 +101,13 @@ function install_precommit() { rm -rf "${GIT_REPO}" } -function clean_debian() { +function cleanup() { apt-get --quiet autoremove -y apt-get autoclean apt-get clean rm -rf /var/lib/apt/lists + cd / + go clean -cache -modcache } if [[ ${VERBOSE} -eq 1 ]]; then @@ -126,4 +127,4 @@ install_packages install_golang "${ARCH}" install_docker install_precommit -clean_debian +cleanup diff --git a/builders/images/test-tools/Dockerfile b/builders/images/test-tools/Dockerfile index 21bb383a..45fab3cf 100644 --- a/builders/images/test-tools/Dockerfile +++ b/builders/images/test-tools/Dockerfile @@ -12,31 +12,36 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM alpine:3.16 as builder +FROM alpine:3.18 as slowhttptest_builder # hadolint ignore=DL3018 -RUN apk add --no-cache build-base git openssl-dev autoconf automake +RUN apk add --no-cache autoconf automake build-base git openssl-dev WORKDIR /build -ADD https://github.com/shekyan/slowhttptest/archive/refs/tags/v1.9.0.tar.gz /build -RUN tar xz --strip-components 1 -f v1.9.0.tar.gz && ls -l && ./configure && make +ADD https://github.com/shekyan/slowhttptest/archive/refs/tags/v1.9.0.tar.gz /build/src.tar.gz +RUN tar xz --strip-components 1 -f src.tar.gz && ./configure && make -FROM golang:1.19.4-alpine3.17 AS golang -ENV BUILD_ARCH="${TARGETARCH}" \ - GOBIN=/usr/local/go/bin +FROM alpine:3.18 as wrk_builder +ARG TARGETARCH +ENV BUILD_ARCH="${TARGETARCH}" +COPY build_wrk /build/ +WORKDIR /build +ADD https://github.com/giltene/wrk2/archive/44a94c17d8e6a0bac8559b53da76848e430cb7a7.tar.gz /build/src.tar.gz +RUN /build/build_wrk + +FROM golang:1.21-alpine3.18 AS golang +ENV GOBIN=/usr/local/go/bin COPY build_golang_apps /scripts/ RUN /scripts/build_golang_apps -FROM fullstorydev/grpcurl:v1.8.7 AS grpcurl -FROM alpine:3.17.2 +FROM fullstorydev/grpcurl:v1.8.9-alpine AS grpcurl +FROM alpine:3.18 COPY --from=golang /usr/local/go/bin/* /usr/local/bin/ COPY --from=grpcurl /bin/grpcurl /usr/local/bin/ - ARG TARGETARCH ENV BUILD_ARCH="${TARGETARCH}" \ PATH="${PATH}:/usr/local/go/bin" \ GOBIN=/usr/local/go/bin - COPY install_apps /scripts/ - RUN /scripts/install_apps -COPY --from=builder /build/src/slowhttptest /usr/bin/ +COPY --from=slowhttptest_builder /build/src/slowhttptest /usr/bin/ +COPY --from=wrk_builder /build/wrk /usr/bin/wrk2 diff --git a/builders/images/test-tools/build_wrk b/builders/images/test-tools/build_wrk new file mode 100755 index 00000000..d843dd06 --- /dev/null +++ b/builders/images/test-tools/build_wrk @@ -0,0 +1,27 @@ +#!/bin/busybox sh + +set -o errexit +set -o xtrace + +install_no_wrk() { + cat </build/wrk +#!/bin/busybox sh +PROGRAM_NAME="\$(basename \$0)" +printf "Error: %s not supported on Aarch64\n" "\${PROGRAM_NAME}" +printf "build_arch: %s\n" "${BUILD_ARCH}" >/dev/stderr +exit 1 +EOF + chmod 755 /build/wrk +} + +install_wrk() { + apk add --no-cache build-base git openssl-dev zlib-dev + tar xz --strip-components 1 -f src.tar.gz + make -j6 +} + +if [[ ${BUILD_ARCH} == arm64 ]]; then + install_no_wrk +else + install_wrk +fi diff --git a/builders/tests/data/hashes/build-amazonlinux2 b/builders/tests/data/hashes/build-amazonlinux2 index 770d4751..7aed7b0f 100644 --- a/builders/tests/data/hashes/build-amazonlinux2 +++ b/builders/tests/data/hashes/build-amazonlinux2 @@ -1 +1 @@ -1feb61b0cf40e2797fc6a3425c3e50ce928ca194f3338d9a7155e51a4917312a +3efa00f3a5dbe0a4708be523aa32aca91dcd56d403d3ff32e0202756b8321b3b diff --git a/builders/tests/data/hashes/build-amazonlinux2023 b/builders/tests/data/hashes/build-amazonlinux2023 index 036c5cd3..1bcc412e 100644 --- a/builders/tests/data/hashes/build-amazonlinux2023 +++ b/builders/tests/data/hashes/build-amazonlinux2023 @@ -1 +1 @@ -a1c165d019f546e1fcad06506da3b274094b61a9add5576bf3fed6e5a45fb8d4 +57396ff1c765f7b63905963cfe4498912f7f75b5cb9f7bc36bd6879af69872e7 diff --git a/builders/tests/data/hashes/build-debian b/builders/tests/data/hashes/build-debian index 1e7e375d..c89114f2 100644 --- a/builders/tests/data/hashes/build-debian +++ b/builders/tests/data/hashes/build-debian @@ -1 +1 @@ -a10495fe1e23472aea6770b0c9760e1cdfc011a4883ff5fb3d488774e6995ae6 +38cc8a23a6a56eb6567bef3685100cd3be1c0491dcc8b953993c42182da3fa40 diff --git a/builders/tests/data/hashes/presubmit b/builders/tests/data/hashes/presubmit index 8e2b4d7c..a35c6c86 100644 --- a/builders/tests/data/hashes/presubmit +++ b/builders/tests/data/hashes/presubmit @@ -1 +1 @@ -40b437991cd3ec239fc0d0eef162005dc62784def9c40548f3ea235d99c2e5a0 +d9dab1c798d51f79e68fd8eb3bb83312086808d789bbc09d0f2dbf708ef5f114 diff --git a/builders/tests/data/hashes/test-tools b/builders/tests/data/hashes/test-tools index 72a3fa38..fc5e0b5c 100644 --- a/builders/tests/data/hashes/test-tools +++ b/builders/tests/data/hashes/test-tools @@ -1 +1 @@ -e19aaa4e08668be8056a6064e27be159ee7e158b3a90d75b7c4e5483369ebe41 +dd1ec6137d4dd22fec555044cd85f484adfa6c7b686880ea5449cff936bad34e diff --git a/builders/tests/run-tests b/builders/tests/run-tests index f98389a4..037067d2 100755 --- a/builders/tests/run-tests +++ b/builders/tests/run-tests @@ -243,7 +243,9 @@ fi generate_hashes_cbuild # CLI tests - +if [[ ${VERBOSE} -eq 1 ]]; then + set -o xtrace +fi cli_tests_misc for img in ${IMAGE_LIST}; do cli_tests_test-tools "${img}" diff --git a/builders/tools/awscurl b/builders/tools/awscurl index 355d58e1..975dbb17 100755 --- a/builders/tools/awscurl +++ b/builders/tools/awscurl @@ -29,8 +29,21 @@ set -o errexit +TOOLS_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +readonly TOOLS_DIR + +function check_arch() { + local -r ARCH="$("${TOOLS_DIR}"/get-architecture)" + if [[ ${ARCH} == arm64 ]]; then + printf "architecture %s not supported\n" "${ARCH}" &>/dev/stderr + exit 0 + fi +} + +check_arch + # shellcheck disable=SC1090 -source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/builder.sh +source "${TOOLS_DIR}"/builder.sh declare -a ENV_VARS builder::add_aws_env_vars ENV_VARS @@ -68,4 +81,4 @@ docker run \ --volume "${HOME}"/.aws/:/home/.aws/ \ --volume "${WORKSPACE}":/src/workspace \ --workdir /src/workspace/"${REL_PWD}" \ - ghcr.io/okigan/awscurl:latest "$@" + ghcr.io/okigan/awscurl:v0.29 "$@" diff --git a/builders/tools/cbuild b/builders/tools/cbuild index f0e4255f..c40a3aed 100755 --- a/builders/tools/cbuild +++ b/builders/tools/cbuild @@ -134,13 +134,6 @@ if ! [[ " ${IMAGE_LIST[*]} " =~ " ${IMAGE} " ]]; then usage 1 fi -if [[ ${WITH_CMD_PROFILER} -eq 1 ]]; then - if [[ ${IMAGE} != build-debian ]]; then - printf "error: --cmd-profiler is only compatible with build-debian\n" &>/dev/stderr - usage 1 - fi - CMD_PROFILER="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so " -fi TOOLS_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" readonly TOOLS_DIR # shellcheck disable=SC1090 @@ -160,6 +153,9 @@ if [[ ${VERBOSE} -eq 1 ]]; then printf "mounting workspace into container: %s\n" "${WORKSPACE_MOUNT}" &>/dev/stderr fi +TOOLS_RELDIR="$(realpath "${TOOLS_DIR}" --relative-to="${PWD}")" +readonly TOOLS_RELDIR + if [[ -n ${CBUILD_IMAGE} ]]; then IMAGE_TAGGED=${CBUILD_IMAGE} else @@ -167,12 +163,20 @@ else fi readonly IMAGE_TAGGED +PWD_WORKSPACE_REL_PATH="$(realpath --relative-base="${WORKSPACE}" "${PWD}")" +readonly PWD_WORKSPACE_REL_PATH +WORKDIR=/src/workspace +if [[ ${PWD_WORKSPACE_REL_PATH:0:1} != / ]]; then + WORKDIR="/src/workspace/${PWD_WORKSPACE_REL_PATH}" +fi +readonly WORKDIR + declare -a DOCKER_RUN_ARGS DOCKER_RUN_ARGS+=( "--rm" "--entrypoint=/bin/bash" "--volume=${WORKSPACE_MOUNT}:/src/workspace" - "--workdir=/src/workspace" + "--workdir=${WORKDIR}" "--network=${DOCKER_NETWORK}" "$(echo "${EXTRA_DOCKER_RUN_ARGS}" | envsubst)" ) @@ -181,6 +185,17 @@ if [[ ${DOCKER_SECCOMP_UNCONFINED} -eq 1 ]]; then DOCKER_RUN_ARGS+=("--security-opt=seccomp=unconfined") fi +if [[ ${WITH_CMD_PROFILER} -eq 1 ]]; then + if [[ ${IMAGE} != build-debian ]]; then + printf "error: --cmd-profiler is only compatible with build-debian\n" &>/dev/stderr + usage 1 + fi + DOCKER_RUN_ARGS+=( + "--env=CMD_PROFILER=LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so" + "--env=CPUPROFILE=benchmark.prof" + ) +fi + # inside the docker build images, /bazel_root is the bazel cache dir, per the system-wide bazelrc readonly BAZEL_ROOT=/bazel_root if [[ ${WITH_SHARED_CACHE} -eq 0 ]]; then @@ -222,10 +237,16 @@ if [[ -z ${CMD} ]]; then ${DOCKER_RUN_ARGS[@]} \ "${IMAGE_TAGGED}" \ --login +elif [[ ${WITH_CMD_PROFILER} -eq 1 ]]; then + # shellcheck disable=SC2068 + docker run \ + ${DOCKER_RUN_ARGS[@]} \ + "${IMAGE_TAGGED}" \ + --login -c "'${TOOLS_RELDIR}'/normalize-bazel-symlinks; env \${CMD_PROFILER} ${CMD}" else # shellcheck disable=SC2068 docker run \ ${DOCKER_RUN_ARGS[@]} \ "${IMAGE_TAGGED}" \ - --login -c "${CMD_PROFILER}${CMD}" + --login -c "${CMD}" fi diff --git a/builders/tools/collect-coverage b/builders/tools/collect-coverage index 2083fbdb..746185c2 100755 --- a/builders/tools/collect-coverage +++ b/builders/tools/collect-coverage @@ -14,12 +14,50 @@ # See the License for the specific language governing permissions and # limitations under the License. -# environment variables (all optional): +# environment variables: # WORKSPACE Set the path to the workspace (repo root) set -o pipefail set -o errexit +declare LCOV_REPORT="${WORKSPACE}/bazel-out/_coverage/_coverage_report.dat" +declare COVERAGE_FILENAME=coverage.zip + +function usage() { + local exitval=${1-1} + cat &>/dev/stderr << USAGE +usage: + $0 + --lcov_report path to lcov report relative to the WORKSPACE + --coverage_output_filename name of ZIP file that will contain artifacts from coverage collection +USAGE + # shellcheck disable=SC2086 + exit ${exitval} +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --lcov_report) + LCOV_REPORT="${WORKSPACE}/$2" + shift 2 || usage + ;; + --coverage_output_filename) + COVERAGE_FILENAME="$2" + shift 2 || usage + if [[ ${COVERAGE_FILENAME##*.} != zip ]]; then + printf "error: --coverage_output_filename must be a ZIP file\n" &>/dev/stderr + exit 1 + fi + ;; + -h | --help) + usage 0 + ;; + *) + usage + ;; + esac +done + trap _cleanup EXIT function _cleanup() { declare -r -i status=$? @@ -33,10 +71,9 @@ function _cleanup() { function generate_coverage_report() { local -r cov_dir="$(mktemp --tmpdir="${WORKSPACE}" --directory coverage-XXXX)" trap 'rm -rf "${cov_dir}"' RETURN EXIT - local -r cov_dat="${WORKSPACE}/bazel-out/_coverage/_coverage_report.dat" - cp "${cov_dat}" "${cov_dir}" + cp "${LCOV_REPORT}" "${cov_dir}"/_coverage_report.dat local -r dist_dir="${WORKSPACE}"/dist - cp "${cov_dat}" "${dist_dir}" + cp "${LCOV_REPORT}" "${dist_dir}"/_coverage_report.dat chmod -x {"${cov_dir}","${dist_dir}"}/_coverage_report.dat "${TOOLS_DIR}"/lcov --list dist/_coverage_report.dat >"${dist_dir}"/coverage_report.txt @@ -64,10 +101,5 @@ source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/builder.sh TOOLS_DIR="$(builder::get_tools_dir)" readonly TOOLS_DIR -declare COVERAGE_FILENAME="$1" -if [[ ${COVERAGE_FILENAME##*.} != zip ]]; then - COVERAGE_FILENAME=coverage.zip -fi - generate_coverage_report "${TOOLS_DIR}"/normalize-dist diff --git a/builders/tools/normalize-bazel-symlinks b/builders/tools/normalize-bazel-symlinks index a66f5f34..8506ac96 100755 --- a/builders/tools/normalize-bazel-symlinks +++ b/builders/tools/normalize-bazel-symlinks @@ -12,11 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [[ -f /.dockerenv ]]; then - printf "Running inside Docker container? This script is only designed to be executed outside docker\n" &>/dev/stderr - exit 1 -fi - declare -r BAZEL_CACHE_DIR="${HOME}/.cache/bazel" function normalize_symlink() { @@ -26,11 +21,28 @@ function normalize_symlink() { return fi local -r link_path="$(readlink "${link_name}")" - local -r output_user_root="${link_path///bazel_root\/}" + local -r output_user_root="${link_path///bazel_root/}" + rm -f "${link_name}" + ln -s "$(realpath "${BAZEL_CACHE_DIR}/${output_user_root}")" "${link_name}" +} + +function normalize_symlink_docker() { + declare -r link_name="$1" + if readlink --canonicalize-existing "${link_name}" &>/dev/null ; then + printf "symlink %s resolves fully, skipping\n" "${link_name}" + return + fi + local -r link_path="$(readlink "${link_name}")" + local -r output_user_root="${link_path##*/.cache/bazel/}" rm -f "${link_name}" - ln -s "${BAZEL_CACHE_DIR}/${output_user_root}" "${link_name}" + ln -s "$(realpath "/bazel_root/${output_user_root}")" "${link_name}" } +declare _normalize_fn=normalize_symlink +if [[ -f /.dockerenv ]]; then + _normalize_fn=normalize_symlink_docker +fi + declare -a -r LINK_DIRS=( bazel-bin bazel-out @@ -38,5 +50,7 @@ declare -a -r LINK_DIRS=( bazel-workspace ) for link in "${LINK_DIRS[@]}"; do - normalize_symlink "${link}" + if [[ -L ${link} ]]; then + ${_normalize_fn} "${link}" + fi done diff --git a/builders/tools/pre-commit b/builders/tools/pre-commit index be8c5c7c..7bfc7058 100755 --- a/builders/tools/pre-commit +++ b/builders/tools/pre-commit @@ -96,14 +96,13 @@ function __init() { export CBUILD_IMAGE="${IMAGE_TAGGED}" local -r ARCH="$("${TOOLS_DIR}"/get-architecture)" + SKIP_HOOKS="${SKIP}" if [[ ${ARCH} == arm64 ]]; then if [[ -z ${SKIP} ]]; then SKIP_HOOKS="terraform-fmt" else - SKIP_HOOKS="${SKIP},terraform-fmt" + SKIP_HOOKS+=",terraform-fmt" fi - else - SKIP_HOOKS="${SKIP}" fi if [[ -n ${SKIP_HOOKS} ]]; then printf "Skipping pre-commit hooks: %s\n" "${SKIP_HOOKS}" diff --git a/builders/tools/wrk2 b/builders/tools/wrk2 new file mode 120000 index 00000000..d609abda --- /dev/null +++ b/builders/tools/wrk2 @@ -0,0 +1 @@ +test-tool \ No newline at end of file diff --git a/builders/version.txt b/builders/version.txt index fdae41d2..78756de3 100644 --- a/builders/version.txt +++ b/builders/version.txt @@ -1 +1 @@ -0.48.0 \ No newline at end of file +0.57.0 \ No newline at end of file diff --git a/components/cloud_config/BUILD.bazel b/components/cloud_config/BUILD.bazel index 04ce12eb..8cd15ad6 100644 --- a/components/cloud_config/BUILD.bazel +++ b/components/cloud_config/BUILD.bazel @@ -30,10 +30,10 @@ cc_library( ], deps = [ "//public:constants", - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:marshalling", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -49,14 +49,14 @@ cc_library( "parameter_client.h", ], deps = [ - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//scp/cc/public/core/interface:errors", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface/parameter_client", + "@google_privacysandbox_servers_common//src/public/core/interface:errors", + "@google_privacysandbox_servers_common//src/public/cpio/interface/parameter_client", ], ) @@ -82,13 +82,13 @@ cc_test( ], "//:gcp_platform": [ "@com_google_googletest//:gtest", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface:cpio", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/mock/parameter_client:parameter_client_mock", + "@google_privacysandbox_servers_common//src/public/cpio/interface:cpio", + "@google_privacysandbox_servers_common//src/public/cpio/mock/parameter_client:parameter_client_mock", ], "//:local_platform": [ "//components/data/common:mocks", "//components/util:sleepfor_mock", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_github_grpc_grpc//:grpc++", ], }) + [ @@ -114,13 +114,13 @@ cc_library( "@aws_sdk_cpp//:ssm", ], "//:gcp_platform": [ - "@google_privacysandbox_servers_common//scp/cc/public/core/interface:errors", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface:cpio", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface/parameter_client", + "@google_privacysandbox_servers_common//src/public/core/interface:errors", + "@google_privacysandbox_servers_common//src/public/cpio/interface:cpio", + "@google_privacysandbox_servers_common//src/public/cpio/interface/parameter_client", ], "//conditions:default": [], }) + [ - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", ], @@ -147,21 +147,22 @@ cc_library( "//components/errors:gcp_error_util", "@com_github_googleapis_google_cloud_cpp//:compute_instances", "@com_google_absl//absl/flags:flag", - "@com_google_absl//absl/log:check", "@com_google_absl//absl/synchronization", - "@google_privacysandbox_servers_common//scp/cc/public/core/interface:execution_result", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface/instance_client", + "@google_privacysandbox_servers_common//src/public/core/interface:execution_result", + "@google_privacysandbox_servers_common//src/public/cpio/interface/instance_client", ], "//:local_instance": [ "@com_google_absl//absl/flags:flag", ], "//conditions:default": [], }) + [ - "@com_github_google_glog//:glog", + "//components/errors:error_tag", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) diff --git a/components/cloud_config/instance_client_aws.cc b/components/cloud_config/instance_client_aws.cc index 77d70b73..0191e891 100644 --- a/components/cloud_config/instance_client_aws.cc +++ b/components/cloud_config/instance_client_aws.cc @@ -16,6 +16,8 @@ #include #include +#include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" @@ -37,12 +39,17 @@ #include "aws/ec2/model/DescribeTagsResponse.h" #include "aws/ec2/model/Filter.h" #include "components/cloud_config/instance_client.h" +#include "components/errors/error_tag.h" #include "components/errors/error_util_aws.h" -#include "glog/logging.h" namespace kv_server { namespace { +enum class ErrorTag : int { + kGetAwsHttpResourceError = 1, + kAutoScalingSizeError = 2 +}; + using Aws::AutoScaling::Model::DescribeAutoScalingGroupsRequest; using Aws::AutoScaling::Model::Instance; using Aws::AutoScaling::Model::LifecycleState; @@ -93,8 +100,10 @@ absl::StatusOr GetAwsHttpResource( if (result.GetResponseCode() == Aws::Http::HttpResponseCode::OK) { return Aws::Utils::StringUtils::Trim(result.GetPayload().c_str()); } - return absl::Status(HttpResponseCodeToStatusCode(result.GetResponseCode()), - "Failed to get AWS Http resource."); + return StatusWithErrorTag( + absl::Status(HttpResponseCodeToStatusCode(result.GetResponseCode()), + "Failed to get AWS Http resource."), + __FILE__, ErrorTag::kGetAwsHttpResourceError); } absl::StatusOr GetImdsToken( @@ -142,7 +151,8 @@ absl::StatusOr GetAutoScalingGroupName( "Could not get auto scaling instances for instance ", instance_id, ". Retrieved ", outcome.GetResult().GetAutoScalingInstances().size(), " auto scaling groups."); - return absl::NotFoundError(error_msg); + return StatusWithErrorTag(absl::NotFoundError(error_msg), __FILE__, + ErrorTag::kAutoScalingSizeError); } return outcome.GetResult() .GetAutoScalingInstances()[0] diff --git a/components/cloud_config/instance_client_gcp.cc b/components/cloud_config/instance_client_gcp.cc index cd622517..4839204e 100644 --- a/components/cloud_config/instance_client_gcp.cc +++ b/components/cloud_config/instance_client_gcp.cc @@ -19,6 +19,7 @@ #include "absl/flags/flag.h" #include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" @@ -26,11 +27,10 @@ #include "absl/synchronization/notification.h" #include "components/cloud_config/instance_client.h" #include "components/errors/error_util_gcp.h" -#include "glog/logging.h" #include "google/cloud/compute/instances/v1/instances_client.h" -#include "scp/cc/public/core/interface/execution_result.h" -#include "scp/cc/public/cpio/interface/instance_client/instance_client_interface.h" -#include "src/cpp/util/status_macro/status_macros.h" +#include "src/public/core/interface/execution_result.h" +#include "src/public/cpio/interface/instance_client/instance_client_interface.h" +#include "src/util/status_macro/status_macros.h" ABSL_FLAG(std::string, shard_num, "0", "Shard number."); @@ -275,7 +275,7 @@ class GcpInstanceClient : public InstanceClient { if (result.Successful()) { resource_name = std::string{response.instance_resource_name()}; } else { - LOG(ERROR) << "Faild to get instance resource name: " + LOG(ERROR) << "Failed to get instance resource name: " << GetErrorMessage(result.status_code); } diff --git a/components/cloud_config/instance_client_local.cc b/components/cloud_config/instance_client_local.cc index 510565f8..32a30f19 100644 --- a/components/cloud_config/instance_client_local.cc +++ b/components/cloud_config/instance_client_local.cc @@ -17,10 +17,10 @@ #include #include "absl/flags/flag.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "components/cloud_config/instance_client.h" -#include "glog/logging.h" ABSL_FLAG(std::string, environment, "local", "Environment name."); ABSL_FLAG(std::string, shard_num, "0", "Shard number."); diff --git a/components/cloud_config/parameter_client_aws.cc b/components/cloud_config/parameter_client_aws.cc index 2a5655ac..bb13143a 100644 --- a/components/cloud_config/parameter_client_aws.cc +++ b/components/cloud_config/parameter_client_aws.cc @@ -20,6 +20,7 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/numbers.h" @@ -31,7 +32,6 @@ #include "aws/ssm/model/GetParameterResult.h" #include "components/cloud_config/parameter_client.h" #include "components/errors/error_util_aws.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/components/cloud_config/parameter_client_gcp.cc b/components/cloud_config/parameter_client_gcp.cc index ba9762f1..a5aee5e4 100644 --- a/components/cloud_config/parameter_client_gcp.cc +++ b/components/cloud_config/parameter_client_gcp.cc @@ -20,16 +20,17 @@ #include "absl/container/flat_hash_map.h" #include "absl/flags/flag.h" +#include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "absl/synchronization/blocking_counter.h" #include "components/cloud_config/parameter_client.h" -#include "glog/logging.h" -#include "scp/cc/public/core/interface/errors.h" -#include "scp/cc/public/core/interface/execution_result.h" -#include "scp/cc/public/cpio/interface/parameter_client/parameter_client_interface.h" +#include "src/public/core/interface/errors.h" +#include "src/public/core/interface/execution_result.h" +#include "src/public/cpio/interface/parameter_client/parameter_client_interface.h" namespace kv_server { namespace { @@ -106,6 +107,8 @@ class GcpParameterClient : public ParameterClient { << " with error: " << status; return status; } + LOG(INFO) << "Got parameter: " << parameter_name + << " with value: " << param_value; return param_value; } diff --git a/components/cloud_config/parameter_client_gcp_test.cc b/components/cloud_config/parameter_client_gcp_test.cc index 7e076998..1f81c585 100644 --- a/components/cloud_config/parameter_client_gcp_test.cc +++ b/components/cloud_config/parameter_client_gcp_test.cc @@ -22,9 +22,9 @@ #include "components/cloud_config/parameter_client.h" #include "gtest/gtest.h" -#include "scp/cc/public/cpio/interface/error_codes.h" -#include "scp/cc/public/cpio/interface/parameter_client/parameter_client_interface.h" -#include "scp/cc/public/cpio/mock/parameter_client/mock_parameter_client.h" +#include "src/public/cpio/interface/error_codes.h" +#include "src/public/cpio/interface/parameter_client/parameter_client_interface.h" +#include "src/public/cpio/mock/parameter_client/mock_parameter_client.h" namespace kv_server { namespace { diff --git a/components/cloud_config/parameter_client_local.cc b/components/cloud_config/parameter_client_local.cc index 7fb9882f..ea8bf5f4 100644 --- a/components/cloud_config/parameter_client_local.cc +++ b/components/cloud_config/parameter_client_local.cc @@ -51,11 +51,22 @@ ABSL_FLAG(bool, route_v1_to_v2, false, ABSL_FLAG(std::string, data_loading_file_format, std::string(kv_server::kFileFormats[static_cast( kv_server::FileFormat::kRiegeli)]), - "File format of the input data files."); + "File format of the input data files. See /public/constants.h for " + "possible values."); ABSL_FLAG(std::int32_t, logging_verbosity_level, 0, "Loggging verbosity level."); ABSL_FLAG(absl::Duration, udf_timeout, absl::Seconds(5), "Timeout for one UDF invocation"); +ABSL_FLAG(int32_t, udf_min_log_level, 0, + "Minimum logging level for UDFs. Info=0, Warn=1, Error=2. Default is " + "0(info)."); +ABSL_FLAG(bool, enable_otel_logger, false, "Whether to enable otel logger."); +ABSL_FLAG(std::string, telemetry_config, "mode: EXPERIMENT", + "Telemetry configuration for exporting raw or noised metrics"); +ABSL_FLAG(std::string, data_loading_prefix_allowlist, "", + "Allowlist for blob prefixes."); +ABSL_FLAG(bool, add_missing_keys_v1, false, + "Whether to add missing keys for v1."); namespace kv_server { namespace { @@ -79,6 +90,11 @@ class LocalParameterClient : public ParameterClient { absl::GetFlag(FLAGS_realtime_directory)}); string_flag_values_.insert({"kv-server-local-data-loading-file-format", absl::GetFlag(FLAGS_data_loading_file_format)}); + string_flag_values_.insert({"kv-server-local-telemetry-config", + absl::GetFlag(FLAGS_telemetry_config)}); + string_flag_values_.insert( + {"kv-server-local-data-loading-blob-prefix-allowlist", + absl::GetFlag(FLAGS_data_loading_prefix_allowlist)}); // Insert more string flag values here. int32_t_flag_values_.insert( @@ -111,13 +127,19 @@ class LocalParameterClient : public ParameterClient { int32_t_flag_values_.insert( {"kv-server-local-udf-timeout-millis", absl::ToInt64Milliseconds(absl::GetFlag(FLAGS_udf_timeout))}); + int32_t_flag_values_.insert({"kv-server-local-udf-min-log-level", + absl::GetFlag(FLAGS_udf_min_log_level)}); // Insert more int32 flag values here. bool_flag_values_.insert({"kv-server-local-route-v1-to-v2", absl::GetFlag(FLAGS_route_v1_to_v2)}); + bool_flag_values_.insert({"kv-server-local-add-missing-keys-v1", + absl::GetFlag(FLAGS_add_missing_keys_v1)}); bool_flag_values_.insert({"kv-server-local-use-real-coordinators", false}); bool_flag_values_.insert( {"kv-server-local-use-external-metrics-collector-endpoint", false}); bool_flag_values_.insert({"kv-server-local-use-sharding-key-regex", false}); + bool_flag_values_.insert({"kv-server-local-enable-otel-logger", + absl::GetFlag(FLAGS_enable_otel_logger)}); // Insert more bool flag values here. } diff --git a/components/cloud_config/parameter_client_local_test.cc b/components/cloud_config/parameter_client_local_test.cc index 839e8a4f..62393754 100644 --- a/components/cloud_config/parameter_client_local_test.cc +++ b/components/cloud_config/parameter_client_local_test.cc @@ -104,12 +104,24 @@ TEST(ParameterClientLocal, ExpectedFlagDefaultsArePresent) { ASSERT_TRUE(statusor.ok()); EXPECT_EQ(5000, *statusor); } + { + const auto statusor = + client->GetInt32Parameter("kv-server-local-udf-min-log-level"); + ASSERT_TRUE(statusor.ok()); + EXPECT_EQ(0, *statusor); + } { const auto statusor = client->GetBoolParameter("kv-server-local-route-v1-to-v2"); ASSERT_TRUE(statusor.ok()); EXPECT_EQ(false, *statusor); } + { + const auto statusor = + client->GetBoolParameter("kv-server-local-add-missing-keys-v1"); + ASSERT_TRUE(statusor.ok()); + EXPECT_EQ(false, *statusor); + } { const auto statusor = client->GetBoolParameter("kv-server-local-use-real-coordinators"); @@ -122,6 +134,18 @@ TEST(ParameterClientLocal, ExpectedFlagDefaultsArePresent) { ASSERT_TRUE(statusor.ok()); EXPECT_EQ(false, *statusor); } + { + const auto statusor = + client->GetBoolParameter("kv-server-local-enable-otel-logger"); + ASSERT_TRUE(statusor.ok()); + EXPECT_EQ(false, *statusor); + } + { + const auto statusor = + client->GetParameter("kv-server-local-telemetry-config"); + ASSERT_TRUE(statusor.ok()); + EXPECT_EQ("mode: EXPERIMENT", *statusor); + } } } // namespace diff --git a/components/data/blob_storage/BUILD.bazel b/components/data/blob_storage/BUILD.bazel index 3cb4deeb..3597529b 100644 --- a/components/data/blob_storage/BUILD.bazel +++ b/components/data/blob_storage/BUILD.bazel @@ -25,12 +25,12 @@ cc_library( hdrs = ["seeking_input_streambuf.h"], deps = [ "//components/telemetry:server_definition", - "@com_github_google_glog//:glog", "@com_google_absl//absl/base", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) @@ -43,8 +43,31 @@ cc_test( ":seeking_input_streambuf", "@com_google_absl//absl/strings", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", + ], +) + +cc_library( + name = "blob_prefix_allowlist", + srcs = ["blob_prefix_allowlist.cc"], + hdrs = ["blob_prefix_allowlist.h"], + deps = [ + "//public/data_loading:filename_utils", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", + ], +) + +cc_test( + name = "blob_prefix_allowlist_test", + size = "small", + srcs = ["blob_prefix_allowlist_test.cc"], + deps = [ + ":blob_prefix_allowlist", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", ], ) @@ -72,8 +95,9 @@ cc_library( ], "//conditions:default": [], }) + [ + ":blob_prefix_allowlist", ":seeking_input_streambuf", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -109,7 +133,6 @@ cc_test( "@com_github_grpc_grpc//:grpc++", "@com_google_absl//absl/flags:flag", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -160,10 +183,9 @@ cc_test( ], }) + [ ":blob_storage_change_notifier", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -176,6 +198,7 @@ cc_library( "delta_file_notifier.h", ], deps = [ + ":blob_prefix_allowlist", ":blob_storage_change_notifier", ":blob_storage_client", "//components/data/common:thread_manager", @@ -183,13 +206,14 @@ cc_library( "//components/util:sleepfor", "//public:constants", "//public/data_loading:filename_utils", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/synchronization", - "@google_privacysandbox_servers_common//src/cpp/util:duration", + "@google_privacysandbox_servers_common//src/util:duration", ], ) diff --git a/components/data/blob_storage/blob_prefix_allowlist.cc b/components/data/blob_storage/blob_prefix_allowlist.cc new file mode 100644 index 00000000..1a0e0b23 --- /dev/null +++ b/components/data/blob_storage/blob_prefix_allowlist.cc @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "components/data/blob_storage/blob_prefix_allowlist.h" + +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" + +namespace kv_server { +namespace { +constexpr std::string_view kBlobNameDelimiter = "/"; +constexpr std::string_view kPrefixListDelimiter = ","; +} // namespace + +BlobPrefixAllowlist::BlobName ParseBlobName(std::string_view blob_name) { + if (blob_name.empty()) { + return BlobPrefixAllowlist::BlobName{}; + } + std::string blob_name_copy(blob_name); + std::reverse(blob_name_copy.begin(), blob_name_copy.end()); + std::vector name_parts = absl::StrSplit( + blob_name_copy, + absl::MaxSplits(/*delimiter=*/kBlobNameDelimiter, /*limit=*/1)); + for (auto& name_part : name_parts) { + std::reverse(name_part.begin(), name_part.end()); + } + auto prefix = name_parts.size() == 1 ? "" : std::move(name_parts.back()); + return BlobPrefixAllowlist::BlobName{.prefix = std::move(prefix), + .key = std::move(name_parts.front())}; +} + +BlobPrefixAllowlist::BlobPrefixAllowlist(std::string_view allowed_prefixes) { + std::vector prefixes = + absl::StrSplit(allowed_prefixes, kPrefixListDelimiter); + // We always allow reading blobs at the bucket level. + allowed_prefixes_.insert(std::string(kDefaultBlobPrefix)); + allowed_prefixes_.insert(prefixes.begin(), prefixes.end()); +} + +bool BlobPrefixAllowlist::Contains(std::string_view prefix) const { + return allowed_prefixes_.contains(prefix); +} + +bool BlobPrefixAllowlist::ContainsBlobPrefix(std::string_view blob_name) const { + return Contains(ParseBlobName(blob_name).prefix); +} + +const absl::flat_hash_set& BlobPrefixAllowlist::Prefixes() const { + return allowed_prefixes_; +} + +} // namespace kv_server diff --git a/components/data/blob_storage/blob_prefix_allowlist.h b/components/data/blob_storage/blob_prefix_allowlist.h new file mode 100644 index 00000000..52ccad78 --- /dev/null +++ b/components/data/blob_storage/blob_prefix_allowlist.h @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_DATA_BLOB_STORAGE_BLOB_PREFIX_ALLOWLIST_H_ +#define COMPONENTS_DATA_BLOB_STORAGE_BLOB_PREFIX_ALLOWLIST_H_ + +#include +#include + +#include "absl/container/flat_hash_set.h" + +namespace kv_server { + +// Default prefix for blobs at the bucket level. +constexpr std::string_view kDefaultBlobPrefix = ""; + +// List of blob prefixes that are allowlisted for data loading. Blobs with +// prefix not included in this are ignored. +class BlobPrefixAllowlist { + public: + struct BlobName { + std::string prefix; + std::string key; + }; + + BlobPrefixAllowlist() : BlobPrefixAllowlist("") {} + explicit BlobPrefixAllowlist(std::string_view allowed_prefixes); + + // Returns true if `prefix` is allowlisted, meaning that blobs with this + // prefix are eligible for data loading. + [[nodiscard]] bool Contains(std::string_view prefix) const; + // Returns true if `blob_name`'s is allowlisted, meaning that the blob is + // eligible for data loading. + [[nodiscard]] bool ContainsBlobPrefix(std::string_view blob_name) const; + // Returns the set of prefixes contained in this list. + [[nodiscard]] const absl::flat_hash_set& Prefixes() const; + + private: + absl::flat_hash_set allowed_prefixes_; +}; + +// Parses a `blob_name` into it's corresponding name parts. +// +// For example: +// (1) blob_name="prefix1/DELTA_1705430864435450" => {.prefix="prefix1", +// .key="DELTA_1705430864435450"} +// (2) blob_name="DELTA_1705430864435450" => {.prefix="", +// .key="DELTA_1705430864435450"} +BlobPrefixAllowlist::BlobName ParseBlobName(std::string_view blob_name); + +} // namespace kv_server + +#endif // COMPONENTS_DATA_BLOB_STORAGE_BLOB_PREFIX_ALLOWLIST_H_ diff --git a/components/data/blob_storage/blob_prefix_allowlist_test.cc b/components/data/blob_storage/blob_prefix_allowlist_test.cc new file mode 100644 index 00000000..8ebcdea2 --- /dev/null +++ b/components/data/blob_storage/blob_prefix_allowlist_test.cc @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "components/data/blob_storage/blob_prefix_allowlist.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace kv_server { +namespace { + +using testing::UnorderedElementsAre; + +TEST(BlobPrefixAllowlistTest, ValidateParsingBlobName) { + auto result = ParseBlobName("DELTA_1705430864435450"); + EXPECT_EQ(result.key, "DELTA_1705430864435450"); + EXPECT_EQ(result.prefix, ""); + + result = ParseBlobName("prefix/DELTA_1705430864435450"); + EXPECT_EQ(result.key, "DELTA_1705430864435450"); + EXPECT_EQ(result.prefix, "prefix"); + + result = ParseBlobName("prefix1/prefix2/DELTA_1705430864435450"); + EXPECT_EQ(result.key, "DELTA_1705430864435450"); + EXPECT_EQ(result.prefix, "prefix1/prefix2"); +} + +TEST(BlobPrefixAllowlistTest, ValidateContainsPrefix) { + auto allowlist = BlobPrefixAllowlist("prefix1,prefix1/prefix2"); + EXPECT_TRUE(allowlist.Contains("")); + EXPECT_TRUE(allowlist.ContainsBlobPrefix("DELTA_1705430864435450")); + EXPECT_TRUE(allowlist.Contains("prefix1")); + EXPECT_TRUE(allowlist.ContainsBlobPrefix("prefix1/DELTA_1705430864435450")); + EXPECT_TRUE(allowlist.Contains("prefix1/prefix2")); + EXPECT_TRUE( + allowlist.ContainsBlobPrefix("prefix1/prefix2/DELTA_1705430864435450")); + EXPECT_FALSE(allowlist.Contains("non-existant")); + EXPECT_FALSE( + allowlist.ContainsBlobPrefix("non-existant/DELTA_1705430864435450")); +} + +TEST(BlobPrefixAllowlistTest, ValidateGettingAllPrefixes) { + auto allowlist = BlobPrefixAllowlist("prefix1,prefix1/prefix2"); + EXPECT_THAT(allowlist.Prefixes(), + UnorderedElementsAre("", "prefix1", "prefix1/prefix2")); +} + +} // namespace +} // namespace kv_server diff --git a/components/data/blob_storage/blob_storage_change_notifier_s3.cc b/components/data/blob_storage/blob_storage_change_notifier_s3.cc index 4e02db89..0f24f007 100644 --- a/components/data/blob_storage/blob_storage_change_notifier_s3.cc +++ b/components/data/blob_storage/blob_storage_change_notifier_s3.cc @@ -13,13 +13,13 @@ // limitations under the License. #include "absl/functional/bind_front.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "aws/core/utils/json/JsonSerializer.h" #include "components/data/blob_storage/blob_storage_change_notifier.h" #include "components/data/common/change_notifier.h" #include "components/telemetry/server_definition.h" -#include "glog/logging.h" namespace kv_server { namespace { @@ -47,10 +47,7 @@ class S3BlobStorageChangeNotifier : public BlobStorageChangeNotifier { if (!parsedMessage.ok()) { LOG(ERROR) << "Failed to parse JSON. Error: " << parsedMessage.status() << " Message:" << message; - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kAwsJsonParseError), 1}})); + LogServerErrorMetric(kAwsJsonParseError); continue; } parsed_notifications.push_back(std::move(*parsedMessage)); diff --git a/components/data/blob_storage/blob_storage_change_notifier_s3_test.cc b/components/data/blob_storage/blob_storage_change_notifier_s3_test.cc index 472c7e04..0709d015 100644 --- a/components/data/blob_storage/blob_storage_change_notifier_s3_test.cc +++ b/components/data/blob_storage/blob_storage_change_notifier_s3_test.cc @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "aws/sqs/SQSClient.h" #include "aws/sqs/model/ReceiveMessageRequest.h" @@ -21,7 +22,6 @@ #include "components/data/common/msg_svc.h" #include "components/telemetry/server_definition.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" #include "gmock/gmock.h" #include "gtest/gtest.h" diff --git a/components/data/blob_storage/blob_storage_client.h b/components/data/blob_storage/blob_storage_client.h index 796fba03..68cb3f27 100644 --- a/components/data/blob_storage/blob_storage_client.h +++ b/components/data/blob_storage/blob_storage_client.h @@ -26,6 +26,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" namespace kv_server { @@ -43,10 +44,12 @@ class BlobStorageClient { public: struct DataLocation { std::string bucket; + std::string prefix; std::string key; bool operator==(const DataLocation& other) const { - return key == other.key && bucket == other.bucket; + return prefix == other.prefix && key == other.key && + bucket == other.bucket; } }; struct ListOptions { @@ -56,7 +59,7 @@ class BlobStorageClient { // Options for the underyling storage client. struct ClientOptions { - ClientOptions() {} + ClientOptions() = default; int64_t max_connections = std::thread::hardware_concurrency(); int64_t max_range_bytes = 8 * 1024 * 1024; // 8MB }; @@ -81,7 +84,9 @@ class BlobStorageClient { inline std::ostream& operator<<( std::ostream& os, const BlobStorageClient::DataLocation& location) { - os << location.bucket << "/" << location.key; + location.prefix.empty() + ? os << location.bucket << "/" << location.key + : os << location.bucket << "/" << location.prefix << "/" << location.key; return os; } diff --git a/components/data/blob_storage/blob_storage_client_gcp.cc b/components/data/blob_storage/blob_storage_client_gcp.cc index 9e17c4eb..5be23c51 100644 --- a/components/data/blob_storage/blob_storage_client_gcp.cc +++ b/components/data/blob_storage/blob_storage_client_gcp.cc @@ -24,18 +24,25 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "components/data/blob_storage/blob_prefix_allowlist.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/blob_storage/seeking_input_streambuf.h" #include "components/errors/error_util_gcp.h" -#include "glog/logging.h" #include "google/cloud/storage/client.h" namespace kv_server { namespace { +std::string AppendPrefix(const std::string& value, const std::string& prefix) { + return prefix.empty() ? value : absl::StrCat(prefix, "/", value); +} + class GcpBlobInputStreamBuf : public SeekingInputStreambuf { public: GcpBlobInputStreamBuf(google::cloud::storage::Client& client, @@ -50,8 +57,8 @@ class GcpBlobInputStreamBuf : public SeekingInputStreambuf { protected: absl::StatusOr SizeImpl() override { - auto object_metadata = - client_.GetObjectMetadata(location_.bucket, location_.key); + auto object_metadata = client_.GetObjectMetadata( + location_.bucket, AppendPrefix(location_.key, location_.prefix)); if (!object_metadata) { return GoogleErrorStatusToAbslStatus(object_metadata.status()); } @@ -61,7 +68,7 @@ class GcpBlobInputStreamBuf : public SeekingInputStreambuf { absl::StatusOr ReadChunk(int64_t offset, int64_t chunk_size, char* dest_buffer) override { auto stream = client_.ReadObject( - location_.bucket, location_.key, + location_.bucket, AppendPrefix(location_.key, location_.prefix), google::cloud::storage::ReadRange(offset, offset + chunk_size)); if (!stream.status().ok()) { return GoogleErrorStatusToAbslStatus(stream.status()); @@ -84,7 +91,8 @@ class GcpBlobReader : public BlobReader { : BlobReader(), streambuf_(client, location, GetOptions([this, location](absl::Status status) { - LOG(ERROR) << "Blob " << location.key + LOG(ERROR) << "Blob " + << AppendPrefix(location.key, location.prefix) << " failed stream with: " << status; is_.setstate(std::ios_base::badbit); })), @@ -117,7 +125,8 @@ std::unique_ptr GcpBlobStorageClient::GetBlobReader( absl::Status GcpBlobStorageClient::PutBlob(BlobReader& blob_reader, DataLocation location) { - auto blob_ostream = client_->WriteObject(location.bucket, location.key); + auto blob_ostream = client_->WriteObject( + location.bucket, AppendPrefix(location.key, location.prefix)); if (!blob_ostream) { return GoogleErrorStatusToAbslStatus(blob_ostream.last_status()); } @@ -129,16 +138,19 @@ absl::Status GcpBlobStorageClient::PutBlob(BlobReader& blob_reader, } absl::Status GcpBlobStorageClient::DeleteBlob(DataLocation location) { - google::cloud::Status status = - client_->DeleteObject(location.bucket, location.key); + google::cloud::Status status = client_->DeleteObject( + location.bucket, AppendPrefix(location.key, location.prefix)); return status.ok() ? absl::OkStatus() : GoogleErrorStatusToAbslStatus(status); } absl::StatusOr> GcpBlobStorageClient::ListBlobs( DataLocation location, ListOptions options) { - auto list_object_reader = client_->ListObjects( - location.bucket, google::cloud::storage::Prefix(options.prefix), - google::cloud::storage::StartOffset(options.start_after)); + auto list_object_reader = + client_->ListObjects(location.bucket, + google::cloud::storage::Prefix( + AppendPrefix(options.prefix, location.prefix)), + google::cloud::storage::StartOffset(AppendPrefix( + options.start_after, location.prefix))); std::vector keys; if (list_object_reader.begin() == list_object_reader.end()) { return keys; @@ -149,13 +161,13 @@ absl::StatusOr> GcpBlobStorageClient::ListBlobs( << std::move(object_metadata).status().message(); continue; } - // Manually exclude the starting name as the StartOffset option is - // inclusive. - if (object_metadata->name() == options.start_after) { + // inclusive and also drop blobs with different prefix. + auto blob = ParseBlobName(object_metadata->name()); + if (blob.key == options.start_after || blob.prefix != location.prefix) { continue; } - keys.push_back(object_metadata->name()); + keys.push_back(std::move(blob.key)); } std::sort(keys.begin(), keys.end()); return keys; diff --git a/components/data/blob_storage/blob_storage_client_gcp_test.cc b/components/data/blob_storage/blob_storage_client_gcp_test.cc index 510fe0d2..e08c952e 100644 --- a/components/data/blob_storage/blob_storage_client_gcp_test.cc +++ b/components/data/blob_storage/blob_storage_client_gcp_test.cc @@ -39,12 +39,13 @@ #include "google/cloud/storage/testing/canonical_errors.h" #include "google/cloud/storage/testing/mock_client.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { using ::google::cloud::storage::testing::canonical_errors::TransientError; +using testing::AllOf; +using testing::Property; class GcpBlobStorageClientTest : public ::testing::Test { protected: @@ -191,5 +192,30 @@ TEST_F(GcpBlobStorageClientTest, ListBlobWithNoNewObject) { EXPECT_TRUE(my_blobs->begin() == my_blobs->end()); } +TEST_F(GcpBlobStorageClientTest, DeleteBlobWithPrefixSucceeds) { + namespace gcs = google::cloud::storage; + std::shared_ptr mock = + std::make_shared(); + std::unique_ptr mock_client = std::make_unique( + gcs::internal::ClientImplDetails::CreateWithoutDecorations(mock)); + EXPECT_CALL(*mock, + DeleteObject(AllOf( + Property(&gcs::internal::DeleteObjectRequest::bucket_name, + "test_bucket"), + Property(&gcs::internal::DeleteObjectRequest::object_name, + "test_prefix/test_object")))) + .WillOnce(testing::Return( + google::cloud::make_status_or(gcs::internal::EmptyResponse{}))); + + std::unique_ptr client = + std::make_unique(std::move(mock_client)); + BlobStorageClient::DataLocation location{ + .bucket = "test_bucket", + .prefix = "test_prefix", + .key = "test_object", + }; + EXPECT_TRUE(client->DeleteBlob(location).ok()); +} + } // namespace } // namespace kv_server diff --git a/components/data/blob_storage/blob_storage_client_local.cc b/components/data/blob_storage/blob_storage_client_local.cc index d55d0829..8f823a38 100644 --- a/components/data/blob_storage/blob_storage_client_local.cc +++ b/components/data/blob_storage/blob_storage_client_local.cc @@ -22,11 +22,11 @@ #include #include +#include "absl/log/log.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/blob_storage/seeking_input_streambuf.h" -#include "glog/logging.h" namespace kv_server { namespace { @@ -87,9 +87,13 @@ absl::Status FileBlobStorageClient::DeleteBlob(DataLocation location) { } absl::StatusOr> FileBlobStorageClient::ListBlobs( DataLocation location, ListOptions options) { + auto directory_name = + location.prefix.empty() + ? location.bucket + : absl::StrCat(location.bucket, "/", location.prefix); { std::error_code error_code; - std::filesystem::directory_entry directory{location.bucket, error_code}; + std::filesystem::directory_entry directory{directory_name, error_code}; if (error_code) { return absl::InternalError(absl::StrCat("Error getting directory entry: ", error_code.message())); @@ -98,7 +102,7 @@ absl::StatusOr> FileBlobStorageClient::ListBlobs( std::error_code error_code; std::vector blob_names; for (const auto& dir_entry : - std::filesystem::directory_iterator(location.bucket, error_code)) { + std::filesystem::directory_iterator(directory_name, error_code)) { if (dir_entry.is_directory()) { continue; } @@ -120,7 +124,10 @@ absl::StatusOr> FileBlobStorageClient::ListBlobs( std::filesystem::path FileBlobStorageClient::GetFullPath( const DataLocation& location) { - return std::filesystem::path(location.bucket) / location.key; + return location.prefix.empty() + ? std::filesystem::path(location.bucket) / location.key + : std::filesystem::path(location.bucket) / location.prefix / + location.key; } namespace { diff --git a/components/data/blob_storage/blob_storage_client_local_test.cc b/components/data/blob_storage/blob_storage_client_local_test.cc index fb00ac1b..e8b714d2 100644 --- a/components/data/blob_storage/blob_storage_client_local_test.cc +++ b/components/data/blob_storage/blob_storage_client_local_test.cc @@ -33,6 +33,11 @@ namespace kv_server { namespace { +void CreateSubDir(std::string_view subdir_name) { + std::filesystem::create_directory( + std::filesystem::path(::testing::TempDir()) / subdir_name); +} + void CreateFileInTmpDir(const std::string& filename) { const std::filesystem::path path = std::filesystem::path(::testing::TempDir()) / filename; @@ -121,6 +126,42 @@ TEST(LocalBlobStorageClientTest, PutBlob) { client->PutBlob(*from_blob_reader, to).code()); } +TEST(LocalBlobStorageClientTest, DeleteBlobWithPrefix) { + std::unique_ptr client = + std::make_unique(); + CreateSubDir("prefix"); + BlobStorageClient::DataLocation location{ + .bucket = ::testing::TempDir(), + .prefix = "prefix", + .key = "object", + }; + CreateFileInTmpDir("prefix/object"); + auto status = client->DeleteBlob(location); + EXPECT_TRUE(status.ok()) << status; + location.key = "non-existent-object"; + status = client->DeleteBlob(location); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInternal) << status; +} + +TEST(LocalBlobStorageClientTest, ListSubDirectoryWithFiles) { + std::unique_ptr client = + std::make_unique(); + CreateSubDir("prefix"); + CreateFileInTmpDir("prefix/object1"); + CreateFileInTmpDir("prefix/object2"); + CreateFileInTmpDir("prefix/object3"); + BlobStorageClient::DataLocation location{ + .bucket = ::testing::TempDir(), + .prefix = "prefix", + }; + kv_server::BlobStorageClient::ListOptions options; + auto status_or = client->ListBlobs(location, options); + ASSERT_TRUE(status_or.ok()) << status_or.status(); + EXPECT_EQ(*status_or, + std::vector({"object1", "object2", "object3"})); +} + // TODO(237669491): Add tests here } // namespace diff --git a/components/data/blob_storage/blob_storage_client_s3.cc b/components/data/blob_storage/blob_storage_client_s3.cc index e3556e5a..984e593c 100644 --- a/components/data/blob_storage/blob_storage_client_s3.cc +++ b/components/data/blob_storage/blob_storage_client_s3.cc @@ -19,7 +19,10 @@ #include #include +#include "absl/log/log.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" #include "aws/core/Aws.h" #include "aws/core/utils/threading/Executor.h" #include "aws/s3/S3Client.h" @@ -32,14 +35,18 @@ #include "aws/s3/model/PutObjectRequest.h" #include "aws/transfer/TransferHandle.h" #include "aws/transfer/TransferManager.h" +#include "components/data/blob_storage/blob_prefix_allowlist.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/blob_storage/seeking_input_streambuf.h" #include "components/errors/error_util_aws.h" -#include "glog/logging.h" namespace kv_server { namespace { +std::string AppendPrefix(const std::string& value, const std::string& prefix) { + return prefix.empty() ? value : absl::StrCat(prefix, "/", value); +} + // Sequentially load byte range data with a fixed amount of memory usage. class S3BlobInputStreamBuf : public SeekingInputStreambuf { public: @@ -57,7 +64,7 @@ class S3BlobInputStreamBuf : public SeekingInputStreambuf { absl::StatusOr SizeImpl() override { Aws::S3::Model::HeadObjectRequest request; request.SetBucket(location_.bucket); - request.SetKey(location_.key); + request.SetKey(AppendPrefix(location_.key, location_.prefix)); auto outcome = client_.HeadObject(request); if (!outcome.IsSuccess()) { return AwsErrorToStatus(outcome.GetError()); @@ -69,7 +76,7 @@ class S3BlobInputStreamBuf : public SeekingInputStreambuf { char* dest_buffer) override { Aws::S3::Model::GetObjectRequest request; request.SetBucket(location_.bucket); - request.SetKey(location_.key); + request.SetKey(AppendPrefix(location_.key, location_.prefix)); request.SetRange(GetRange(offset, chunk_size)); auto outcome = client_.GetObject(request); if (!outcome.IsSuccess()) { @@ -160,7 +167,7 @@ absl::Status S3BlobStorageClient::PutBlob(BlobReader& reader, // The owner of the stream is the caller. auto handle = transfer_manager_->UploadFile( std::shared_ptr(iostream.get(), [](std::iostream*) {}), - location.bucket, location.key, "", {}); + location.bucket, AppendPrefix(location.key, location.prefix), "", {}); handle->WaitUntilFinished(); const bool success = handle->GetStatus() == Aws::Transfer::TransferStatus::COMPLETED; @@ -170,7 +177,7 @@ absl::Status S3BlobStorageClient::PutBlob(BlobReader& reader, absl::Status S3BlobStorageClient::DeleteBlob(DataLocation location) { Aws::S3::Model::DeleteObjectRequest request; request.SetBucket(std::move(location.bucket)); - request.SetKey(std::move(location.key)); + request.SetKey(AppendPrefix(location.key, location.prefix)); const auto outcome = client_->DeleteObject(request); return outcome.IsSuccess() ? absl::OkStatus() : AwsErrorToStatus(outcome.GetError()); @@ -181,10 +188,12 @@ absl::StatusOr> S3BlobStorageClient::ListBlobs( Aws::S3::Model::ListObjectsV2Request request; request.SetBucket(std::move(location.bucket)); if (!options.prefix.empty()) { - request.SetPrefix(std::move(options.prefix)); + request.SetPrefix( + AppendPrefix(/*value=*/options.prefix, /*prefix=*/location.prefix)); } if (!options.start_after.empty()) { - request.SetStartAfter(std::move(options.start_after)); + request.SetStartAfter(AppendPrefix(/*value=*/options.start_after, + /*prefix=*/location.prefix)); } bool done = false; std::vector keys; @@ -196,7 +205,10 @@ absl::StatusOr> S3BlobStorageClient::ListBlobs( const Aws::Vector objects = outcome.GetResult().GetContents(); for (const Aws::S3::Model::Object& object : objects) { - keys.push_back(object.GetKey()); + if (auto blob = ParseBlobName(object.GetKey()); + blob.prefix == location.prefix) { + keys.emplace_back(std::move(blob.key)); + } } done = !outcome.GetResult().GetIsTruncated(); if (!done) { diff --git a/components/data/blob_storage/blob_storage_client_s3_test.cc b/components/data/blob_storage/blob_storage_client_s3_test.cc index 955382a5..7d20884e 100644 --- a/components/data/blob_storage/blob_storage_client_s3_test.cc +++ b/components/data/blob_storage/blob_storage_client_s3_test.cc @@ -31,12 +31,15 @@ #include "components/data/blob_storage/blob_storage_client.h" #include "components/telemetry/server_definition.h" #include "components/util/platform_initializer.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { +using testing::AllOf; +using testing::Property; + constexpr int64_t kMaxRangeBytes = 1024 * 1024 * 8; class MockS3Client : public ::Aws::S3::S3Client { @@ -158,5 +161,64 @@ TEST_F(BlobStorageClientS3Test, ListBlobsFails) { client->ListBlobs(location, list_options).status().code()); } +TEST_F(BlobStorageClientS3Test, DeleteBlobWithPrefixSucceeds) { + auto mock_s3_client = std::make_shared(); + Aws::S3::Model::DeleteObjectResult result; // An empty result means success + EXPECT_CALL( + *mock_s3_client, + DeleteObject(::testing::AllOf( + testing::Property(&Aws::S3::Model::DeleteObjectRequest::GetBucket, + "bucket"), + testing::Property(&Aws::S3::Model::DeleteObjectRequest::GetKey, + "prefix/object")))) + .WillOnce(::testing::Return(result)); + std::unique_ptr client = + std::make_unique(mock_s3_client, kMaxRangeBytes); + BlobStorageClient::DataLocation location{ + .bucket = "bucket", + .prefix = "prefix", + .key = "object", + }; + EXPECT_TRUE(client->DeleteBlob(location).ok()); +} + +TEST_F(BlobStorageClientS3Test, ListBlobsWithPrefixSucceeds) { + auto mock_s3_client = std::make_shared(); + { + Aws::S3::Model::ListObjectsV2Result + result; // An empty result means success. + Aws::S3::Model::Object object_to_return; + object_to_return.SetKey("directory1/DELTA_1699834075511696"); + Aws::Vector objects_to_return = {object_to_return}; + result.SetContents(objects_to_return); + EXPECT_CALL( + *mock_s3_client, + ListObjectsV2( + AllOf(Property(&Aws::S3::Model::ListObjectsV2Request::GetBucket, + "bucket"), + Property(&Aws::S3::Model::ListObjectsV2Request::GetPrefix, + "directory1/DELTA"), + Property(&Aws::S3::Model::ListObjectsV2Request::GetStartAfter, + "directory1/DELTA_1699834075511695")))) + .WillOnce(::testing::Return(result)); + } + + std::unique_ptr client = + std::make_unique(mock_s3_client, kMaxRangeBytes); + BlobStorageClient::DataLocation location{ + .bucket = "bucket", + .prefix = "directory1", + }; + BlobStorageClient::ListOptions list_options{ + .prefix = "DELTA", + .start_after = "DELTA_1699834075511695", + }; + absl::StatusOr> response = + client->ListBlobs(location, list_options); + ASSERT_TRUE(response.ok()); + EXPECT_THAT(*response, + testing::UnorderedElementsAreArray({"DELTA_1699834075511696"})); +} + } // namespace } // namespace kv_server diff --git a/components/data/blob_storage/delta_file_notifier.cc b/components/data/blob_storage/delta_file_notifier.cc index ac67cdb4..cdef6df9 100644 --- a/components/data/blob_storage/delta_file_notifier.cc +++ b/components/data/blob_storage/delta_file_notifier.cc @@ -19,14 +19,15 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "components/data/common/thread_manager.h" #include "components/errors/retry.h" -#include "glog/logging.h" #include "public/constants.h" #include "public/data_loading/filename_utils.h" -#include "src/cpp/util/duration.h" +#include "src/util/duration.h" namespace kv_server { namespace { @@ -39,24 +40,27 @@ class DeltaFileNotifierImpl : public DeltaFileNotifier { explicit DeltaFileNotifierImpl(BlobStorageClient& client, const absl::Duration poll_frequency, std::unique_ptr sleep_for, - SteadyClock& clock) - : thread_manager_(TheadManager::Create("Delta file notifier")), + SteadyClock& clock, + BlobPrefixAllowlist blob_prefix_allowlist) + : thread_manager_(ThreadManager::Create("Delta file notifier")), client_(client), poll_frequency_(poll_frequency), sleep_for_(std::move(sleep_for)), - clock_(clock) {} + clock_(clock), + blob_prefix_allowlist_(std::move(blob_prefix_allowlist)) {} absl::Status Start( BlobStorageChangeNotifier& change_notifier, - BlobStorageClient::DataLocation location, std::string start_after, - std::function callback) override { - return thread_manager_->Start([this, location = std::move(location), - start_after = std::move(start_after), - callback = std::move(callback), - &change_notifier]() mutable { - Watch(change_notifier, std::move(location), std::move(start_after), - std::move(callback)); - }); + BlobStorageClient::DataLocation location, + absl::flat_hash_map&& prefix_start_after_map, + std::function callback) override { + return thread_manager_->Start( + [this, location = std::move(location), + prefix_start_after_map = std::move(prefix_start_after_map), + callback = std::move(callback), &change_notifier]() mutable { + Watch(change_notifier, std::move(location), + std::move(prefix_start_after_map), std::move(callback)); + }); } absl::Status Stop() override { @@ -71,128 +75,179 @@ class DeltaFileNotifierImpl : public DeltaFileNotifier { // Returns max DeltaFile in alphabetical order from notification // Empty string if wait_duration exceeded. absl::StatusOr WaitForNotification( - BlobStorageChangeNotifier& change_notifier, - absl::Duration wait_duration) { - absl::StatusOr> changes = - change_notifier.GetNotifications( - wait_duration, [this]() { return thread_manager_->ShouldStop(); }); - if (!changes.ok()) { - return changes.status(); - } - std::string_view max_change = ""; - for (const auto& change : *changes) { - if (change > max_change && IsDeltaFilename(change)) { - max_change = change; - } - } - return std::string(max_change); - } - + BlobStorageChangeNotifier& change_notifier, absl::Duration wait_duration); // Returns true if we need to check for new blobs. Returns the error on // failure. absl::StatusOr ShouldListBlobs( BlobStorageChangeNotifier& change_notifier, ExpiringFlag& expiring_flag, - std::string_view last_key) { - if (!expiring_flag.Get()) { - VLOG(5) << "Backup poll"; - return true; + const absl::flat_hash_map& + prefix_start_after_map); + void Watch( + BlobStorageChangeNotifier& change_notifier, + BlobStorageClient::DataLocation location, + absl::flat_hash_map&& prefix_start_after_map, + std::function callback); + + std::unique_ptr thread_manager_; + BlobStorageClient& client_; + const absl::Duration poll_frequency_; + std::unique_ptr sleep_for_; + SteadyClock& clock_; + BlobPrefixAllowlist blob_prefix_allowlist_; +}; + +absl::StatusOr DeltaFileNotifierImpl::WaitForNotification( + BlobStorageChangeNotifier& change_notifier, absl::Duration wait_duration) { + absl::StatusOr> changes = + change_notifier.GetNotifications( + wait_duration, [this]() { return thread_manager_->ShouldStop(); }); + if (!changes.ok()) { + return changes.status(); + } + std::string_view max_change = ""; + for (const auto& change : *changes) { + if (auto blob = ParseBlobName(change); + change > max_change && IsDeltaFilename(blob.key)) { + max_change = change; } - absl::StatusOr notification_key = - WaitForNotification(change_notifier, expiring_flag.GetTimeRemaining()); - if (notification_key.ok() && *notification_key < last_key) { - // Ignore notifications for keys we've already seen. - return false; + } + return std::string(max_change); +} + +// Returns true if we need to check for new blobs. Returns the error on +// failure. +absl::StatusOr DeltaFileNotifierImpl::ShouldListBlobs( + BlobStorageChangeNotifier& change_notifier, ExpiringFlag& expiring_flag, + const absl::flat_hash_map& + prefix_start_after_map) { + if (!expiring_flag.Get()) { + VLOG(5) << "Backup poll"; + return true; + } + absl::StatusOr notification_key = + WaitForNotification(change_notifier, expiring_flag.GetTimeRemaining()); + // Don't poll on error. A backup poll will trigger if necessary. + if (absl::IsDeadlineExceeded(notification_key.status())) { + // Deadline exceeded while waiting, trigger backup poll + VLOG(5) << "Backup poll"; + return true; + } + if (!notification_key.ok()) { + return notification_key.status(); + } + auto notification_blob = ParseBlobName(*notification_key); + if (auto iter = prefix_start_after_map.find(notification_blob.prefix); + iter != prefix_start_after_map.end() && + notification_blob.key < iter->second) { + // Ignore notifications for keys we've already seen. + return false; + } + // Return True if there is a Delta file notification + // False is returned on DeadlineExceeded. + return blob_prefix_allowlist_.Contains(notification_blob.prefix) && + IsDeltaFilename(notification_blob.key); +} + +absl::flat_hash_map> ListPrefixDeltaFiles( + BlobStorageClient::DataLocation location, + const BlobPrefixAllowlist& prefix_allowlist, + const absl::flat_hash_map& prefix_start_after_map, + BlobStorageClient& blob_client) { + absl::flat_hash_map> prefix_blobs_map; + for (const auto& blob_prefix : prefix_allowlist.Prefixes()) { + location.prefix = blob_prefix; + auto iter = prefix_start_after_map.find(blob_prefix); + absl::StatusOr> result = blob_client.ListBlobs( + location, + {.prefix = std::string(FilePrefix()), + .start_after = + (iter == prefix_start_after_map.end()) ? "" : iter->second}); + if (!result.ok()) { + LOG(ERROR) << "Failed to list " << location << ": " << result.status(); + continue; } - if (absl::IsDeadlineExceeded(notification_key.status())) { - // Deadline exceeded while waiting, trigger backup poll - VLOG(5) << "Backup poll"; - return true; + if (result->empty()) { + continue; } - // Don't poll on error. A backup poll will trigger if necessary. - if (!notification_key.ok()) { - return notification_key.status(); + auto& prefix_blobs = prefix_blobs_map[blob_prefix]; + prefix_blobs.reserve(result->size()); + for (const auto& blob : *result) { + prefix_blobs.push_back(blob); } - // Return True if there is a Delta file notification - // False is returned on DeadlineExceeded. - return IsDeltaFilename(*notification_key); } + return prefix_blobs_map; +} - void Watch(BlobStorageChangeNotifier& change_notifier, - BlobStorageClient::DataLocation location, std::string start_after, - std::function callback) { - LOG(INFO) << "Started to watch " << location; - std::string last_key = std::move(start_after); - // Flag starts expired, and forces an initial poll. - ExpiringFlag expiring_flag(clock_); - uint32_t sequential_failures = 0; - while (!thread_manager_->ShouldStop()) { - const absl::StatusOr should_list_blobs = - ShouldListBlobs(change_notifier, expiring_flag, last_key); - if (!should_list_blobs.ok()) { - ++sequential_failures; - const absl::Duration backoff_time = - std::min(expiring_flag.GetTimeRemaining(), - ExponentialBackoffForRetry(sequential_failures)); - LOG(ERROR) << "Failed to get delta file notifications: " - << should_list_blobs.status() << ". Waiting for " - << backoff_time; - if (!sleep_for_->Duration(backoff_time)) { - LOG(ERROR) << "Failed to sleep for " << backoff_time - << ". SleepFor invalid."; - } - continue; - } - sequential_failures = 0; - if (!*should_list_blobs) { - continue; - } - absl::StatusOr> result = client_.ListBlobs( - location, {.prefix = std::string(FilePrefix()), - .start_after = last_key}); - if (!result.ok()) { - LOG(ERROR) << "Failed to list " << location << ": " << result.status(); - continue; +void DeltaFileNotifierImpl::Watch( + BlobStorageChangeNotifier& change_notifier, + BlobStorageClient::DataLocation location, + absl::flat_hash_map&& prefix_start_after_map, + std::function callback) { + LOG(INFO) << "Started to watch " << location; + // Flag starts expired, and forces an initial poll. + ExpiringFlag expiring_flag(clock_); + uint32_t sequential_failures = 0; + while (!thread_manager_->ShouldStop()) { + const absl::StatusOr should_list_blobs = + ShouldListBlobs(change_notifier, expiring_flag, prefix_start_after_map); + if (!should_list_blobs.ok()) { + ++sequential_failures; + const absl::Duration backoff_time = + std::min(expiring_flag.GetTimeRemaining(), + ExponentialBackoffForRetry(sequential_failures)); + LOG(ERROR) << "Failed to get delta file notifications: " + << should_list_blobs.status() << ". Waiting for " + << backoff_time; + if (!sleep_for_->Duration(backoff_time)) { + LOG(ERROR) << "Failed to sleep for " << backoff_time + << ". SleepFor invalid."; } - // Set expiring flag before callback for unit test simplicity. - // Fake clock is moved forward in callback so flag must be set beforehand. - expiring_flag.Set(poll_frequency_); - int delta_file_count = 0; - for (const std::string& key : *result) { - if (!IsDeltaFilename(key)) { + continue; + } + sequential_failures = 0; + if (!*should_list_blobs) { + continue; + } + // Set expiring flag before callback for unit test simplicity. + // Fake clock is moved forward in callback so flag must be set beforehand. + expiring_flag.Set(poll_frequency_); + int delta_file_count = 0; + auto prefix_blobs_map = ListPrefixDeltaFiles( + location, blob_prefix_allowlist_, prefix_start_after_map, client_); + for (const auto& [prefix, prefix_blobs] : prefix_blobs_map) { + for (const auto& blob : prefix_blobs) { + if (!IsDeltaFilename(blob)) { continue; } - callback(key); - last_key = key; + callback(prefix.empty() ? blob : absl::StrCat(prefix, "/", blob)); + prefix_start_after_map[prefix] = blob; delta_file_count++; } - if (!delta_file_count) { - VLOG(2) << "No new file found"; - } + } + if (delta_file_count == 0) { + VLOG(2) << "No new file found"; } } - - std::unique_ptr thread_manager_; - BlobStorageClient& client_; - const absl::Duration poll_frequency_; - std::unique_ptr sleep_for_; - SteadyClock& clock_; -}; +} } // namespace std::unique_ptr DeltaFileNotifier::Create( - BlobStorageClient& client, const absl::Duration poll_frequency) { - return std::make_unique(client, poll_frequency, - std::make_unique(), - SteadyClock::RealClock()); + BlobStorageClient& client, const absl::Duration poll_frequency, + BlobPrefixAllowlist blob_prefix_allowlist) { + return std::make_unique( + client, poll_frequency, std::make_unique(), + SteadyClock::RealClock(), std::move(blob_prefix_allowlist)); } // For test only std::unique_ptr DeltaFileNotifier::Create( BlobStorageClient& client, const absl::Duration poll_frequency, - std::unique_ptr sleep_for, SteadyClock& clock) { - return std::make_unique(client, poll_frequency, - std::move(sleep_for), clock); + std::unique_ptr sleep_for, SteadyClock& clock, + BlobPrefixAllowlist blob_prefix_allowlist) { + return std::make_unique( + client, poll_frequency, std::move(sleep_for), clock, + std::move(blob_prefix_allowlist)); } } // namespace kv_server diff --git a/components/data/blob_storage/delta_file_notifier.h b/components/data/blob_storage/delta_file_notifier.h index c18a647f..6df74bce 100644 --- a/components/data/blob_storage/delta_file_notifier.h +++ b/components/data/blob_storage/delta_file_notifier.h @@ -20,12 +20,14 @@ #include #include +#include "absl/container/flat_hash_map.h" +#include "components/data/blob_storage/blob_prefix_allowlist.h" #include "components/data/blob_storage/blob_storage_change_notifier.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/common/thread_manager.h" #include "components/errors/retry.h" #include "components/util/sleepfor.h" -#include "src/cpp/util/duration.h" +#include "src/util/duration.h" namespace kv_server { @@ -46,8 +48,9 @@ class DeltaFileNotifier { // the constructor. virtual absl::Status Start( BlobStorageChangeNotifier& change_notifier, - BlobStorageClient::DataLocation location, std::string start_after, - std::function callback) = 0; + BlobStorageClient::DataLocation location, + absl::flat_hash_map&& prefix_start_after_map, + std::function callback) = 0; // Blocks until `IsRunning` is False. virtual absl::Status Stop() = 0; @@ -58,13 +61,15 @@ class DeltaFileNotifier { static std::unique_ptr Create( BlobStorageClient& client, - const absl::Duration poll_frequency = absl::Minutes(5)); + const absl::Duration poll_frequency = absl::Minutes(5), + BlobPrefixAllowlist blob_prefix_allowlist = BlobPrefixAllowlist("")); // Used for test static std::unique_ptr Create( BlobStorageClient& client, const absl::Duration poll_frequency, std::unique_ptr sleep_for, - privacy_sandbox::server_common::SteadyClock& clock); + privacy_sandbox::server_common::SteadyClock& clock, + BlobPrefixAllowlist blob_prefix_allowlist = BlobPrefixAllowlist("")); }; } // namespace kv_server diff --git a/components/data/blob_storage/delta_file_notifier_test.cc b/components/data/blob_storage/delta_file_notifier_test.cc index d9e359b5..b6e28224 100644 --- a/components/data/blob_storage/delta_file_notifier_test.cc +++ b/components/data/blob_storage/delta_file_notifier_test.cc @@ -26,6 +26,7 @@ #include "public/data_loading/filename_utils.h" using testing::_; +using testing::AllOf; using testing::Field; using testing::Return; @@ -34,14 +35,17 @@ namespace { using privacy_sandbox::server_common::SimulatedSteadyClock; +constexpr std::string_view kBlobPrefix1 = "prefix1"; + class DeltaFileNotifierTest : public ::testing::Test { protected: void SetUp() override { std::unique_ptr mock_sleep_for = std::make_unique(); sleep_for_ = mock_sleep_for.get(); - notifier_ = DeltaFileNotifier::Create( - client_, poll_frequency_, std::move(mock_sleep_for), sim_clock_); + notifier_ = DeltaFileNotifier::Create(client_, poll_frequency_, + std::move(mock_sleep_for), sim_clock_, + BlobPrefixAllowlist(kBlobPrefix1)); } MockBlobStorageClient client_; @@ -59,20 +63,21 @@ TEST_F(DeltaFileNotifierTest, NotRunning) { TEST_F(DeltaFileNotifierTest, StartFailure) { BlobStorageClient::DataLocation location = {.bucket = "testbucket"}; - absl::Status status = - notifier_->Start(change_notifier_, {.bucket = "testbucket"}, initial_key_, - [](const std::string&) {}); + absl::Status status = notifier_->Start( + change_notifier_, {.bucket = "testbucket"}, + {std::make_pair("", initial_key_)}, [](const std::string&) {}); ASSERT_TRUE(status.ok()); status = notifier_->Start(change_notifier_, {.bucket = "testbucket"}, - initial_key_, [](const std::string&) {}); + {std::make_pair("", initial_key_)}, + [](const std::string&) {}); ASSERT_FALSE(status.ok()); } TEST_F(DeltaFileNotifierTest, StartsAndStops) { BlobStorageClient::DataLocation location = {.bucket = "testbucket"}; - absl::Status status = - notifier_->Start(change_notifier_, {.bucket = "testbucket"}, initial_key_, - [](const std::string&) {}); + absl::Status status = notifier_->Start( + change_notifier_, {.bucket = "testbucket"}, + {std::make_pair("", initial_key_)}, [](const std::string&) {}); ASSERT_TRUE(status.ok()); EXPECT_TRUE(notifier_->IsRunning()); status = notifier_->Stop(); @@ -99,6 +104,13 @@ TEST_F(DeltaFileNotifierTest, NotifiesWithNewFiles) { Field(&BlobStorageClient::ListOptions::start_after, ToDeltaFileName(3).value()))) .WillOnce(Return(std::vector({ToDeltaFileName(4).value()}))); + EXPECT_CALL( + client_, + ListBlobs( + AllOf(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::DataLocation::prefix, kBlobPrefix1)), + Field(&BlobStorageClient::ListOptions::start_after, ""))) + .WillRepeatedly(Return(std::vector())); absl::Notification finished; testing::MockFunction callback; @@ -112,9 +124,9 @@ TEST_F(DeltaFileNotifierTest, NotifiesWithNewFiles) { finished.Notify(); }); - absl::Status status = - notifier_->Start(change_notifier_, {.bucket = "testbucket"}, initial_key_, - callback.AsStdFunction()); + absl::Status status = notifier_->Start( + change_notifier_, {.bucket = "testbucket"}, + {std::make_pair("", initial_key_)}, callback.AsStdFunction()); ASSERT_TRUE(status.ok()); EXPECT_TRUE(notifier_->IsRunning()); finished.WaitForNotification(); @@ -147,6 +159,13 @@ TEST_F(DeltaFileNotifierTest, NotifiesWithInvalidFilesIngored) { Field(&BlobStorageClient::ListOptions::start_after, ToDeltaFileName(3).value()))) .WillOnce(Return(std::vector({ToDeltaFileName(4).value()}))); + EXPECT_CALL( + client_, + ListBlobs( + AllOf(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::DataLocation::prefix, kBlobPrefix1)), + Field(&BlobStorageClient::ListOptions::start_after, ""))) + .WillRepeatedly(Return(std::vector())); EXPECT_CALL( client_, ListBlobs(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), @@ -169,9 +188,9 @@ TEST_F(DeltaFileNotifierTest, NotifiesWithInvalidFilesIngored) { finished.Notify(); }); - absl::Status status = - notifier_->Start(change_notifier_, {.bucket = "testbucket"}, initial_key_, - callback.AsStdFunction()); + absl::Status status = notifier_->Start( + change_notifier_, {.bucket = "testbucket"}, + {std::make_pair("", initial_key_)}, callback.AsStdFunction()); ASSERT_TRUE(status.ok()); EXPECT_TRUE(notifier_->IsRunning()); finished.WaitForNotification(); @@ -194,7 +213,13 @@ TEST_F(DeltaFileNotifierTest, GetChangesFailure) { ToDeltaFileName(1).value()))) .WillOnce(Return(std::vector({}))) .WillOnce(Return(std::vector({ToDeltaFileName(1).value()}))); - + EXPECT_CALL( + client_, + ListBlobs( + AllOf(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::DataLocation::prefix, kBlobPrefix1)), + Field(&BlobStorageClient::ListOptions::start_after, ""))) + .WillRepeatedly(Return(std::vector())); absl::Notification finished; testing::MockFunction callback; EXPECT_CALL(callback, Call).Times(1).WillOnce([&](const std::string& key) { @@ -208,9 +233,9 @@ TEST_F(DeltaFileNotifierTest, GetChangesFailure) { .Times(1) .WillOnce(Return(true)); - absl::Status status = - notifier_->Start(change_notifier_, {.bucket = "testbucket"}, initial_key_, - callback.AsStdFunction()); + absl::Status status = notifier_->Start( + change_notifier_, {.bucket = "testbucket"}, + {std::make_pair("", initial_key_)}, callback.AsStdFunction()); ASSERT_TRUE(status.ok()); EXPECT_TRUE(notifier_->IsRunning()); finished.WaitForNotification(); @@ -241,7 +266,13 @@ TEST_F(DeltaFileNotifierTest, BackupPoll) { Field(&BlobStorageClient::ListOptions::start_after, ToDeltaFileName(3).value()))) .WillOnce(Return(std::vector({ToDeltaFileName(4).value()}))); - + EXPECT_CALL( + client_, + ListBlobs( + AllOf(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::DataLocation::prefix, kBlobPrefix1)), + Field(&BlobStorageClient::ListOptions::start_after, ""))) + .WillRepeatedly(Return(std::vector())); absl::Notification finished; testing::MockFunction callback; EXPECT_CALL(callback, Call) @@ -261,8 +292,70 @@ TEST_F(DeltaFileNotifierTest, BackupPoll) { finished.Notify(); }); + absl::Status status = notifier_->Start( + change_notifier_, {.bucket = "testbucket"}, + {std::make_pair("", initial_key_)}, callback.AsStdFunction()); + ASSERT_TRUE(status.ok()); + EXPECT_TRUE(notifier_->IsRunning()); + finished.WaitForNotification(); + status = notifier_->Stop(); + ASSERT_TRUE(status.ok()); + EXPECT_FALSE(notifier_->IsRunning()); +} + +TEST_F(DeltaFileNotifierTest, NotifiesWithNewPrefixedFiles) { + BlobStorageClient::DataLocation location = {.bucket = "testbucket"}; + EXPECT_CALL(change_notifier_, GetNotifications(_, _)) + .WillOnce(Return(std::vector({ToDeltaFileName(3).value()}))) + .WillOnce(Return(std::vector({ToDeltaFileName(4).value()}))) + .WillRepeatedly(Return(std::vector())); + EXPECT_CALL( + client_, + ListBlobs(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::ListOptions::start_after, + ToDeltaFileName(1).value()))) + .WillOnce(Return(std::vector({}))) + .WillOnce(Return(std::vector({ToDeltaFileName(3).value()}))); + EXPECT_CALL( + client_, + ListBlobs(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::ListOptions::start_after, + ToDeltaFileName(3).value()))) + .WillOnce(Return(std::vector({ToDeltaFileName(4).value()}))); + EXPECT_CALL( + client_, + ListBlobs( + AllOf(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::DataLocation::prefix, kBlobPrefix1)), + Field(&BlobStorageClient::ListOptions::start_after, + ToDeltaFileName(10).value()))) + .WillOnce( + Return(std::vector({ToDeltaFileName(11).value()}))); + EXPECT_CALL( + client_, + ListBlobs( + AllOf(Field(&BlobStorageClient::DataLocation::bucket, "testbucket"), + Field(&BlobStorageClient::DataLocation::prefix, kBlobPrefix1)), + Field(&BlobStorageClient::ListOptions::start_after, + ToDeltaFileName(11).value()))) + .WillRepeatedly(Return(std::vector())); + + absl::Notification finished; + testing::MockFunction callback; + EXPECT_CALL(callback, Call(ToDeltaFileName(3).value())); + EXPECT_CALL(callback, Call(absl::StrCat(kBlobPrefix1, "/", + ToDeltaFileName(11).value()))); + EXPECT_CALL(callback, Call(ToDeltaFileName(4).value())).WillOnce([&]() { + finished.Notify(); + }); + absl::Status status = - notifier_->Start(change_notifier_, {.bucket = "testbucket"}, initial_key_, + notifier_->Start(change_notifier_, {.bucket = "testbucket"}, + { + std::make_pair("", initial_key_), + std::make_pair(std::string(kBlobPrefix1), + ToDeltaFileName(10).value()), + }, callback.AsStdFunction()); ASSERT_TRUE(status.ok()); EXPECT_TRUE(notifier_->IsRunning()); diff --git a/components/data/blob_storage/seeking_input_streambuf.cc b/components/data/blob_storage/seeking_input_streambuf.cc index 531ad8cf..fdb3bba1 100644 --- a/components/data/blob_storage/seeking_input_streambuf.cc +++ b/components/data/blob_storage/seeking_input_streambuf.cc @@ -21,11 +21,11 @@ #include #include "absl/base/optimization.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "components/telemetry/server_definition.h" -#include "glog/logging.h" namespace kv_server { namespace { @@ -57,7 +57,9 @@ std::streampos SeekingInputStreambuf::seekpos(std::streampos pos, std::streampos SeekingInputStreambuf::seekoff(std::streamoff off, std::ios_base::seekdir dir, std::ios_base::openmode which) { - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); const auto size = Size(); if (ABSL_PREDICT_FALSE(!size.ok())) { MaybeReportError(size.status()); @@ -105,17 +107,14 @@ std::streampos SeekingInputStreambuf::seekoff(std::streamoff off, buffer_.data() + (new_position - BufferStartPosition()), buffer_.data() + buffer_.length()); } - auto duration = absl::Now() - start_time; - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); - MaybeVerboseLogLatency(kSeekoffEventName, duration); + MaybeVerboseLogLatency(kSeekoffEventName, latency_recorder.GetLatency()); return std::streampos(std::streamoff(new_position)); } std::streambuf::int_type SeekingInputStreambuf::underflow() { - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); const auto size = Size(); if (ABSL_PREDICT_FALSE(!size.ok())) { MaybeReportError(size.status()); @@ -149,12 +148,7 @@ std::streambuf::int_type SeekingInputStreambuf::underflow() { buffer_.resize(total_bytes_read); } setg(buffer_.data(), buffer_.data(), buffer_.data() + buffer_.length()); - auto duration = absl::Now() - start_time; - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); - MaybeVerboseLogLatency(kUnderflowEventName, duration); + MaybeVerboseLogLatency(kUnderflowEventName, latency_recorder.GetLatency()); return traits_type::to_int_type(buffer_[0]); } @@ -175,7 +169,9 @@ int64_t SeekingInputStreambuf::BufferCursorPosition() { } absl::StatusOr SeekingInputStreambuf::Size() { - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); if (ABSL_PREDICT_TRUE(src_cached_size_ >= 0)) { return src_cached_size_; } @@ -184,12 +180,7 @@ absl::StatusOr SeekingInputStreambuf::Size() { return size.status(); } src_cached_size_ = *size; - auto duration = absl::Now() - start_time; - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); - MaybeVerboseLogLatency(kSizeEventName, duration); + MaybeVerboseLogLatency(kSizeEventName, latency_recorder.GetLatency()); return *size; } diff --git a/components/data/blob_storage/seeking_input_streambuf.h b/components/data/blob_storage/seeking_input_streambuf.h index b7bc0235..c76a0c5f 100644 --- a/components/data/blob_storage/seeking_input_streambuf.h +++ b/components/data/blob_storage/seeking_input_streambuf.h @@ -20,7 +20,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" #ifndef COMPONENTS_DATA_BLOB_STORAGE_SEEKING_INPUT_STREAMBUF_H_ #define COMPONENTS_DATA_BLOB_STORAGE_SEEKING_INPUT_STREAMBUF_H_ diff --git a/components/data/blob_storage/seeking_input_streambuf_test.cc b/components/data/blob_storage/seeking_input_streambuf_test.cc index 242d8795..845f6995 100644 --- a/components/data/blob_storage/seeking_input_streambuf_test.cc +++ b/components/data/blob_storage/seeking_input_streambuf_test.cc @@ -27,7 +27,6 @@ #include "absl/strings/str_format.h" #include "components/telemetry/server_definition.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data/common/BUILD.bazel b/components/data/common/BUILD.bazel index feb343cc..05830260 100644 --- a/components/data/common/BUILD.bazel +++ b/components/data/common/BUILD.bazel @@ -49,7 +49,7 @@ cc_library( "//conditions:default": [], }) + [ ":msg_svc_util", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/random", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", @@ -88,11 +88,11 @@ cc_library( ":message_service", "//components/telemetry:server_definition", "//components/util:sleepfor", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) @@ -123,10 +123,9 @@ cc_test( ], }) + [ ":change_notifier", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -142,13 +141,13 @@ cc_library( "//components/errors:retry", "//public:constants", "//public/data_loading:filename_utils", - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/synchronization", - "@google_privacysandbox_servers_common//src/cpp/util:duration", + "@google_privacysandbox_servers_common//src/util:duration", ], ) @@ -175,6 +174,7 @@ cc_library( "//components/data/blob_storage:delta_file_notifier", "//components/data/realtime:realtime_notifier", "//components/data/realtime:realtime_thread_pool_manager", + "@com_google_absl//absl/container:flat_hash_map", "@com_google_googletest//:gtest", ], ) diff --git a/components/data/common/change_notifier_aws.cc b/components/data/common/change_notifier_aws.cc index d8f0fa0e..9e1ee74c 100644 --- a/components/data/common/change_notifier_aws.cc +++ b/components/data/common/change_notifier_aws.cc @@ -18,6 +18,7 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" @@ -42,8 +43,7 @@ #include "components/data/common/msg_svc.h" #include "components/errors/error_util_aws.h" #include "components/telemetry/server_definition.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { namespace { @@ -78,11 +78,7 @@ class AwsChangeNotifier : public ChangeNotifier { absl::Status status = queue_manager_->SetupQueue(); if (!status.ok()) { LOG(ERROR) << "Could not set up queue for topic " << sns_arn_; - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kAwsChangeNotifierQueueSetupFailure), 1}})); + LogServerErrorMetric(kAwsChangeNotifierQueueSetupFailure); return status; } } @@ -130,10 +126,7 @@ class AwsChangeNotifier : public ChangeNotifier { } else { LOG(ERROR) << "Failed to TagQueue with " << kLastUpdatedTag << ": " << tag << " " << status; - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kAwsChangeNotifierTagFailure), 1}})); + LogServerErrorMetric(kAwsChangeNotifierTagFailure); } } } @@ -155,13 +148,7 @@ class AwsChangeNotifier : public ChangeNotifier { if (!outcome.IsSuccess()) { LOG(ERROR) << "Failed to receive message from SQS: " << outcome.GetError().GetMessage(); - - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kAwsChangeNotifierMessagesReceivingFailure), - 1}})); + LogServerErrorMetric(kAwsChangeNotifierMessagesReceivingFailure); if (!outcome.GetError().ShouldRetry()) { // Handle case where recreating Queue will resolve the issue. // Example: Queue accidentally deleted. @@ -186,11 +173,7 @@ class AwsChangeNotifier : public ChangeNotifier { } DeleteMessages(GetSqsUrl(), messages); if (keys.empty()) { - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kAwsChangeNotifierMessagesDataLoss), 1}})); + LogServerErrorMetric(kAwsChangeNotifierMessagesDataLoss); return absl::DataLossError("All messages invalid."); } return keys; @@ -216,12 +199,7 @@ class AwsChangeNotifier : public ChangeNotifier { if (!outcome.IsSuccess()) { LOG(ERROR) << "Failed to delete message from SQS: " << outcome.GetError().GetMessage(); - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kAwsChangeNotifierMessagesDeletionFailure), - 1}})); + LogServerErrorMetric(kAwsChangeNotifierMessagesDeletionFailure); } } diff --git a/components/data/common/change_notifier_aws_test.cc b/components/data/common/change_notifier_aws_test.cc index 5970ccd6..2db5e4f6 100644 --- a/components/data/common/change_notifier_aws_test.cc +++ b/components/data/common/change_notifier_aws_test.cc @@ -16,6 +16,7 @@ #include +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "aws/sqs/SQSClient.h" #include "aws/sqs/model/DeleteMessageBatchRequest.h" @@ -24,7 +25,6 @@ #include "components/data/common/msg_svc.h" #include "components/telemetry/server_definition.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" #include "gmock/gmock.h" #include "gtest/gtest.h" diff --git a/components/data/common/change_notifier_gcp.cc b/components/data/common/change_notifier_gcp.cc index 2c9718ba..b9f4c19c 100644 --- a/components/data/common/change_notifier_gcp.cc +++ b/components/data/common/change_notifier_gcp.cc @@ -19,12 +19,12 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/time/clock.h" #include "components/data/common/change_notifier.h" #include "components/util/sleepfor.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/components/data/common/change_notifier_gcp_test.cc b/components/data/common/change_notifier_gcp_test.cc index 4ee34e84..94c58ad7 100644 --- a/components/data/common/change_notifier_gcp_test.cc +++ b/components/data/common/change_notifier_gcp_test.cc @@ -20,11 +20,10 @@ #include #include +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "components/data/common/change_notifier.h" -#include "glog/logging.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data/common/change_notifier_local.cc b/components/data/common/change_notifier_local.cc index 0015c858..94d8a09d 100644 --- a/components/data/common/change_notifier_local.cc +++ b/components/data/common/change_notifier_local.cc @@ -16,13 +16,13 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/time/clock.h" #include "components/data/common/change_notifier.h" #include "components/util/sleepfor.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/components/data/common/change_notifier_local_test.cc b/components/data/common/change_notifier_local_test.cc index 94fc27eb..073fe472 100644 --- a/components/data/common/change_notifier_local_test.cc +++ b/components/data/common/change_notifier_local_test.cc @@ -20,11 +20,10 @@ #include #include +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "components/data/common/change_notifier.h" -#include "glog/logging.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data/common/mocks.h b/components/data/common/mocks.h index aa0ea669..707b0a43 100644 --- a/components/data/common/mocks.h +++ b/components/data/common/mocks.h @@ -19,6 +19,7 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/blob_storage/delta_file_notifier.h" #include "components/data/common/change_notifier.h" @@ -50,12 +51,10 @@ class MockBlobStorageChangeNotifier : public BlobStorageChangeNotifier { class MockDeltaFileNotifier : public DeltaFileNotifier { public: MockDeltaFileNotifier() : DeltaFileNotifier() {} - MOCK_METHOD(absl::Status, Start, - (BlobStorageChangeNotifier & change_notifier, - BlobStorageClient::DataLocation location, - std::string start_after, - std::function callback), + (BlobStorageChangeNotifier&, BlobStorageClient::DataLocation, + (absl::flat_hash_map&&), + std::function), (override)); MOCK_METHOD(absl::Status, Stop, (), (override)); MOCK_METHOD(bool, IsRunning, (), (const, override)); diff --git a/components/data/common/msg_svc_gcp.cc b/components/data/common/msg_svc_gcp.cc index 99ba9b4b..892c298e 100644 --- a/components/data/common/msg_svc_gcp.cc +++ b/components/data/common/msg_svc_gcp.cc @@ -14,13 +14,13 @@ #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "components/data/common/msg_svc.h" #include "components/data/common/msg_svc_util.h" #include "components/errors/error_util_gcp.h" -#include "glog/logging.h" #include "google/cloud/pubsub/message.h" #include "google/cloud/pubsub/subscriber.h" #include "google/cloud/pubsub/subscription_admin_client.h" diff --git a/components/data/common/thread_manager.cc b/components/data/common/thread_manager.cc index 1cfd6e42..59c6c6b0 100644 --- a/components/data/common/thread_manager.cc +++ b/components/data/common/thread_manager.cc @@ -21,21 +21,21 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "components/errors/retry.h" -#include "glog/logging.h" #include "public/constants.h" #include "public/data_loading/filename_utils.h" -#include "src/cpp/util/duration.h" +#include "src/util/duration.h" namespace kv_server { namespace { -class TheadManagerImpl : public TheadManager { +class ThreadManagerImpl : public ThreadManager { public: - explicit TheadManagerImpl(std::string thread_name) + explicit ThreadManagerImpl(std::string thread_name) : thread_name_(std::move(thread_name)) {} - ~TheadManagerImpl() { + ~ThreadManagerImpl() { if (!IsRunning()) return; VLOG(8) << thread_name_ << " In destructor. Attempting to stop the thread."; if (const auto s = Stop(); !s.ok()) { @@ -78,8 +78,8 @@ class TheadManagerImpl : public TheadManager { } // namespace -std::unique_ptr TheadManager::Create(std::string thread_name) { - return std::make_unique(std::move(thread_name)); +std::unique_ptr ThreadManager::Create(std::string thread_name) { + return std::make_unique(std::move(thread_name)); } } // namespace kv_server diff --git a/components/data/common/thread_manager.h b/components/data/common/thread_manager.h index abd9f24b..123e9d43 100644 --- a/components/data/common/thread_manager.h +++ b/components/data/common/thread_manager.h @@ -24,11 +24,11 @@ namespace kv_server { -class TheadManager { +class ThreadManager { public: - virtual ~TheadManager() = default; + virtual ~ThreadManager() = default; - // Checks if the TheadManager is already running. + // Checks if the ThreadManager is already running. // If not, starts a thread on which `watch` is executed. // Start and Stop should be called on the same thread as // the constructor. @@ -43,7 +43,7 @@ class TheadManager { virtual bool ShouldStop() = 0; - static std::unique_ptr Create(std::string thread_name); + static std::unique_ptr Create(std::string thread_name); }; } // namespace kv_server diff --git a/components/data/file_group/BUILD.bazel b/components/data/file_group/BUILD.bazel new file mode 100644 index 00000000..28d96438 --- /dev/null +++ b/components/data/file_group/BUILD.bazel @@ -0,0 +1,73 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") + +package(default_visibility = [ + "//components:__subpackages__", +]) + +cc_library( + name = "file_group", + srcs = ["file_group.cc"], + hdrs = ["file_group.h"], + deps = [ + "//public:base_types_cc_proto", + "//public/data_loading:filename_utils", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", + ], +) + +cc_test( + name = "file_group_test", + size = "small", + srcs = ["file_group_test.cc"], + deps = [ + ":file_group", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "file_group_search_utils", + srcs = ["file_group_search_utils.cc"], + hdrs = ["file_group_search_utils.h"], + deps = [ + ":file_group", + "//components/data/blob_storage:blob_storage_client", + "//public:base_types_cc_proto", + "//public:constants", + "//public/data_loading:filename_utils", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", + ], +) + +cc_test( + name = "file_group_search_utils_test", + size = "small", + srcs = ["file_group_search_utils_test.cc"], + deps = [ + ":file_group", + ":file_group_search_utils", + "//components/data/blob_storage:blob_storage_client", + "//components/data/common:mocks", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/components/data/file_group/file_group.cc b/components/data/file_group/file_group.cc new file mode 100644 index 00000000..93954366 --- /dev/null +++ b/components/data/file_group/file_group.cc @@ -0,0 +1,141 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "components/data/file_group/file_group.h" + +#include "absl/status/statusor.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "public/constants.h" +#include "public/data_loading/filename_utils.h" +#include "src/util/status_macro/status_macros.h" + +namespace kv_server { +namespace { + +constexpr int32_t kFileTypeStrIndex = 0; +constexpr int32_t kLogicalCommitTimeStrIndex = 1; +constexpr int32_t kFileGroupFileIndexStrIndex = 2; +constexpr int32_t kFileGroupSizeStrIndex = 4; +constexpr int32_t kFileGroupFileNameNumComponents = 5; +constexpr int32_t kStandardDeltaOrSnapshotFileGroupSize = 1; + +absl::StatusOr ToInt64(absl::string_view digits) { + if (int64_t result; absl::SimpleAtoi(digits, &result)) { + return result; + } + return absl::InvalidArgumentError( + absl::StrCat("Input ", digits, " is not numeric.")); +} + +} // namespace + +absl::StatusOr FileGroup::ParseMetadataFromFilename( + std::string_view filename) { + Metadata metadata; + std::vector name_parts = + absl::StrSplit(filename, kFileComponentDelimiter); + FileType::Enum file_type; + if (!FileType_Enum_Parse(name_parts[kFileTypeStrIndex], &file_type)) { + return absl::InvalidArgumentError( + absl::StrCat("File type: ", name_parts[kFileTypeStrIndex])); + } + PS_ASSIGN_OR_RETURN(auto logical_commit_time, + ToInt64(name_parts[kLogicalCommitTimeStrIndex])); + metadata.file_type = file_type; + metadata.logical_commit_time = logical_commit_time; + metadata.basename = + absl::StrCat(name_parts[kFileTypeStrIndex], kFileComponentDelimiter, + name_parts[kLogicalCommitTimeStrIndex]); + if (name_parts.size() == kFileGroupFileNameNumComponents) { + PS_ASSIGN_OR_RETURN(auto file_index, + ToInt64(name_parts[kFileGroupFileIndexStrIndex])); + PS_ASSIGN_OR_RETURN(auto file_group_size, + ToInt64(name_parts[kFileGroupSizeStrIndex])); + if (file_index >= file_group_size) { + return absl::InvalidArgumentError( + absl::StrCat(filename, " is invalid. index: ", file_index, + " must be < ", file_group_size)); + } + metadata.group_size = file_group_size; + } else { + metadata.group_size = kStandardDeltaOrSnapshotFileGroupSize; + } + return metadata; +} + +absl::Status FileGroup::ValidateIncomingMetadata( + const Metadata& existing_metadata, const Metadata& incoming_metadata) { + if (existing_metadata.group_size != incoming_metadata.group_size) { + return absl::InvalidArgumentError( + absl::StrCat("Wrong group size: ", incoming_metadata.group_size, + ", group size is: ", existing_metadata.group_size)); + } + if (existing_metadata.logical_commit_time != + incoming_metadata.logical_commit_time) { + return absl::InvalidArgumentError(absl::StrCat( + "Wrong logical commit time: ", incoming_metadata.logical_commit_time, + ", group commit time is: ", existing_metadata.logical_commit_time)); + } + if (existing_metadata.file_type != incoming_metadata.file_type) { + return absl::InvalidArgumentError(absl::StrCat( + "Wrong file type: ", FileType_Enum_Name(incoming_metadata.file_type), + ", group file type is: ", + FileType_Enum_Name(existing_metadata.file_type))); + } + return absl::OkStatus(); +} + +int32_t FileGroup::Size() const { return cached_metadata_.group_size; } + +FileType::Enum FileGroup::Type() const { return cached_metadata_.file_type; } + +std::string_view FileGroup::Basename() const { + return cached_metadata_.basename; +} + +const absl::flat_hash_set& FileGroup::Filenames() const { + return files_set_; +} + +FileGroup::FileStatus FileGroup::GetStatus() const { + return !files_set_.empty() && Size() == files_set_.size() + ? FileStatus::kComplete + : FileStatus::kPending; +} + +absl::Status FileGroup::AddFile(std::string_view filename) { + if (GetStatus() == FileStatus::kComplete || files_set_.contains(filename)) { + return absl::OkStatus(); + } + if (!IsDeltaFilename(filename) && !IsSnapshotFilename(filename) && + !IsFileGroupFileName(filename)) { + return absl::InvalidArgumentError(absl::StrCat( + "File name: ", filename, " is not supported for file groups.")); + } + PS_ASSIGN_OR_RETURN(auto incoming_metadata, + ParseMetadataFromFilename(filename)); + if (cached_metadata_.file_type == FileType::FILE_TYPE_UNSPECIFIED) { + cached_metadata_ = incoming_metadata; + } + PS_RETURN_IF_ERROR( + ValidateIncomingMetadata(cached_metadata_, incoming_metadata)); + files_set_.emplace(filename); + return absl::OkStatus(); +} + +} // namespace kv_server diff --git a/components/data/file_group/file_group.h b/components/data/file_group/file_group.h new file mode 100644 index 00000000..ab6e3ee2 --- /dev/null +++ b/components/data/file_group/file_group.h @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_DATA_FILE_GROUP_FILE_GROUP_H_ +#define COMPONENTS_DATA_FILE_GROUP_FILE_GROUP_H_ + +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "public/base_types.pb.h" + +namespace kv_server { + +// A "file group" is a group of related snapshot/delta files that share a common +// prefix and are treated as a single file for reading purposes by the KV +// server. +// +// Note that this class is not thread safe. +class FileGroup { + public: + enum class FileStatus : int8_t { + // Means some files are not yet added to the group. + kPending = 0, + // Means all files are added to the group. + kComplete, + }; + + // Returns the total number of files in the group (including files that are + // not yet added to the group but are supposed to be). + [[nodiscard]] int32_t Size() const; + + // Returns type of files contained by this group. Currently this can only be + // either `FileType::DELTA` or `FileType::SNAPSHOT`. + [[nodiscard]] FileType::Enum Type() const; + + [[nodiscard]] std::string_view Basename() const; + + // Returns a list of filenames that have been added to the group so far. + [[nodiscard]] const absl::flat_hash_set& Filenames() const; + + // Attempts to add `filename` to this file group. + // Returns: + // - absl::OKStatus: when filename is added successfully or already exists in + // the group. + // - absl::InvalidArgumentError: when `filename` does not belong to this file + // group or is not supported for file groups. + absl::Status AddFile(std::string_view filename); + + [[nodiscard]] FileStatus GetStatus() const; + + private: + struct Metadata { + std::string basename; + int32_t group_size = 0; + int64_t logical_commit_time = -1; + FileType::Enum file_type = FileType::FILE_TYPE_UNSPECIFIED; + }; + + absl::StatusOr ParseMetadataFromFilename(std::string_view filename); + absl::Status ValidateIncomingMetadata(const Metadata& existing_metadata, + const Metadata& incoming_metadata); + + // Cache metadata so we don't have to repeatedly parse one of the existing + // files to get the group's `file_type`, `logical_commit_time` and`group_size` + Metadata cached_metadata_; + absl::flat_hash_set files_set_; +}; + +} // namespace kv_server + +#endif // COMPONENTS_DATA_FILE_GROUP_FILE_GROUP_H_ diff --git a/components/data/file_group/file_group_search_utils.cc b/components/data/file_group/file_group_search_utils.cc new file mode 100644 index 00000000..9a0d2def --- /dev/null +++ b/components/data/file_group/file_group_search_utils.cc @@ -0,0 +1,100 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/data/file_group/file_group_search_utils.h" + +#include +#include + +#include "absl/strings/match.h" +#include "public/data_loading/filename_utils.h" +#include "src/util/status_macro/status_macros.h" + +namespace kv_server { +namespace { + +BlobStorageClient::ListOptions CreateBlobListOptions( + const FileGroupFilter& filter) { + BlobStorageClient::ListOptions options; + if (!filter.start_after_basename.empty()) { + options.start_after = filter.start_after_basename; + } + if (filter.file_type.has_value()) { + options.prefix = FileType::Enum_Name(*filter.file_type); + } + return options; +} + +// Assumes that filenames are lexicographically ordered. +absl::StatusOr> CreateFileGroupsFromBlobNames( + const std::vector& filenames) { + std::vector file_groups; + if (filenames.empty()) { + return file_groups; + } + FileGroup current_group; + PS_RETURN_IF_ERROR(current_group.AddFile(filenames[0])); + for (int32_t file_index = 1; file_index < filenames.size(); file_index++) { + const auto& filename = filenames[file_index]; + if (!absl::StartsWith(filename, current_group.Basename())) { + file_groups.push_back(current_group); + current_group = FileGroup(); + } + PS_RETURN_IF_ERROR(current_group.AddFile(filename)); + } + file_groups.push_back(current_group); + return file_groups; +} + +} // namespace + +absl::StatusOr> FindMostRecentFileGroup( + const BlobStorageClient::DataLocation& location, + const FileGroupFilter& filter, BlobStorageClient& blob_client) { + PS_ASSIGN_OR_RETURN(auto file_groups, + FindFileGroups(location, filter, blob_client)); + if (!file_groups.empty()) { + return file_groups.back(); + } + return std::nullopt; +} + +absl::StatusOr> FindFileGroups( + const BlobStorageClient::DataLocation& location, + const FileGroupFilter& filter, BlobStorageClient& blob_client) { + PS_ASSIGN_OR_RETURN( + auto blob_names, + blob_client.ListBlobs(location, CreateBlobListOptions(filter))); + std::vector supported_blob_names; + std::copy_if( + blob_names.begin(), blob_names.end(), + std::back_inserter(supported_blob_names), [](std::string_view blob_name) { + return IsDeltaFilename(blob_name) || IsSnapshotFilename(blob_name) || + IsFileGroupFileName(blob_name); + }); + PS_ASSIGN_OR_RETURN(auto file_groups, + CreateFileGroupsFromBlobNames(supported_blob_names)); + if (!filter.status.has_value()) { + return file_groups; + } + std::vector result; + std::copy_if(file_groups.begin(), file_groups.end(), + std::back_inserter(result), + [&filter](const FileGroup& file_group) { + return *filter.status == file_group.GetStatus(); + }); + return result; +} + +} // namespace kv_server diff --git a/components/data/file_group/file_group_search_utils.h b/components/data/file_group/file_group_search_utils.h new file mode 100644 index 00000000..9fedbd99 --- /dev/null +++ b/components/data/file_group/file_group_search_utils.h @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_DATA_FILE_GROUP_FILE_GROUP_SEARCH_UTILS_H_ +#define COMPONENTS_DATA_FILE_GROUP_FILE_GROUP_SEARCH_UTILS_H_ + +#include +#include +#include + +#include "absl/status/statusor.h" +#include "components/data/blob_storage/blob_storage_client.h" +#include "components/data/file_group/file_group.h" +#include "public/base_types.pb.h" + +namespace kv_server { + +struct FileGroupFilter { + // Only return file groups with a basename more recent than this one. + // Return all file groups when `start_after_basename.empty()`. + std::string start_after_basename; + + // Only return file groups of this type. + // Return all file groups when `!type.has_value()`. + std::optional file_type; + + // Only consider file groups with this upload status. + // Return all file groups when `!upload_status.has_value()`. + std::optional status; +}; + +// Finds the most recent file group in blob storage that matches the `filter` +// criteria. +// +// Returns a descriptive error on failure. +absl::StatusOr> FindMostRecentFileGroup( + const BlobStorageClient::DataLocation& location, + const FileGroupFilter& filter, BlobStorageClient& blob_client); + +// Finds all file groups in blob storage that meet the `filter` criteria. +// Returned file groups are lexicographically ordered by basename. +// +// Returns (when status is ok): +// - file group: when file group matching `filer` is found. +// - std::nullopt: when file group matching `filter` is not found. +// Returns a descriptive error on failure. +absl::StatusOr> FindFileGroups( + const BlobStorageClient::DataLocation& location, + const FileGroupFilter& filter, BlobStorageClient& blob_client); + +} // namespace kv_server + +#endif // COMPONENTS_DATA_FILE_GROUP_FILE_GROUP_SEARCH_UTILS_H_ diff --git a/components/data/file_group/file_group_search_utils_test.cc b/components/data/file_group/file_group_search_utils_test.cc new file mode 100644 index 00000000..2294c7f9 --- /dev/null +++ b/components/data/file_group/file_group_search_utils_test.cc @@ -0,0 +1,152 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/data/file_group/file_group_search_utils.h" + +#include "components/data/blob_storage/blob_storage_client.h" +#include "components/data/common/mocks.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace kv_server { +namespace { + +using testing::_; +using testing::AllOf; +using testing::Field; +using testing::Return; +using testing::UnorderedElementsAre; + +TEST(FileGroupSearchUtilsTest, ValidateFindingFileGroupsBlobClientError) { + BlobStorageClient::DataLocation location{.bucket = "bucket"}; + MockBlobStorageClient blob_client; + EXPECT_CALL(blob_client, ListBlobs(_, _)) + .Times(2) + .WillRepeatedly( + Return(absl::InvalidArgumentError("bucket does not exist."))); + auto file_groups = FindFileGroups(location, FileGroupFilter{}, blob_client); + EXPECT_FALSE(file_groups.ok()) << file_groups.status(); + EXPECT_EQ(file_groups.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_EQ(file_groups.status().message(), "bucket does not exist."); + auto file_group = + FindMostRecentFileGroup(location, FileGroupFilter{}, blob_client); + EXPECT_FALSE(file_group.ok()) << file_group.status(); + EXPECT_EQ(file_group.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_EQ(file_group.status().message(), "bucket does not exist."); +} + +TEST(FileGroupSearchUtilsTest, + ValidateFindingFileGroupsBlobClientEmptyResponse) { + FileGroupFilter filter{.file_type = FileType::SNAPSHOT}; + BlobStorageClient::DataLocation location{.bucket = "bucket"}; + MockBlobStorageClient blob_client; + EXPECT_CALL( + blob_client, + ListBlobs(Field(&BlobStorageClient::DataLocation::bucket, "bucket"), + Field(&BlobStorageClient::ListOptions::prefix, "SNAPSHOT"))) + .Times(2) + .WillRepeatedly(Return(std::vector{})); + auto file_group = FindMostRecentFileGroup(location, filter, blob_client); + EXPECT_TRUE(file_group.ok()) << file_group.status(); + EXPECT_FALSE(file_group->has_value()); // No file group found. + auto file_groups = FindFileGroups(location, filter, blob_client); + EXPECT_TRUE(file_groups.ok()) << file_groups.status(); + EXPECT_TRUE(file_groups->empty()); +} + +TEST(FileGroupSearchUtilsTest, ValidateFindingSnapshotFileGroups) { + MockBlobStorageClient blob_client; + EXPECT_CALL( + blob_client, + ListBlobs(Field(&BlobStorageClient::DataLocation::bucket, "bucket"), + Field(&BlobStorageClient::ListOptions::prefix, "SNAPSHOT"))) + .WillOnce(Return(std::vector{ + "SNAPSHOT_1705430864435450_00000_OF_000003", + "SNAPSHOT_1705430864435450_00001_OF_000003", + "SNAPSHOT_1705430864435450_00002_OF_000003", + "SNAPSHOT_1705430864435451_00000_OF_000002"})); + auto file_groups = FindFileGroups( + BlobStorageClient::DataLocation{.bucket = "bucket"}, + FileGroupFilter{.file_type = FileType::SNAPSHOT}, blob_client); + EXPECT_TRUE(file_groups.ok()) << file_groups.status(); + ASSERT_EQ(file_groups->size(), 2); + // Verify first file group + EXPECT_EQ((*file_groups)[0].Size(), 3); + EXPECT_EQ((*file_groups)[0].Basename(), "SNAPSHOT_1705430864435450"); + EXPECT_EQ((*file_groups)[0].GetStatus(), FileGroup::FileStatus::kComplete); + EXPECT_EQ((*file_groups)[0].Type(), FileType::SNAPSHOT); + EXPECT_THAT( + (*file_groups)[0].Filenames(), + UnorderedElementsAre("SNAPSHOT_1705430864435450_00000_OF_000003", + "SNAPSHOT_1705430864435450_00001_OF_000003", + "SNAPSHOT_1705430864435450_00002_OF_000003")); + // Verify second file group + EXPECT_EQ((*file_groups)[1].Size(), 2); + EXPECT_EQ((*file_groups)[1].Basename(), "SNAPSHOT_1705430864435451"); + EXPECT_EQ((*file_groups)[1].GetStatus(), FileGroup::FileStatus::kPending); + EXPECT_EQ((*file_groups)[1].Type(), FileType::SNAPSHOT); + EXPECT_THAT( + (*file_groups)[1].Filenames(), + UnorderedElementsAre("SNAPSHOT_1705430864435451_00000_OF_000002")); +} + +TEST(FileGroupSearchUtilsTest, ValidateFindingMostRecentSnapshotFileGroups) { + MockBlobStorageClient blob_client; + EXPECT_CALL( + blob_client, + ListBlobs( + Field(&BlobStorageClient::DataLocation::bucket, "bucket"), + AllOf(Field(&BlobStorageClient::ListOptions::prefix, "SNAPSHOT"), + Field(&BlobStorageClient::ListOptions::start_after, + "start_after")))) + .Times(2) + .WillRepeatedly(Return(std::vector{ + "SNAPSHOT_1705430864435450_00000_OF_000003", + "SNAPSHOT_1705430864435450_00001_OF_000003", + "SNAPSHOT_1705430864435450_00002_OF_000003", + "SNAPSHOT_1705430864435451_00000_OF_000002"})); + auto file_group = FindMostRecentFileGroup( + BlobStorageClient::DataLocation{.bucket = "bucket"}, + FileGroupFilter{.start_after_basename = "start_after", + .file_type = FileType::SNAPSHOT, + .status = FileGroup::FileStatus::kComplete}, + blob_client); + ASSERT_TRUE(file_group.ok()) << file_group.status(); + EXPECT_EQ((*file_group)->Size(), 3); + EXPECT_EQ((*file_group)->Basename(), "SNAPSHOT_1705430864435450"); + EXPECT_EQ((*file_group)->GetStatus(), FileGroup::FileStatus::kComplete); + EXPECT_EQ((*file_group)->Type(), FileType::SNAPSHOT); + EXPECT_THAT( + (*file_group)->Filenames(), + UnorderedElementsAre("SNAPSHOT_1705430864435450_00000_OF_000003", + "SNAPSHOT_1705430864435450_00001_OF_000003", + "SNAPSHOT_1705430864435450_00002_OF_000003")); + file_group = FindMostRecentFileGroup( + BlobStorageClient::DataLocation{.bucket = "bucket"}, + FileGroupFilter{.start_after_basename = "start_after", + .file_type = FileType::SNAPSHOT, + .status = FileGroup::FileStatus::kPending}, + blob_client); + ASSERT_TRUE(file_group.ok()) << file_group.status(); + EXPECT_EQ((*file_group)->Size(), 2); + EXPECT_EQ((*file_group)->Basename(), "SNAPSHOT_1705430864435451"); + EXPECT_EQ((*file_group)->GetStatus(), FileGroup::FileStatus::kPending); + EXPECT_EQ((*file_group)->Type(), FileType::SNAPSHOT); + EXPECT_THAT( + (*file_group)->Filenames(), + UnorderedElementsAre("SNAPSHOT_1705430864435451_00000_OF_000002")); +} + +} // namespace +} // namespace kv_server diff --git a/components/data/file_group/file_group_test.cc b/components/data/file_group/file_group_test.cc new file mode 100644 index 00000000..2d37566b --- /dev/null +++ b/components/data/file_group/file_group_test.cc @@ -0,0 +1,122 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/data/file_group/file_group.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace kv_server { +namespace { + +TEST(FileGroupTest, ValidateDefaultFileGroup) { + FileGroup file_group; + EXPECT_EQ(file_group.Type(), FileType::FILE_TYPE_UNSPECIFIED); + EXPECT_EQ(file_group.Size(), 0); + EXPECT_EQ(file_group.GetStatus(), FileGroup::FileStatus::kPending); + EXPECT_EQ(file_group.Basename(), ""); + EXPECT_TRUE(file_group.Filenames().empty()); +} + +TEST(FileGroupTest, ValidateAddingFilesUntilGroupIsComplete) { + auto basename = "SNAPSHOT_1705430864435450"; + auto filename0 = "SNAPSHOT_1705430864435450_00000_OF_000002"; + FileGroup file_group; + auto status = file_group.AddFile(filename0); + EXPECT_TRUE(status.ok()) << status; + EXPECT_EQ(file_group.Basename(), basename); + EXPECT_EQ(file_group.Size(), 2); + EXPECT_EQ(file_group.Type(), FileType::SNAPSHOT); + EXPECT_EQ(file_group.GetStatus(), FileGroup::FileStatus::kPending); + EXPECT_THAT(file_group.Filenames(), testing::UnorderedElementsAre(filename0)); + // Add the remaining file + auto filename1 = "SNAPSHOT_1705430864435450_00001_OF_000002"; + status = file_group.AddFile(filename1); + EXPECT_TRUE(status.ok()) << status; + EXPECT_EQ(file_group.Basename(), basename); + EXPECT_EQ(file_group.Size(), 2); + EXPECT_EQ(file_group.Type(), FileType::SNAPSHOT); + EXPECT_EQ(file_group.GetStatus(), FileGroup::FileStatus::kComplete); + EXPECT_THAT(file_group.Filenames(), + testing::UnorderedElementsAre(filename0, filename1)); +} + +TEST(FileGroupTest, ValidateAddingSingleDeltaFile) { + auto delta_file = "DELTA_1705430864435450"; + FileGroup file_group; + auto status = file_group.AddFile(delta_file); + EXPECT_TRUE(status.ok()) << status; + EXPECT_EQ(file_group.Basename(), delta_file); + EXPECT_EQ(file_group.Size(), 1); + EXPECT_EQ(file_group.Type(), FileType::DELTA); + EXPECT_EQ(file_group.GetStatus(), FileGroup::FileStatus::kComplete); + EXPECT_THAT(file_group.Filenames(), + testing::UnorderedElementsAre(delta_file)); +} + +TEST(FileGroupTest, ValidateAddingSingleSnapshotFile) { + auto snapshot_file = "SNAPSHOT_1705430864435450"; + FileGroup file_group; + auto status = file_group.AddFile(snapshot_file); + EXPECT_TRUE(status.ok()) << status; + EXPECT_EQ(file_group.Basename(), snapshot_file); + EXPECT_EQ(file_group.Size(), 1); + EXPECT_EQ(file_group.Type(), FileType::SNAPSHOT); + EXPECT_EQ(file_group.GetStatus(), FileGroup::FileStatus::kComplete); + EXPECT_THAT(file_group.Filenames(), + testing::UnorderedElementsAre(snapshot_file)); +} + +TEST(FileGroupTest, ValidateAddingInvalidFilenames) { + FileGroup file_group; + auto status = file_group.AddFile(""); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + status = file_group.AddFile("UNKNOWN_1705430864435450_00000_OF_000010"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + status = file_group.AddFile("SNAPSHOT_1705430864435450_00000"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + status = file_group.AddFile("SNAPSHOT_1705430864435450_00000_OF"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + status = file_group.AddFile("DELTA_1705430864435450_OF_000010"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + status = file_group.AddFile("SNAPSHOT_1705430864435450_00010_OF_000010"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); +} + +TEST(FileGroupTest, ValidateAddingFilesWithMismatchingMetadata) { + FileGroup file_group; + auto status = file_group.AddFile("SNAPSHOT_1705430864435450_00000_OF_000010"); + EXPECT_TRUE(status.ok()) << status; + // check logical commit time mismatch + status = file_group.AddFile("SNAPSHOT_1705430000000000_00000_OF_000010"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + // check file type mismatch + status = file_group.AddFile("DELTA_1705430864435450_00001_OF_000010"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); + // check group size mismatch + status = file_group.AddFile("SNAPSHOT_1705430864435450_00001_OF_000020"); + EXPECT_FALSE(status.ok()) << status; + EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument); +} + +} // namespace +} // namespace kv_server diff --git a/components/data/realtime/BUILD.bazel b/components/data/realtime/BUILD.bazel index 5c8f5e34..f8dc66c1 100644 --- a/components/data/realtime/BUILD.bazel +++ b/components/data/realtime/BUILD.bazel @@ -40,7 +40,7 @@ cc_library( "//components/data/common:change_notifier", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/telemetry", + "@google_privacysandbox_servers_common//src/telemetry", ], ) @@ -64,10 +64,9 @@ cc_test( ":delta_file_record_change_notifier", "//components/data/common:change_notifier", "//components/data/common:mocks", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -95,11 +94,11 @@ cc_library( "//components/errors:retry", "//components/util:sleepfor", "//public:constants", - "@google_privacysandbox_servers_common//src/cpp/util:duration", + "@google_privacysandbox_servers_common//src/util:duration", ], }) + [ "//components/data/common:thread_manager", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", ], @@ -130,7 +129,6 @@ cc_test( "//public/data_loading:filename_utils", "@com_github_grpc_grpc//:grpc++", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -151,8 +149,6 @@ cc_library( ":realtime_notifier", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -180,6 +176,5 @@ cc_test( "//components/util:sleepfor_mock", "@com_github_grpc_grpc//:grpc++", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) diff --git a/components/data/realtime/delta_file_record_change_notifier.h b/components/data/realtime/delta_file_record_change_notifier.h index 6fd66c6f..d48cda20 100644 --- a/components/data/realtime/delta_file_record_change_notifier.h +++ b/components/data/realtime/delta_file_record_change_notifier.h @@ -26,7 +26,7 @@ #include "absl/status/statusor.h" #include "absl/time/time.h" #include "components/data/common/change_notifier.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/telemetry/telemetry.h" namespace kv_server { diff --git a/components/data/realtime/delta_file_record_change_notifier_aws.cc b/components/data/realtime/delta_file_record_change_notifier_aws.cc index 9bc72b14..5d0cb768 100644 --- a/components/data/realtime/delta_file_record_change_notifier_aws.cc +++ b/components/data/realtime/delta_file_record_change_notifier_aws.cc @@ -14,6 +14,7 @@ #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/escaping.h" @@ -21,8 +22,7 @@ #include "components/data/common/change_notifier.h" #include "components/data/realtime/delta_file_record_change_notifier.h" #include "components/telemetry/server_definition.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/telemetry/telemetry.h" namespace kv_server { namespace { @@ -65,12 +65,7 @@ class AwsDeltaFileRecordChangeNotifier : public DeltaFileRecordChangeNotifier { if (!parsedMessage.ok()) { LOG(ERROR) << "Failed to parse JSON: " << message << ", error: " << parsedMessage.status(); - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kDeltaFileRecordChangeNotifierParsingFailure), - 1}})); + LogServerErrorMetric(kDeltaFileRecordChangeNotifierParsingFailure); continue; } nc.realtime_messages.push_back(RealtimeMessage{ diff --git a/components/data/realtime/delta_file_record_change_notifier_aws_test.cc b/components/data/realtime/delta_file_record_change_notifier_aws_test.cc index 131e0444..e2f91460 100644 --- a/components/data/realtime/delta_file_record_change_notifier_aws_test.cc +++ b/components/data/realtime/delta_file_record_change_notifier_aws_test.cc @@ -25,7 +25,6 @@ #include "components/data/common/mocks.h" #include "components/data/realtime/delta_file_record_change_notifier.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" using testing::_; diff --git a/components/data/realtime/delta_file_record_change_notifier_local.cc b/components/data/realtime/delta_file_record_change_notifier_local.cc index a7ad5906..f444cdf3 100644 --- a/components/data/realtime/delta_file_record_change_notifier_local.cc +++ b/components/data/realtime/delta_file_record_change_notifier_local.cc @@ -14,11 +14,11 @@ #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "components/data/common/change_notifier.h" #include "components/data/realtime/delta_file_record_change_notifier.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/components/data/realtime/delta_file_record_change_notifier_local_test.cc b/components/data/realtime/delta_file_record_change_notifier_local_test.cc index 6a33768c..ef3589e3 100644 --- a/components/data/realtime/delta_file_record_change_notifier_local_test.cc +++ b/components/data/realtime/delta_file_record_change_notifier_local_test.cc @@ -23,7 +23,6 @@ #include "components/data/common/mocks.h" #include "components/data/realtime/delta_file_record_change_notifier.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data/realtime/realtime_notifier_aws.cc b/components/data/realtime/realtime_notifier_aws.cc index 19b5f917..56f4cc6d 100644 --- a/components/data/realtime/realtime_notifier_aws.cc +++ b/components/data/realtime/realtime_notifier_aws.cc @@ -18,15 +18,15 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "components/data/common/thread_manager.h" #include "components/data/realtime/delta_file_record_change_notifier.h" #include "components/data/realtime/realtime_notifier.h" #include "components/errors/retry.h" -#include "glog/logging.h" #include "public/constants.h" -#include "src/cpp/telemetry/telemetry.h" -#include "src/cpp/util/duration.h" +#include "src/telemetry/telemetry.h" +#include "src/util/duration.h" namespace kv_server { namespace { @@ -36,7 +36,7 @@ class RealtimeNotifierImpl : public RealtimeNotifier { explicit RealtimeNotifierImpl( std::unique_ptr sleep_for, std::unique_ptr change_notifier) - : thread_manager_(TheadManager::Create("Realtime notifier")), + : thread_manager_(ThreadManager::Create("Realtime notifier")), sleep_for_(std::move(sleep_for)), change_notifier_(std::move(change_notifier)) {} @@ -82,48 +82,50 @@ class RealtimeNotifierImpl : public RealtimeNotifier { ExponentialBackoffForRetry(sequential_failures); LOG(ERROR) << "Failed to get realtime notifications: " << updates.status() << ". Waiting for " << backoff_time; - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kRealtimeGetNotificationsFailure), 1}})); + LogServerErrorMetric(kRealtimeGetNotificationsFailure); if (!sleep_for_->Duration(backoff_time)) { LOG(ERROR) << "Failed to sleep for " << backoff_time << ". SleepFor invalid."; - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - {{std::string(kRealtimeSleepFailure), 1}})); + LogServerErrorMetric(kRealtimeSleepFailure); } continue; } sequential_failures = 0; for (const auto& realtime_message : updates->realtime_messages) { - auto count = callback(realtime_message.parsed_notification); - if (count.ok()) { - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - static_cast(count->total_updated_records + - count->total_deleted_records))); + if (auto count = callback(realtime_message.parsed_notification); + !count.ok()) { + LOG(ERROR) << "Data loading callback failed: " << count.status(); + LogServerErrorMetric(kRealtimeMessageApplicationFailure); + } + auto e2e_cloud_provided_latency = absl::ToDoubleMicroseconds( + absl::Now() - realtime_message.notifications_sns_inserted); + // we're getting this value based on two different clocks. Opentelemetry + // does not allow negative values for histograms. However, not logging + // this will affect the pvalues, so the next best thing is set it to 0. + if (e2e_cloud_provided_latency < 0) { + e2e_cloud_provided_latency = 0; } LogIfError( KVServerContextMap() ->SafeMetric() .LogHistogram( - absl::ToDoubleMicroseconds( - absl::Now() - - (realtime_message.notifications_sns_inserted)))); + e2e_cloud_provided_latency)); if (realtime_message.notifications_inserted) { - auto e2eDuration = - absl::Now() - (realtime_message.notifications_inserted).value(); + // we're getting this value based on two different clocks. + // Opentelemetry does not allow negative values for histograms. + // However, not logging this will affect the pvalues, so the next best + // thing is set it to 0. + auto e2e_latency = absl::ToDoubleMicroseconds( + absl::Now() - realtime_message.notifications_inserted.value()); + if (e2e_latency < 0) { + e2e_latency = 0; + } LogIfError(KVServerContextMap() ->SafeMetric() .LogHistogram( - absl::ToDoubleMicroseconds(e2eDuration))); + e2e_latency)); } } LogIfError(KVServerContextMap() @@ -138,7 +140,7 @@ class RealtimeNotifierImpl : public RealtimeNotifier { } } - std::unique_ptr thread_manager_; + std::unique_ptr thread_manager_; std::unique_ptr sleep_for_; std::unique_ptr change_notifier_; }; diff --git a/components/data/realtime/realtime_notifier_aws_test.cc b/components/data/realtime/realtime_notifier_aws_test.cc index 54a12c5e..238acdc1 100644 --- a/components/data/realtime/realtime_notifier_aws_test.cc +++ b/components/data/realtime/realtime_notifier_aws_test.cc @@ -24,7 +24,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "public/data_loading/filename_utils.h" -#include "src/cpp/telemetry/mocks.h" using testing::_; using testing::Field; diff --git a/components/data/realtime/realtime_notifier_gcp.cc b/components/data/realtime/realtime_notifier_gcp.cc index d76c2ea8..073ed9a4 100644 --- a/components/data/realtime/realtime_notifier_gcp.cc +++ b/components/data/realtime/realtime_notifier_gcp.cc @@ -14,15 +14,15 @@ #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/strings/escaping.h" #include "components/data/common/msg_svc.h" #include "components/data/common/thread_manager.h" #include "components/data/realtime/realtime_notifier.h" -#include "glog/logging.h" #include "google/cloud/pubsub/message.h" #include "google/cloud/pubsub/subscriber.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/telemetry/telemetry.h" namespace kv_server { namespace { @@ -37,7 +37,7 @@ class RealtimeNotifierGcp : public RealtimeNotifier { public: explicit RealtimeNotifierGcp(std::unique_ptr gcp_subscriber, std::unique_ptr sleep_for) - : thread_manager_(TheadManager::Create("Realtime notifier")), + : thread_manager_(ThreadManager::Create("Realtime notifier")), sleep_for_(std::move(sleep_for)), gcp_subscriber_(std::move(gcp_subscriber)) {} @@ -113,24 +113,14 @@ class RealtimeNotifierGcp : public RealtimeNotifier { auto start = absl::Now(); std::string string_decoded; if (!absl::Base64Unescape(m.data(), &string_decoded)) { - LogIfError( - KVServerContextMap()->SafeMetric().LogUpDownCounter( - {{std::string(kRealtimeDecodeMessageFailure), 1}})); + LogServerErrorMetric(kRealtimeDecodeMessageFailure); LOG(ERROR) << "The body of the message is not a base64 encoded string."; std::move(h).ack(); return; } - auto count = callback(string_decoded); - if (count.ok()) { - LogIfError(KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - static_cast(count->total_updated_records + - count->total_deleted_records))); - } else { - LogIfError( - KVServerContextMap()->SafeMetric().LogUpDownCounter( - {{std::string(kRealtimeMessageApplicationFailure), 1}})); + if (auto count = callback(string_decoded); !count.ok()) { + LOG(ERROR) << "Data loading callback failed: " << count.status(); + LogServerErrorMetric(kRealtimeMessageApplicationFailure); } RecordGcpSuppliedE2ELatency(m); RecordProducerSuppliedE2ELatency(m); @@ -156,7 +146,7 @@ class RealtimeNotifierGcp : public RealtimeNotifier { LOG(INFO) << "Realtime updater stopped watching."; } - std::unique_ptr thread_manager_; + std::unique_ptr thread_manager_; mutable absl::Mutex mutex_; future session_ ABSL_GUARDED_BY(mutex_); std::unique_ptr sleep_for_; diff --git a/components/data/realtime/realtime_notifier_gcp_test.cc b/components/data/realtime/realtime_notifier_gcp_test.cc index 31d731c1..fa6e5603 100644 --- a/components/data/realtime/realtime_notifier_gcp_test.cc +++ b/components/data/realtime/realtime_notifier_gcp_test.cc @@ -29,7 +29,6 @@ #include "google/cloud/pubsub/subscriber.h" #include "gtest/gtest.h" #include "public/data_loading/filename_utils.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data/realtime/realtime_thread_pool_manager.h b/components/data/realtime/realtime_thread_pool_manager.h index 8c1540e8..07371c0e 100644 --- a/components/data/realtime/realtime_thread_pool_manager.h +++ b/components/data/realtime/realtime_thread_pool_manager.h @@ -25,7 +25,7 @@ #include "absl/status/statusor.h" #include "components/data/common/notifier_metadata.h" #include "components/data/realtime/realtime_notifier.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/telemetry/telemetry.h" namespace kv_server { diff --git a/components/data/realtime/realtime_thread_pool_manager_aws_test.cc b/components/data/realtime/realtime_thread_pool_manager_aws_test.cc index 4fa49b1f..3ee12dc2 100644 --- a/components/data/realtime/realtime_thread_pool_manager_aws_test.cc +++ b/components/data/realtime/realtime_thread_pool_manager_aws_test.cc @@ -24,7 +24,6 @@ #include "components/util/sleepfor_mock.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data/realtime/realtime_thread_pool_manager_gcp_test.cc b/components/data/realtime/realtime_thread_pool_manager_gcp_test.cc index 584083b9..b56e4282 100644 --- a/components/data/realtime/realtime_thread_pool_manager_gcp_test.cc +++ b/components/data/realtime/realtime_thread_pool_manager_gcp_test.cc @@ -25,7 +25,6 @@ #include "google/cloud/pubsub/mocks/mock_subscriber_connection.h" #include "google/cloud/pubsub/subscriber.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/data_server/cache/BUILD.bazel b/components/data_server/cache/BUILD.bazel index 06112b56..d6eaecc1 100644 --- a/components/data_server/cache/BUILD.bazel +++ b/components/data_server/cache/BUILD.bazel @@ -40,6 +40,7 @@ cc_library( ], deps = [ ":get_key_value_set_result_impl", + "//components/util:request_context", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", ], @@ -57,12 +58,11 @@ cc_library( ":cache", ":get_key_value_set_result_impl", "//public:base_types_cc_proto", - "@com_github_google_glog//:glog", "@com_google_absl//absl/base", "@com_google_absl//absl/container:btree", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", "@com_google_absl//absl/synchronization", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -80,8 +80,7 @@ cc_test( "@com_google_absl//absl/container:flat_hash_map", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) diff --git a/components/data_server/cache/cache.h b/components/data_server/cache/cache.h index 1136e1f0..4341cff7 100644 --- a/components/data_server/cache/cache.h +++ b/components/data_server/cache/cache.h @@ -27,6 +27,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "components/data_server/cache/get_key_value_set_result.h" +#include "components/util/request_context.h" namespace kv_server { @@ -38,35 +39,42 @@ class Cache { // Looks up and returns key-value pairs for the given keys. virtual absl::flat_hash_map GetKeyValuePairs( + const RequestContext& request_context, const absl::flat_hash_set& key_list) const = 0; // Looks up and returns key-value set result for the given key set. virtual std::unique_ptr GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const = 0; - // Inserts or updates the key with the new value. + // Inserts or updates the key with the new value for a given prefix virtual void UpdateKeyValue(std::string_view key, std::string_view value, - int64_t logical_commit_time) = 0; + int64_t logical_commit_time, + std::string_view prefix = "") = 0; - // Inserts or updates values in the set for a given key, if a value exists, - // updates its timestamp to the latest logical commit time. + // Inserts or updates values in the set for a given key and prefix, if a value + // exists, updates its timestamp to the latest logical commit time. virtual void UpdateKeyValueSet(std::string_view key, absl::Span value_set, - int64_t logical_commit_time) = 0; + int64_t logical_commit_time, + std::string_view prefix = "") = 0; - // Deletes a particular (key, value) pair. - virtual void DeleteKey(std::string_view key, int64_t logical_commit_time) = 0; + // Deletes a particular (key, value) pair for a given prefix. + virtual void DeleteKey(std::string_view key, int64_t logical_commit_time, + std::string_view prefix = "") = 0; - // Deletes values in the set for a given key. The deletion, this object - // still exist and is marked "deleted", in case there are - // late-arriving updates to this value. + // Deletes values in the set for a given key and prefix. The deletion, this + // object still exist and is marked "deleted", in case there are late-arriving + // updates to this value. virtual void DeleteValuesInSet(std::string_view key, absl::Span value_set, - int64_t logical_commit_time) = 0; + int64_t logical_commit_time, + std::string_view prefix = "") = 0; // Removes the values that were deleted before the specified - // logical_commit_time. - virtual void RemoveDeletedKeys(int64_t logical_commit_time) = 0; + // logical_commit_time for a given prefix. + virtual void RemoveDeletedKeys(int64_t logical_commit_time, + std::string_view prefix = "") = 0; }; } // namespace kv_server diff --git a/components/data_server/cache/key_value_cache.cc b/components/data_server/cache/key_value_cache.cc index 3d4866ff..f984de36 100644 --- a/components/data_server/cache/key_value_cache.cc +++ b/components/data_server/cache/key_value_cache.cc @@ -19,32 +19,20 @@ #include #include "absl/container/flat_hash_map.h" +#include "absl/log/log.h" #include "absl/memory/memory.h" #include "absl/synchronization/mutex.h" #include "components/data_server/cache/cache.h" #include "components/data_server/cache/get_key_value_set_result.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::ScopeLatencyRecorder; - -constexpr char kGetKeyValuePairsEvent[] = "GetKeyValuePairs"; -constexpr char kGetKeyValueSetEvent[] = "GetKeyValueSet"; -constexpr char kUpdateKeyValueEvent[] = "UpdateKeyValue"; -constexpr char kUpdateKeyValueSetEvent[] = "UpdateKeyValueSet"; -constexpr char kDeleteKeyEvent[] = "DeleteKey"; -constexpr char kDeleteValuesInSetEvent[] = "DeleteValuesInSet"; -constexpr char kRemoveDeletedKeysEvent[] = "RemoveDeletedKeys"; -constexpr char kCleanUpKeyValueMapEvent[] = "CleanUpKeyValueMap"; -constexpr char kCleanUpKeyValueSetMapEvent[] = "CleanUpKeyValueSetMap"; - absl::flat_hash_map KeyValueCache::GetKeyValuePairs( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const { - ScopeLatencyRecorder latency_recorder(kGetKeyValuePairsEvent, - metrics_recorder_); + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetInternalLookupMetricsContext()); absl::flat_hash_map kv_pairs; absl::ReaderMutexLock lock(&mutex_); for (std::string_view key : key_set) { @@ -57,16 +45,24 @@ absl::flat_hash_map KeyValueCache::GetKeyValuePairs( kv_pairs.insert_or_assign(key, *(key_iter->second.value)); } } + if (kv_pairs.empty()) { + LogCacheAccessMetrics(request_context, kKeyValueCacheMiss); + } else { + LogCacheAccessMetrics(request_context, kKeyValueCacheHit); + } return kv_pairs; } std::unique_ptr KeyValueCache::GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const { - ScopeLatencyRecorder latency_recorder(kGetKeyValueSetEvent, - metrics_recorder_); + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetInternalLookupMetricsContext()); // lock the cache map absl::ReaderMutexLock lock(&set_map_mutex_); auto result = GetKeyValueSetResult::Create(); + bool cache_hit = false; for (const auto& key : key_set) { VLOG(8) << "Getting key: " << key; const auto key_itr = key_to_value_set_map_.find(key); @@ -81,25 +77,35 @@ std::unique_ptr KeyValueCache::GetKeyValueSet( } // Add key value set to the result result->AddKeyValueSet(key, std::move(value_set), std::move(set_lock)); + cache_hit = true; } } + if (cache_hit) { + LogCacheAccessMetrics(request_context, kKeyValueSetCacheHit); + } else { + LogCacheAccessMetrics(request_context, kKeyValueSetCacheMiss); + } return result; } // Replaces the current key-value entry with the new key-value entry. void KeyValueCache::UpdateKeyValue(std::string_view key, std::string_view value, - int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kUpdateKeyValueEvent, - metrics_recorder_); + int64_t logical_commit_time, + std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); VLOG(9) << "Received update for [" << key << "] at " << logical_commit_time << ". value will be set to: " << value; absl::MutexLock lock(&mutex_); - if (logical_commit_time <= max_cleanup_logical_commit_time_) { + auto max_cleanup_logical_commit_time = + max_cleanup_logical_commit_time_map_[prefix]; + + if (logical_commit_time <= max_cleanup_logical_commit_time) { VLOG(1) << "Skipping the update as its logical_commit_time: " << logical_commit_time << " is not newer than the current cutoff time:" - << max_cleanup_logical_commit_time_; + << max_cleanup_logical_commit_time; return; } @@ -119,10 +125,15 @@ void KeyValueCache::UpdateKeyValue(std::string_view key, std::string_view value, key_iter->second.last_logical_commit_time < logical_commit_time && key_iter->second.value == nullptr) { // should always have this, but checking just in case - auto dl_key_iter = - deleted_nodes_.find(key_iter->second.last_logical_commit_time); - if (dl_key_iter != deleted_nodes_.end() && dl_key_iter->second == key) { - deleted_nodes_.erase(dl_key_iter); + + if (auto prefix_deleted_nodes_iter = deleted_nodes_map_.find(prefix); + prefix_deleted_nodes_iter != deleted_nodes_map_.end()) { + auto dl_key_iter = prefix_deleted_nodes_iter->second.find( + key_iter->second.last_logical_commit_time); + if (dl_key_iter != prefix_deleted_nodes_iter->second.end() && + dl_key_iter->second == key) { + prefix_deleted_nodes_iter->second.erase(dl_key_iter); + } } } @@ -132,9 +143,10 @@ void KeyValueCache::UpdateKeyValue(std::string_view key, std::string_view value, void KeyValueCache::UpdateKeyValueSet( std::string_view key, absl::Span input_value_set, - int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kUpdateKeyValueSetEvent, - metrics_recorder_); + int64_t logical_commit_time, std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); VLOG(9) << "Received update for [" << key << "] at " << logical_commit_time; std::unique_ptr key_lock; absl::flat_hash_map* existing_value_set; @@ -142,11 +154,14 @@ void KeyValueCache::UpdateKeyValueSet( { absl::MutexLock lock_map(&set_map_mutex_); - if (logical_commit_time <= max_cleanup_logical_commit_time_for_set_cache_) { + auto max_cleanup_logical_commit_time = + max_cleanup_logical_commit_time_map_for_set_cache_[prefix]; + + if (logical_commit_time <= max_cleanup_logical_commit_time) { VLOG(1) << "Skipping the update as its logical_commit_time: " << logical_commit_time << " is older than the current cutoff time:" - << max_cleanup_logical_commit_time_for_set_cache_; + << max_cleanup_logical_commit_time; return; } else if (input_value_set.empty()) { VLOG(1) << "Skipping the update as it has no value in the set."; @@ -191,11 +206,14 @@ void KeyValueCache::UpdateKeyValueSet( // end locking key } -void KeyValueCache::DeleteKey(std::string_view key, - int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kDeleteKeyEvent, metrics_recorder_); +void KeyValueCache::DeleteKey(std::string_view key, int64_t logical_commit_time, + std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); absl::MutexLock lock(&mutex_); - if (logical_commit_time <= max_cleanup_logical_commit_time_) { + auto max_cleanup_logical_commit_time = + max_cleanup_logical_commit_time_map_[prefix]; + if (logical_commit_time <= max_cleanup_logical_commit_time) { return; } const auto key_iter = map_.find(key); @@ -208,23 +226,25 @@ void KeyValueCache::DeleteKey(std::string_view key, map_.insert_or_assign( key, {.value = nullptr, .last_logical_commit_time = logical_commit_time}); - - auto result = deleted_nodes_.emplace(logical_commit_time, key); + auto result = deleted_nodes_map_[prefix].emplace(logical_commit_time, key); } } void KeyValueCache::DeleteValuesInSet(std::string_view key, absl::Span value_set, - int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kDeleteValuesInSetEvent, - metrics_recorder_); + int64_t logical_commit_time, + std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); std::unique_ptr key_lock; absl::flat_hash_map* existing_value_set; // The max cleanup time needs to be locked before doing this comparison { absl::MutexLock lock_map(&set_map_mutex_); - - if (logical_commit_time <= max_cleanup_logical_commit_time_for_set_cache_ || + auto max_cleanup_logical_commit_time = + max_cleanup_logical_commit_time_map_for_set_cache_[prefix]; + if (logical_commit_time <= max_cleanup_logical_commit_time || value_set.empty()) { return; } @@ -243,7 +263,7 @@ void KeyValueCache::DeleteValuesInSet(std::string_view key, key_to_value_set_map_.emplace(key, std::move(mutex_value_map_pair)); // Add to deleted set nodes for (const std::string_view value : value_set) { - deleted_set_nodes_[logical_commit_time][key].emplace(value); + deleted_set_nodes_map_[prefix][logical_commit_time][key].emplace(value); } return; } @@ -273,25 +293,36 @@ void KeyValueCache::DeleteValuesInSet(std::string_view key, key_lock.reset(); absl::MutexLock lock_map(&set_map_mutex_); for (const std::string_view value : values_to_delete) { - deleted_set_nodes_[logical_commit_time][key].emplace(value); + deleted_set_nodes_map_[prefix][logical_commit_time][key].emplace(value); } } } -void KeyValueCache::RemoveDeletedKeys(int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kRemoveDeletedKeysEvent, - metrics_recorder_); - CleanUpKeyValueMap(logical_commit_time); - CleanUpKeyValueSetMap(logical_commit_time); +void KeyValueCache::RemoveDeletedKeys(int64_t logical_commit_time, + std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); + CleanUpKeyValueMap(logical_commit_time, prefix); + CleanUpKeyValueSetMap(logical_commit_time, prefix); } -void KeyValueCache::CleanUpKeyValueMap(int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kCleanUpKeyValueMapEvent, - metrics_recorder_); +void KeyValueCache::CleanUpKeyValueMap(int64_t logical_commit_time, + std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); absl::MutexLock lock(&mutex_); - auto it = deleted_nodes_.begin(); + if (max_cleanup_logical_commit_time_map_[prefix] < logical_commit_time) { + max_cleanup_logical_commit_time_map_[prefix] = logical_commit_time; + } + auto deleted_nodes_per_prefix = deleted_nodes_map_.find(prefix); + if (deleted_nodes_per_prefix == deleted_nodes_map_.end()) { + return; + } + auto it = deleted_nodes_per_prefix->second.begin(); - while (it != deleted_nodes_.end()) { + while (it != deleted_nodes_per_prefix->second.end()) { if (it->first > logical_commit_time) { break; } @@ -305,17 +336,30 @@ void KeyValueCache::CleanUpKeyValueMap(int64_t logical_commit_time) { ++it; } - deleted_nodes_.erase(deleted_nodes_.begin(), it); - max_cleanup_logical_commit_time_ = - std::max(max_cleanup_logical_commit_time_, logical_commit_time); + deleted_nodes_per_prefix->second.erase( + deleted_nodes_per_prefix->second.begin(), it); + if (deleted_nodes_per_prefix->second.empty()) { + deleted_nodes_map_.erase(prefix); + } } -void KeyValueCache::CleanUpKeyValueSetMap(int64_t logical_commit_time) { - ScopeLatencyRecorder latency_recorder(kCleanUpKeyValueSetMapEvent, - metrics_recorder_); +void KeyValueCache::CleanUpKeyValueSetMap(int64_t logical_commit_time, + std::string_view prefix) { + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); absl::MutexLock lock_set_map(&set_map_mutex_); - auto delete_itr = deleted_set_nodes_.begin(); - while (delete_itr != deleted_set_nodes_.end()) { + if (max_cleanup_logical_commit_time_map_for_set_cache_[prefix] < + logical_commit_time) { + max_cleanup_logical_commit_time_map_for_set_cache_[prefix] = + logical_commit_time; + } + auto deleted_nodes_per_prefix = deleted_set_nodes_map_.find(prefix); + if (deleted_nodes_per_prefix == deleted_set_nodes_map_.end()) { + return; + } + auto delete_itr = deleted_nodes_per_prefix->second.begin(); + while (delete_itr != deleted_nodes_per_prefix->second.end()) { if (delete_itr->first > logical_commit_time) { break; } @@ -341,13 +385,22 @@ void KeyValueCache::CleanUpKeyValueSetMap(int64_t logical_commit_time) { } ++delete_itr; } - deleted_set_nodes_.erase(deleted_set_nodes_.begin(), delete_itr); - max_cleanup_logical_commit_time_for_set_cache_ = std::max( - max_cleanup_logical_commit_time_for_set_cache_, logical_commit_time); + deleted_nodes_per_prefix->second.erase( + deleted_nodes_per_prefix->second.begin(), delete_itr); + if (deleted_nodes_per_prefix->second.empty()) { + deleted_set_nodes_map_.erase(prefix); + } +} + +void KeyValueCache::LogCacheAccessMetrics( + const RequestContext& request_context, + std::string_view cache_access_event) const { + LogIfError( + request_context.GetInternalLookupMetricsContext() + .AccumulateMetric(1, cache_access_event)); } -std::unique_ptr KeyValueCache::Create( - MetricsRecorder& metrics_recorder) { - return absl::WrapUnique(new KeyValueCache(metrics_recorder)); +std::unique_ptr KeyValueCache::Create() { + return absl::WrapUnique(new KeyValueCache()); } } // namespace kv_server diff --git a/components/data_server/cache/key_value_cache.h b/components/data_server/cache/key_value_cache.h index dd327427..aeae908c 100644 --- a/components/data_server/cache/key_value_cache.h +++ b/components/data_server/cache/key_value_cache.h @@ -32,53 +32,54 @@ #include "components/data_server/cache/cache.h" #include "components/data_server/cache/get_key_value_set_result.h" #include "public/base_types.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { // In-memory datastore. // One cache object is only for keys in one namespace. class KeyValueCache : public Cache { public: - KeyValueCache( - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) - : metrics_recorder_(metrics_recorder) {} - // Looks up and returns key-value pairs for the given keys. absl::flat_hash_map GetKeyValuePairs( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const override; // Looks up and returns key-value set result for the given key set. std::unique_ptr GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const override; - // Inserts or updates the key with the new value. + // Inserts or updates the key with the new value for a given prefix void UpdateKeyValue(std::string_view key, std::string_view value, - int64_t logical_commit_time) override; + int64_t logical_commit_time, + std::string_view prefix = "") override; - // Inserts or updates values in the set for a given key, if a value exists, - // updates its timestamp to the latest logical commit time. + // Inserts or updates values in the set for a given key and prefix, if a value + // exists, updates its timestamp to the latest logical commit time. void UpdateKeyValueSet(std::string_view key, absl::Span input_value_set, - int64_t logical_commit_time) override; + int64_t logical_commit_time, + std::string_view prefix = "") override; - // Deletes a particular (key, value) pair. - void DeleteKey(std::string_view key, int64_t logical_commit_time) override; + // Deletes a particular (key, value) pair for a given prefix. + void DeleteKey(std::string_view key, int64_t logical_commit_time, + std::string_view prefix = "") override; - // Deletes values in the set for a given key. The deletion, this object - // still exist and is marked "deleted", in case there are - // late-arriving updates to this value. + // Deletes values in the set for a given key and prefix. The deletion, this + // object still exist and is marked "deleted", in case there are late-arriving + // updates to this value. void DeleteValuesInSet(std::string_view key, absl::Span value_set, - int64_t logical_commit_time) override; + int64_t logical_commit_time, + std::string_view prefix = "") override; // Removes the values that were deleted before the specified - // logical_commit_time. + // logical_commit_time for a given prefix. // TODO: b/267182790 -- Cache cleanup should be done periodically from a // background thread - void RemoveDeletedKeys(int64_t logical_commit_time) override; + void RemoveDeletedKeys(int64_t logical_commit_time, + std::string_view prefix = "") override; - static std::unique_ptr Create( - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder); + static std::unique_ptr Create(); private: struct CacheValue { @@ -113,18 +114,25 @@ class KeyValueCache : public Cache { // Sorted mapping from the logical timestamp to a key, for nodes that were // deleted We keep this to do proper and efficient clean up in map_. - std::multimap deleted_nodes_ ABSL_GUARDED_BY(mutex_); + // The key in the inner map is the prefix and the value is the keys of key + // value pairs deleted from that prefix + + absl::flat_hash_map> + deleted_nodes_map_ ABSL_GUARDED_BY(mutex_); - // The maximum value that was passed to RemoveDeletedKeys. - int64_t max_cleanup_logical_commit_time_ ABSL_GUARDED_BY(mutex_) = 0; + // The key is the prefix and the value is the + // maximum timestamp that was passed to RemoveDeletedKeys. + absl::flat_hash_map max_cleanup_logical_commit_time_map_ + ABSL_GUARDED_BY(mutex_); - // The maximum value of logical commit time that is used to do update/delete - // for key-value set map. + // The key is the prefix and the value is the maximum + // logical commit time that is used to do update/delete for key-value set map. // TODO(b/284474892) Need to evaluate if we really need to make this variable // guarded b mutex, if not, we may want to remove it and use one // max_cleanup_logical_commit_time in update/deletion for both maps - int64_t max_cleanup_logical_commit_time_for_set_cache_ - ABSL_GUARDED_BY(set_map_mutex_) = 0; + absl::flat_hash_map + max_cleanup_logical_commit_time_map_for_set_cache_ + ABSL_GUARDED_BY(set_map_mutex_); // Mapping from a key to its value map. The key in the inner map is the // value string, and value is the ValueMeta. The inner map allows value @@ -136,23 +144,30 @@ class KeyValueCache : public Cache { std::unique_ptr>>> key_to_value_set_map_ ABSL_GUARDED_BY(set_map_mutex_); - // Sorted mapping from logical timestamp to key-value_set map to keep track of + // The key of outer map is the prefix, and value is the sorted mapping + // from logical timestamp to key-value_set map to keep track of // deleted key-values to handle out of order update case. In the inner map, // the key string is the key for the values, and the string // in the flat_hash_set is the value - absl::btree_map>> - deleted_set_nodes_ ABSL_GUARDED_BY(set_map_mutex_); - - // Removes deleted keys from key-value map - void CleanUpKeyValueMap(int64_t logical_commit_time); - - // Removes deleted key-values from key-value_set map - void CleanUpKeyValueSetMap(int64_t logical_commit_time); + absl::flat_hash_map< + std::string, + absl::btree_map< + int64_t, + absl::flat_hash_map>>> + deleted_set_nodes_map_ ABSL_GUARDED_BY(set_map_mutex_); + + // Removes deleted keys from key-value map for a given prefix + void CleanUpKeyValueMap(int64_t logical_commit_time, std::string_view prefix); + + // Removes deleted key-values from key-value_set map for a given prefix + void CleanUpKeyValueSetMap(int64_t logical_commit_time, + std::string_view prefix); + // Logs cache access metrics for cache hit or miss counts. The cache access + // event name is defined in server_definition.h file + void LogCacheAccessMetrics(const RequestContext& request_context, + std::string_view cache_access_event) const; friend class KeyValueCacheTestPeer; - - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; }; } // namespace kv_server diff --git a/components/data_server/cache/key_value_cache_test.cc b/components/data_server/cache/key_value_cache_test.cc index 2bc09b39..12ee3d6f 100644 --- a/components/data_server/cache/key_value_cache_test.cc +++ b/components/data_server/cache/key_value_cache_test.cc @@ -30,8 +30,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "public/base_types.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { @@ -39,9 +38,12 @@ class KeyValueCacheTestPeer { public: KeyValueCacheTestPeer() = delete; static std::multimap ReadDeletedNodes( - const KeyValueCache& c) { + const KeyValueCache& c, std::string_view prefix = "") { absl::MutexLock lock(&c.mutex_); - return c.deleted_nodes_; + auto map_itr = c.deleted_nodes_map_.find(prefix); + return map_itr == c.deleted_nodes_map_.end() + ? std::multimap() + : c.deleted_nodes_map_.find(prefix)->second; } static absl::flat_hash_map& ReadNodes(KeyValueCache& c) { @@ -49,18 +51,24 @@ class KeyValueCacheTestPeer { return c.map_; } - static int GetDeletedSetNodesMapSize(const KeyValueCache& c) { + static int GetDeletedSetNodesMapSize(const KeyValueCache& c, + std::string prefix = "") { absl::MutexLock lock(&c.set_map_mutex_); - return c.deleted_set_nodes_.size(); + auto map_itr = c.deleted_set_nodes_map_.find(prefix); + return map_itr == c.deleted_set_nodes_map_.end() ? 0 + : map_itr->second.size(); } static absl::flat_hash_set ReadDeletedSetNodesForTimestamp( - const KeyValueCache& c, int64_t logical_commit_time, - std::string_view key) { + const KeyValueCache& c, int64_t logical_commit_time, std::string_view key, + std::string_view prefix = "") { absl::MutexLock lock(&c.set_map_mutex_); - return c.deleted_set_nodes_.find(logical_commit_time) - ->second.find(key) - ->second; + auto map_itr = c.deleted_set_nodes_map_.find(prefix); + return map_itr == c.deleted_set_nodes_map_.end() + ? absl::flat_hash_set() + : map_itr->second.find(logical_commit_time) + ->second.find(key) + ->second; } static int GetCacheKeyValueSetMapSize(KeyValueCache& c) { @@ -82,7 +90,7 @@ class KeyValueCacheTestPeer { } static void CallCacheCleanup(KeyValueCache& c, int64_t logical_commit_time) { - c.CleanUpKeyValueMap(logical_commit_time); + c.RemoveDeletedKeys(logical_commit_time); } }; @@ -91,24 +99,33 @@ namespace { using privacy_sandbox::server_common::TelemetryProvider; using testing::UnorderedElementsAre; -TEST(CacheTest, RetrievesMatchingEntry) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +class CacheTest : public ::testing::Test { + protected: + CacheTest() { + InitMetricsContextMap(); + scope_metrics_context_ = std::make_unique(); + request_context_ = + std::make_unique(*scope_metrics_context_); + } + RequestContext& GetRequestContext() { return *request_context_; } + std::unique_ptr scope_metrics_context_; + std::unique_ptr request_context_; +}; + +TEST_F(CacheTest, RetrievesMatchingEntry) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 1); absl::flat_hash_set keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); absl::flat_hash_set wrong_keys = {"wrong_key"}; - EXPECT_FALSE(cache->GetKeyValuePairs(keys).empty()); - EXPECT_TRUE(cache->GetKeyValuePairs(wrong_keys).empty()); + EXPECT_FALSE(cache->GetKeyValuePairs(GetRequestContext(), keys).empty()); + EXPECT_TRUE(cache->GetKeyValuePairs(GetRequestContext(), wrong_keys).empty()); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); } -TEST(CacheTest, GetWithMultipleKeysReturnsMatchingValues) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, GetWithMultipleKeysReturnsMatchingValues) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("key1", "value1", 1); cache->UpdateKeyValue("key2", "value2", 2); cache->UpdateKeyValue("key3", "value3", 3); @@ -116,116 +133,101 @@ TEST(CacheTest, GetWithMultipleKeysReturnsMatchingValues) { absl::flat_hash_set full_keys = {"key1", "key2"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 2); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("key1", "value1"), KVPairEq("key2", "value2"))); } -TEST(CacheTest, GetAfterUpdateReturnsNewValue) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, GetAfterUpdateReturnsNewValue) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 1); absl::flat_hash_set keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); cache->UpdateKeyValue("my_key", "my_new_value", 2); - kv_pairs = cache->GetKeyValuePairs(keys); + kv_pairs = cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_EQ(kv_pairs.size(), 1); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_new_value"))); } -TEST(CacheTest, GetAfterUpdateDifferentKeyReturnsSameValue) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, GetAfterUpdateDifferentKeyReturnsSameValue) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 1); cache->UpdateKeyValue("new_key", "new_value", 2); absl::flat_hash_set keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); } -TEST(CacheTest, GetForEmptyCacheReturnsEmptyList) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, GetForEmptyCacheReturnsEmptyList) { + std::unique_ptr cache = KeyValueCache::Create(); absl::flat_hash_set keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_EQ(kv_pairs.size(), 0); } -TEST(CacheTest, GetForCacheReturnsValueSet) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, GetForCacheReturnsValueSet) { + std::unique_ptr cache = KeyValueCache::Create(); std::vector values = {"v1", "v2"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v1", "v2")); } -TEST(CacheTest, GetForCacheMissingKeyReturnsEmptySet) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, GetForCacheMissingKeyReturnsEmptySet) { + std::unique_ptr cache = KeyValueCache::Create(); std::vector values = {"v1", "v2"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); auto get_key_value_set_result = - cache->GetKeyValueSet({"missing_key", "my_key"}); + cache->GetKeyValueSet(GetRequestContext(), {"missing_key", "my_key"}); EXPECT_EQ(get_key_value_set_result->GetValueSet("missing_key").size(), 0); EXPECT_THAT(get_key_value_set_result->GetValueSet("my_key"), UnorderedElementsAre("v1", "v2")); } -TEST(DeleteKeyTest, RemovesKeyEntry) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyTestRemovesKeyEntry) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 1); cache->DeleteKey("my_key", 2); absl::flat_hash_set full_keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 0); } -TEST(DeleteKeyValueSetTest, WrongkeyDoesNotRemoveEntry) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyValueSetWrongkeyDoesNotRemoveEntry) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 1); cache->DeleteKey("wrong_key", 1); absl::flat_hash_set keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); } -TEST(DeleteKeyValueSetTest, RemovesValueEntry) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyValueSetRemovesValueEntry) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1", "v2", "v3"}; std::vector values_to_delete = {"v1", "v2"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("my_key", absl::Span(values_to_delete), 2); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v3")); auto value_meta_v3 = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v3"); @@ -243,18 +245,15 @@ TEST(DeleteKeyValueSetTest, RemovesValueEntry) { EXPECT_EQ(value_meta_v2_deleted.is_deleted, true); } -TEST(DeleteKeyValueSetTest, WrongKeyDoesNotRemoveKeyValueEntry) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyValueSetWrongKeyDoesNotRemoveKeyValueEntry) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1", "v2", "v3"}; std::vector values_to_delete = {"v1"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("wrong_key", absl::Span(values_to_delete), 2); std::unique_ptr result = - cache->GetKeyValueSet({"my_key", "wrong_key"}); + cache->GetKeyValueSet(GetRequestContext(), {"my_key", "wrong_key"}); EXPECT_THAT(result->GetValueSet("my_key"), UnorderedElementsAre("v1", "v2", "v3")); EXPECT_EQ(result->GetValueSet("wrong_key").size(), 0); @@ -276,18 +275,16 @@ TEST(DeleteKeyValueSetTest, WrongKeyDoesNotRemoveKeyValueEntry) { EXPECT_EQ(value_meta_v1_deleted_for_wrong_key.is_deleted, true); } -TEST(DeleteKeyValueSetTest, WrongValueDoesNotRemoveEntry) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyValueSetWrongValueDoesNotRemoveEntry) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1", "v2", "v3"}; std::vector values_to_delete = {"v4"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("my_key", absl::Span(values_to_delete), 2); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v1", "v2", "v3")); auto value_meta_v1 = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -304,85 +301,73 @@ TEST(DeleteKeyValueSetTest, WrongValueDoesNotRemoveEntry) { EXPECT_EQ(value_set_in_cache_size, 4); } -TEST(CacheTest, OutOfOrderUpdateAfterUpdateWorks) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, OutOfOrderUpdateAfterUpdateWorks) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 2); absl::flat_hash_set keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); cache->UpdateKeyValue("my_key", "my_new_value", 1); - kv_pairs = cache->GetKeyValuePairs(keys); + kv_pairs = cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_EQ(kv_pairs.size(), 1); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); } -TEST(DeleteKeyTest, OutOfOrderDeleteAfterUpdateWorks) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyOutOfOrderDeleteAfterUpdateWorks) { + std::unique_ptr cache = KeyValueCache::Create(); cache->DeleteKey("my_key", 2); cache->UpdateKeyValue("my_key", "my_value", 1); absl::flat_hash_set full_keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 0); } -TEST(DeleteKeyTest, OutOfOrderUpdateAfterDeleteWorks) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyOutOfOrderUpdateAfterDeleteWorks) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 2); cache->DeleteKey("my_key", 1); absl::flat_hash_set full_keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 1); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); } -TEST(DeleteKeyTest, InOrderUpdateAfterDeleteWorks) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyInOrderUpdateAfterDeleteWorks) { + std::unique_ptr cache = KeyValueCache::Create(); cache->DeleteKey("my_key", 1); cache->UpdateKeyValue("my_key", "my_value", 2); absl::flat_hash_set full_keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 1); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key", "my_value"))); } -TEST(DeleteKeyTest, InOrderDeleteAfterUpdateWorks) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); +TEST_F(CacheTest, DeleteKeyInOrderDeleteAfterUpdateWorks) { + std::unique_ptr cache = KeyValueCache::Create(); cache->UpdateKeyValue("my_key", "my_value", 1); cache->DeleteKey("my_key", 2); absl::flat_hash_set full_keys = {"my_key"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 0); } -TEST(UpdateKeyValueSetTest, UpdateAfterUpdateWithSameValue) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, UpdateSetTestUpdateAfterUpdateWithSameValue) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->UpdateKeyValueSet("my_key", absl::Span(values), 2); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v1")); auto value_meta = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -390,11 +375,8 @@ TEST(UpdateKeyValueSetTest, UpdateAfterUpdateWithSameValue) { EXPECT_EQ(value_meta.is_deleted, false); } -TEST(UpdateKeyValueSetTest, UpdateAfterUpdateWithDifferentValue) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, UpdateSetTestUpdateAfterUpdateWithDifferentValue) { + std::unique_ptr cache = std::make_unique(); std::vector first_value = {"v1"}; std::vector second_value = {"v2"}; cache->UpdateKeyValueSet("my_key", absl::Span(first_value), @@ -402,7 +384,8 @@ TEST(UpdateKeyValueSetTest, UpdateAfterUpdateWithDifferentValue) { cache->UpdateKeyValueSet("my_key", absl::Span(second_value), 2); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v1", "v2")); auto value_meta_v1 = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -414,16 +397,14 @@ TEST(UpdateKeyValueSetTest, UpdateAfterUpdateWithDifferentValue) { EXPECT_EQ(value_meta_v2.is_deleted, false); } -TEST(InOrderUpdateKeyValueSetTest, InsertAfterDeleteExpectInsert) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, InOrderUpdateSetInsertAfterDeleteExpectInsert) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1"}; cache->DeleteValuesInSet("my_key", absl::Span(values), 1); cache->UpdateKeyValueSet("my_key", absl::Span(values), 2); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v1")); auto value_meta = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -431,16 +412,14 @@ TEST(InOrderUpdateKeyValueSetTest, InsertAfterDeleteExpectInsert) { EXPECT_EQ(value_meta.is_deleted, false); } -TEST(InOrderUpdateKeyValueSetTest, DeleteAfterInsert) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, InOrderUpdateSetDeleteAfterInsert) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("my_key", absl::Span(values), 2); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_EQ(value_set.size(), 0); auto value_meta_v1 = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -448,16 +427,14 @@ TEST(InOrderUpdateKeyValueSetTest, DeleteAfterInsert) { EXPECT_EQ(value_meta_v1.is_deleted, true); } -TEST(OutOfOrderUpdateKeyValueSetTest, InsertAfterDeleteExpectNoInsert) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, OutOfOrderUpdateSetInsertAfterDeleteExpectNoInsert) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1"}; cache->DeleteValuesInSet("my_key", absl::Span(values), 2); cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_EQ(value_set.size(), 0); auto value_meta = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -465,16 +442,14 @@ TEST(OutOfOrderUpdateKeyValueSetTest, InsertAfterDeleteExpectNoInsert) { EXPECT_EQ(value_meta.is_deleted, true); } -TEST(OutOfOrderUpdateKeyValueSetTest, DeleteAfterInsertExpectNoDelete) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, OutOfOrderUpdateSetDeleteAfterInsertExpectNoDelete) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 2); cache->DeleteValuesInSet("my_key", absl::Span(values), 1); absl::flat_hash_set value_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_THAT(value_set, UnorderedElementsAre("v1")); auto value_meta_v1 = KeyValueCacheTestPeer::GetSetValueMeta(*cache, "my_key", "v1"); @@ -482,22 +457,16 @@ TEST(OutOfOrderUpdateKeyValueSetTest, DeleteAfterInsertExpectNoDelete) { EXPECT_EQ(value_meta_v1.is_deleted, false); } -TEST(CleanUpTimestamps, InsertAKeyDoesntUpdateDeletedNodes) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsInsertAKeyDoesntUpdateDeletedNodes) { + std::unique_ptr cache = std::make_unique(); cache->UpdateKeyValue("my_key", "my_value", 1); auto deleted_nodes = KeyValueCacheTestPeer::ReadDeletedNodes(*cache); EXPECT_EQ(deleted_nodes.size(), 0); } -TEST(CleanUpTimestamps, RemoveDeletedKeysRemovesOldRecords) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsRemoveDeletedKeysRemovesOldRecords) { + std::unique_ptr cache = std::make_unique(); cache->UpdateKeyValue("my_key", "my_value", 1); cache->DeleteKey("my_key", 2); @@ -510,11 +479,8 @@ TEST(CleanUpTimestamps, RemoveDeletedKeysRemovesOldRecords) { EXPECT_EQ(nodes.size(), 0); } -TEST(CleanUpTimestamps, RemoveDeletedKeysDoesntAffectNewRecords) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsRemoveDeletedKeysDoesntAffectNewRecords) { + std::unique_ptr cache = std::make_unique(); cache->UpdateKeyValue("my_key", "my_value", 5); cache->DeleteKey("my_key", 6); @@ -527,12 +493,9 @@ TEST(CleanUpTimestamps, RemoveDeletedKeysDoesntAffectNewRecords) { EXPECT_EQ(range.first->second, "my_key"); } -TEST(CleanUpTimestamps, - RemoveDeletedKeysRemovesOldRecordsDoesntAffectNewRecords) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, + CleanupRemoveDeletedKeysRemovesOldRecordsDoesntAffectNewRecords) { + std::unique_ptr cache = std::make_unique(); cache->UpdateKeyValue("my_key1", "my_value", 1); cache->UpdateKeyValue("my_key2", "my_value", 2); cache->UpdateKeyValue("my_key3", "my_value", 3); @@ -559,17 +522,14 @@ TEST(CleanUpTimestamps, "my_key1", "my_key2", "my_key3", "my_key4", "my_key5", }; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(full_keys); + cache->GetKeyValuePairs(GetRequestContext(), full_keys); EXPECT_EQ(kv_pairs.size(), 2); EXPECT_THAT(kv_pairs, UnorderedElementsAre(KVPairEq("my_key4", "my_value"), KVPairEq("my_key5", "my_value"))); } -TEST(CleanUpTimestamps, CantInsertOldRecordsAfterCleanup) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsCantInsertOldRecordsAfterCleanup) { + std::unique_ptr cache = std::make_unique(); cache->UpdateKeyValue("my_key1", "my_value", 10); cache->DeleteKey("my_key1", 12); cache->RemoveDeletedKeys(13); @@ -582,15 +542,12 @@ TEST(CleanUpTimestamps, CantInsertOldRecordsAfterCleanup) { absl::flat_hash_set keys = {"my_key1"}; absl::flat_hash_map kv_pairs = - cache->GetKeyValuePairs(keys); + cache->GetKeyValuePairs(GetRequestContext(), keys); EXPECT_EQ(kv_pairs.size(), 0); } -TEST(CleanUpTimestampsForSetCache, InsertKeyValueSetDoesntUpdateDeletedNodes) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsInsertKeyValueSetDoesntUpdateDeletedNodes) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"my_value"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); int deleted_nodes_map_size = @@ -598,11 +555,8 @@ TEST(CleanUpTimestampsForSetCache, InsertKeyValueSetDoesntUpdateDeletedNodes) { EXPECT_EQ(deleted_nodes_map_size, 0); } -TEST(CleanUpTimestampsForSetCache, DeleteKeyValueSetExpectUpdateDeletedNodes) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsDeleteKeyValueSetExpectUpdateDeletedNodes) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"my_value"}; cache->DeleteValuesInSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("another_key", absl::Span(values), @@ -620,11 +574,8 @@ TEST(CleanUpTimestampsForSetCache, DeleteKeyValueSetExpectUpdateDeletedNodes) { 1); } -TEST(CleanUpTimestampsForSetCache, RemoveDeletedKeyValuesRemovesOldRecords) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsRemoveDeletedKeyValuesRemovesOldRecords) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"my_value"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("my_key", absl::Span(values), 2); @@ -639,12 +590,9 @@ TEST(CleanUpTimestampsForSetCache, RemoveDeletedKeyValuesRemovesOldRecords) { EXPECT_EQ(KeyValueCacheTestPeer::GetCacheKeyValueSetMapSize(*cache), 0); } -TEST(CleanUpTimestampsForSetCache, - RemoveDeletedKeyValuesDoesntAffectNewRecords) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, + CleanupTimestampsRemoveDeletedKeyValuesDoesntAffectNewRecords) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"my_value"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 5); cache->DeleteValuesInSet("my_key", absl::Span(values), 6); @@ -660,12 +608,10 @@ TEST(CleanUpTimestampsForSetCache, 1); } -TEST(CleanUpTimestampsForSetCache, - RemoveDeletedKeysRemovesOldRecordsDoesntAffectNewRecords) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F( + CacheTest, + CleanupSetCacheRemoveDeletedKeysRemovesOldRecordsDoesntAffectNewRecords) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"v1", "v2"}; std::vector values_to_delete = {"v1"}; cache->UpdateKeyValueSet("my_key1", absl::Span(values), 1); @@ -689,8 +635,8 @@ TEST(CleanUpTimestampsForSetCache, "my_key2") .size(), 1); - auto get_value_set_result = - cache->GetKeyValueSet({"my_key1", "my_key4", "my_key3"}); + auto get_value_set_result = cache->GetKeyValueSet( + GetRequestContext(), {"my_key1", "my_key4", "my_key3"}); EXPECT_THAT(get_value_set_result->GetValueSet("my_key4"), UnorderedElementsAre("v1", "v2")); EXPECT_THAT(get_value_set_result->GetValueSet("my_key3"), @@ -699,11 +645,8 @@ TEST(CleanUpTimestampsForSetCache, UnorderedElementsAre("v2")); } -TEST(CleanUpTimestampsForSetCache, CantInsertOldRecordsAfterCleanup) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsSetCacheCantInsertOldRecordsAfterCleanup) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"my_value"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("my_key", absl::Span(values), 2); @@ -717,15 +660,13 @@ TEST(CleanUpTimestampsForSetCache, CantInsertOldRecordsAfterCleanup) { cache->UpdateKeyValueSet("my_key", absl::Span(values), 2); absl::flat_hash_set kv_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_EQ(kv_set.size(), 0); } -TEST(CleanUpTimestampsForSetCache, CantAddOldDeletedRecordsAfterCleanup) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = - std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, CleanupTimestampsCantAddOldDeletedRecordsAfterCleanup) { + std::unique_ptr cache = std::make_unique(); std::vector values = {"my_value"}; cache->UpdateKeyValueSet("my_key", absl::Span(values), 1); cache->DeleteValuesInSet("my_key", absl::Span(values), 2); @@ -755,14 +696,13 @@ TEST(CleanUpTimestampsForSetCache, CantAddOldDeletedRecordsAfterCleanup) { EXPECT_EQ(value_meta.last_logical_commit_time, 4); absl::flat_hash_set kv_set = - cache->GetKeyValueSet({"my_key"})->GetValueSet("my_key"); + cache->GetKeyValueSet(GetRequestContext(), {"my_key"}) + ->GetValueSet("my_key"); EXPECT_EQ(kv_set.size(), 0); } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndGet) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentGetAndGet) { + auto cache = std::make_unique(); absl::flat_hash_set keys_lookup_request = {"key1", "key2"}; std::vector values_for_key1 = {"v1"}; std::vector values_for_key2 = {"v2"}; @@ -771,9 +711,10 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndGet) { cache->UpdateKeyValueSet("key2", absl::Span(values_for_key2), 1); absl::Notification start; - auto lookup_fn = [&cache, &keys_lookup_request, &start]() { + auto request_context = GetRequestContext(); + auto lookup_fn = [&cache, &keys_lookup_request, &start, &request_context]() { start.WaitForNotification(); - auto result = cache->GetKeyValueSet(keys_lookup_request); + auto result = cache->GetKeyValueSet(request_context, keys_lookup_request); EXPECT_THAT(result->GetValueSet("key1"), UnorderedElementsAre("v1")); EXPECT_THAT(result->GetValueSet("key2"), UnorderedElementsAre("v2")); }; @@ -788,19 +729,19 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndGet) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndUpdateExpectNoUpdate) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentGetAndUpdateExpectNoUpdate) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1"}; std::vector existing_values = {"v1"}; cache->UpdateKeyValueSet("key1", absl::Span(existing_values), 3); absl::Notification start; - auto lookup_fn = [&cache, &keys, &start]() { + auto request_context = GetRequestContext(); + auto lookup_fn = [&cache, &keys, &start, &request_context]() { start.WaitForNotification(); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; std::vector new_values = {"v1"}; auto update_fn = [&cache, &new_values, &start]() { @@ -820,19 +761,19 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndUpdateExpectNoUpdate) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndUpdateExpectUpdate) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentGetAndUpdateExpectUpdate) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1", "key2"}; std::vector existing_values = {"v1"}; cache->UpdateKeyValueSet("key1", absl::Span(existing_values), 1); absl::Notification start; - auto lookup_fn = [&cache, &keys, &start]() { + auto request_context = GetRequestContext(); + auto lookup_fn = [&cache, &keys, &start, &request_context]() { start.WaitForNotification(); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; std::vector new_values_for_key2 = {"v2"}; auto update_fn = [&cache, &new_values_for_key2, &start]() { @@ -853,19 +794,19 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndUpdateExpectUpdate) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndDeleteExpectNoDelete) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentGetAndDeleteExpectNoDelete) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1"}; std::vector existing_values = {"v1"}; cache->UpdateKeyValueSet("key1", absl::Span(existing_values), 3); absl::Notification start; - auto lookup_fn = [&cache, &keys, &start]() { + auto request_context = GetRequestContext(); + auto lookup_fn = [&cache, &keys, &start, &request_context]() { start.WaitForNotification(); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; std::vector delete_values = {"v1"}; auto delete_fn = [&cache, &delete_values, &start]() { @@ -886,10 +827,8 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndDeleteExpectNoDelete) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndCleanUp) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentGetAndCleanUp) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1", "key2"}; std::vector existing_values = {"v1"}; cache->UpdateKeyValueSet("key1", @@ -899,11 +838,16 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndCleanUp) { cache->DeleteValuesInSet("key2", absl::Span(existing_values), 2); absl::Notification start; - auto lookup_fn = [&cache, &keys, &start]() { + auto request_context = GetRequestContext(); + auto lookup_fn = [&cache, &keys, &start, &request_context]() { start.WaitForNotification(); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); - EXPECT_EQ(cache->GetKeyValueSet(keys)->GetValueSet("key2").size(), 0); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); + EXPECT_EQ(cache->GetKeyValueSet(request_context, keys) + ->GetValueSet("key2") + .size(), + 0); }; auto cleanup_fn = [&cache, &start]() { // clean up old records @@ -922,29 +866,32 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetAndCleanUp) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndUpdateExpectUpdateBoth) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentUpdateAndUpdateExpectUpdateBoth) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1", "key2"}; std::vector values_for_key1 = {"v1"}; absl::Notification start; - auto update_key1 = [&cache, &keys, &values_for_key1, &start]() { + auto request_context = GetRequestContext(); + auto update_key1 = [&cache, &keys, &values_for_key1, &start, + &request_context]() { start.WaitForNotification(); // expect new value is inserted for key1 cache->UpdateKeyValueSet("key1", absl::Span(values_for_key1), 1); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; std::vector values_for_key2 = {"v2"}; - auto update_key2 = [&cache, &keys, &values_for_key2, &start]() { + auto update_key2 = [&cache, &keys, &values_for_key2, &start, + &request_context]() { // expect new value is inserted for key2 start.WaitForNotification(); cache->UpdateKeyValueSet("key2", absl::Span(values_for_key2), 2); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key2"), - UnorderedElementsAre("v2")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key2"), + UnorderedElementsAre("v2")); }; std::vector threads; for (int i = 0; i < std::min(20, (int)std::thread::hardware_concurrency()); @@ -958,20 +905,21 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndUpdateExpectUpdateBoth) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndDelete) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentUpdateAndDelete) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1", "key2"}; std::vector values_for_key1 = {"v1"}; absl::Notification start; - auto update_key1 = [&cache, &keys, &values_for_key1, &start]() { + auto request_context = GetRequestContext(); + auto update_key1 = [&cache, &keys, &values_for_key1, &start, + &request_context]() { start.WaitForNotification(); // expect new value is inserted for key1 cache->UpdateKeyValueSet("key1", absl::Span(values_for_key1), 1); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; // Update existing value for key2 std::vector existing_values_for_key2 = {"v1", "v2"}; @@ -979,13 +927,15 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndDelete) { "key2", absl::Span(existing_values_for_key2), 1); std::vector values_to_delete_for_key2 = {"v1"}; - auto delete_key2 = [&cache, &keys, &values_to_delete_for_key2, &start]() { + auto delete_key2 = [&cache, &keys, &values_to_delete_for_key2, &start, + &request_context]() { start.WaitForNotification(); // expect value is deleted for key2 cache->DeleteValuesInSet( "key2", absl::Span(values_to_delete_for_key2), 2); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key2"), - UnorderedElementsAre("v2")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key2"), + UnorderedElementsAre("v2")); }; std::vector threads; @@ -1000,23 +950,24 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndDelete) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndCleanUp) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentUpdateAndCleanUp) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1"}; std::vector values_for_key1 = {"v1"}; absl::Notification start; - auto update_fn = [&cache, &keys, &values_for_key1, &start]() { + auto request_context = GetRequestContext(); + auto update_fn = [&cache, &keys, &values_for_key1, &start, + &request_context]() { start.WaitForNotification(); cache->UpdateKeyValueSet("key1", - absl::Span(values_for_key1), 1); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + absl::Span(values_for_key1), 2); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; auto cleanup_fn = [&cache, &start]() { start.WaitForNotification(); - KeyValueCacheTestPeer::CallCacheCleanup(*cache, 2); + KeyValueCacheTestPeer::CallCacheCleanup(*cache, 1); }; std::vector threads; @@ -1031,25 +982,28 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentUpdateAndCleanUp) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentDeleteAndCleanUp) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentDeleteAndCleanUp) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1"}; std::vector values_for_key1 = {"v1"}; cache->UpdateKeyValueSet("key1", absl::Span(values_for_key1), 1); absl::Notification start; - auto delete_fn = [&cache, &keys, &values_for_key1, &start]() { + auto request_context = GetRequestContext(); + auto delete_fn = [&cache, &keys, &values_for_key1, &start, + &request_context]() { start.WaitForNotification(); // expect new value is deleted for key1 cache->DeleteValuesInSet("key1", absl::Span(values_for_key1), 2); - EXPECT_EQ(cache->GetKeyValueSet(keys)->GetValueSet("key1").size(), 0); + EXPECT_EQ(cache->GetKeyValueSet(request_context, keys) + ->GetValueSet("key1") + .size(), + 0); }; auto cleanup_fn = [&cache, &start]() { start.WaitForNotification(); - KeyValueCacheTestPeer::CallCacheCleanup(*cache, 2); + KeyValueCacheTestPeer::CallCacheCleanup(*cache, 1); }; std::vector threads; for (int i = 0; i < std::min(20, (int)std::thread::hardware_concurrency()); @@ -1063,10 +1017,8 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentDeleteAndCleanUp) { } } -TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetUpdateDeleteCleanUp) { - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - auto cache = std::make_unique(*noop_metrics_recorder); +TEST_F(CacheTest, ConcurrentGetUpdateDeleteCleanUp) { + auto cache = std::make_unique(); absl::flat_hash_set keys = {"key1", "key2"}; std::vector existing_values_for_key1 = {"v1"}; std::vector existing_values_for_key2 = {"v1"}; @@ -1090,13 +1042,14 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetUpdateDeleteCleanUp) { }; auto cleanup = [&cache, &start]() { start.WaitForNotification(); - KeyValueCacheTestPeer::CallCacheCleanup(*cache, 2); + KeyValueCacheTestPeer::CallCacheCleanup(*cache, 1); }; - - auto lookup_for_key1 = [&cache, &keys, &start]() { + auto request_context = GetRequestContext(); + auto lookup_for_key1 = [&cache, &keys, &start, &request_context]() { start.WaitForNotification(); - EXPECT_THAT(cache->GetKeyValueSet(keys)->GetValueSet("key1"), - UnorderedElementsAre("v1")); + EXPECT_THAT( + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key1"), + UnorderedElementsAre("v1")); }; std::vector threads; @@ -1112,8 +1065,216 @@ TEST(ConcurrentSetMemoryAccessTest, ConcurrentGetUpdateDeleteCleanUp) { thread.join(); } auto look_up_result_for_key2 = - cache->GetKeyValueSet(keys)->GetValueSet("key2"); + cache->GetKeyValueSet(request_context, keys)->GetValueSet("key2"); EXPECT_THAT(look_up_result_for_key2, UnorderedElementsAre("v2")); } + +TEST_F(CacheTest, MultiplePrefixKeyValueUpdates) { + std::unique_ptr cache = KeyValueCache::Create(); + // Call remove deleted keys for prefix1 to update the max delete cutoff + // timestamp + cache->RemoveDeletedKeys(1, "prefix1"); + cache->UpdateKeyValue("prefix1-key", "value1", 2, "prefix1"); + cache->UpdateKeyValue("prefix2-key", "value2", 1, "prefix2"); + absl::flat_hash_map kv_pairs = + cache->GetKeyValuePairs(GetRequestContext(), + {"prefix1-key", "prefix2-key"}); + EXPECT_EQ(kv_pairs.size(), 2); + EXPECT_EQ(kv_pairs["prefix1-key"], "value1"); + EXPECT_EQ(kv_pairs["prefix2-key"], "value2"); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueNoUpdateForAnother) { + std::unique_ptr cache = KeyValueCache::Create(); + // Call remove deleted keys for prefix1 to update the max delete cutoff + // timestamp + cache->RemoveDeletedKeys(2, "prefix1"); + // Expect no update for prefix1 + cache->UpdateKeyValue("prefix1-key", "value1", 1, "prefix1"); + cache->UpdateKeyValue("prefix2-key", "value2", 1, "prefix2"); + absl::flat_hash_map kv_pairs = + cache->GetKeyValuePairs(GetRequestContext(), + {"prefix1-key", "prefix2-key"}); + EXPECT_EQ(kv_pairs.size(), 1); + EXPECT_EQ(kv_pairs["prefix2-key"], "value2"); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueNoDeleteForAnother) { + std::unique_ptr cache = KeyValueCache::Create(); + // Call remove deleted keys for prefix1 to update the max delete cutoff + // timestamp + cache->RemoveDeletedKeys(2, "prefix1"); + cache->UpdateKeyValue("prefix1-key", "value1", 3, "prefix1"); + // Expect no deletion + cache->DeleteKey("prefix1-key", 1, "prefix1"); + cache->UpdateKeyValue("prefix2-key", "value2", 1, "prefix2"); + absl::flat_hash_map kv_pairs = + cache->GetKeyValuePairs(GetRequestContext(), + {"prefix1-key", "prefix2-key"}); + EXPECT_EQ(kv_pairs.size(), 2); + EXPECT_EQ(kv_pairs["prefix1-key"], "value1"); + EXPECT_EQ(kv_pairs["prefix2-key"], "value2"); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueDeletesAndUpdates) { + std::unique_ptr cache = std::make_unique(); + cache->DeleteKey("prefix1-key", 2, "prefix1"); + cache->UpdateKeyValue("prefix1-key", "value1", 1, "prefix1"); + cache->UpdateKeyValue("prefix2-key", "value2", 1, "prefix2"); + absl::flat_hash_map kv_pairs = + cache->GetKeyValuePairs(GetRequestContext(), + {"prefix1-key", "prefix2-key"}); + EXPECT_EQ(kv_pairs.size(), 1); + EXPECT_EQ(kv_pairs["prefix2-key"], "value2"); + auto deleted_nodes = + KeyValueCacheTestPeer::ReadDeletedNodes(*cache, "prefix1"); + EXPECT_EQ(deleted_nodes.size(), 1); + deleted_nodes = KeyValueCacheTestPeer::ReadDeletedNodes(*cache, "prefix2"); + EXPECT_EQ(deleted_nodes.size(), 0); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueUpdatesAndDeletes) { + std::unique_ptr cache = std::make_unique(); + cache->UpdateKeyValue("prefix1-key", "value1", 2, "prefix1"); + // Expects no deletes + cache->DeleteKey("prefix1-key", 1, "prefix1"); + cache->UpdateKeyValue("prefix2-key", "value2", 1, "prefix2"); + absl::flat_hash_map kv_pairs = + cache->GetKeyValuePairs(GetRequestContext(), + {"prefix1-key", "prefix2-key"}); + EXPECT_EQ(kv_pairs.size(), 2); + EXPECT_EQ(kv_pairs["prefix1-key"], "value1"); + EXPECT_EQ(kv_pairs["prefix2-key"], "value2"); + auto deleted_nodes = KeyValueCacheTestPeer::ReadDeletedNodes(*cache); + EXPECT_EQ(deleted_nodes.size(), 0); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueSetUpdates) { + std::unique_ptr cache = std::make_unique(); + std::vector values1 = {"v1", "v2"}; + std::vector values2 = {"v3", "v4"}; + // Call remove deleted keys for prefix1 to update the max delete cutoff + // timestamp + cache->RemoveDeletedKeys(1, "prefix1"); + cache->UpdateKeyValueSet("prefix1-key", absl::Span(values1), + 2, "prefix1"); + cache->UpdateKeyValueSet("prefix2-key", absl::Span(values2), + 1, "prefix2"); + + auto get_value_set_result = cache->GetKeyValueSet( + GetRequestContext(), {"prefix1-key", "prefix2-key"}); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix1-key"), + UnorderedElementsAre("v1", "v2")); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix2-key"), + UnorderedElementsAre("v3", "v4")); +} + +TEST_F(CacheTest, MultipleKeyValueSetNoUpdateForAnother) { + std::unique_ptr cache = std::make_unique(); + std::vector values1 = {"v1", "v2"}; + std::vector values2 = {"v3", "v4"}; + // Call remove deleted keys for prefix1 to update the max delete cutoff + // timestamp + cache->RemoveDeletedKeys(2, "prefix1"); + cache->UpdateKeyValueSet("prefix1-key", absl::Span(values1), + 1, "prefix1"); + cache->UpdateKeyValueSet("prefix2-key", absl::Span(values2), + 1, "prefix2"); + auto get_value_set_result = cache->GetKeyValueSet( + GetRequestContext(), {"prefix1-key", "prefix2-key"}); + EXPECT_EQ(get_value_set_result->GetValueSet("prefix1-key").size(), 0); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix2-key"), + UnorderedElementsAre("v3", "v4")); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueSetDeletesAndUpdates) { + std::unique_ptr cache = std::make_unique(); + std::vector values1 = {"v1", "v2"}; + std::vector values_to_delete = {"v1"}; + std::vector values2 = {"v3", "v4"}; + cache->DeleteValuesInSet("prefix1-key", + absl::Span(values_to_delete), 2, + "prefix1"); + cache->UpdateKeyValueSet("prefix1-key", absl::Span(values1), + 1, "prefix1"); + cache->UpdateKeyValueSet("prefix2-key", absl::Span(values2), + 1, "prefix2"); + auto get_value_set_result = cache->GetKeyValueSet( + GetRequestContext(), {"prefix1-key", "prefix2-key"}); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix1-key"), + UnorderedElementsAre("v2")); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix2-key"), + UnorderedElementsAre("v3", "v4")); + EXPECT_EQ(KeyValueCacheTestPeer::GetDeletedSetNodesMapSize(*cache, "prefix1"), + 1); + EXPECT_EQ(KeyValueCacheTestPeer::GetDeletedSetNodesMapSize(*cache, "prefix2"), + 0); +} + +TEST_F(CacheTest, MultiplePrefixKeyValueSetUpdatesAndDeletes) { + std::unique_ptr cache = std::make_unique(); + std::vector values1 = {"v1", "v2"}; + std::vector values_to_delete = {"v1"}; + std::vector values2 = {"v3", "v4"}; + + cache->UpdateKeyValueSet("prefix1-key", absl::Span(values1), + 2, "prefix1"); + cache->UpdateKeyValueSet("prefix2-key", absl::Span(values2), + 1, "prefix2"); + // Expect no deletes + cache->DeleteValuesInSet("prefix1-key", + absl::Span(values_to_delete), 1, + "prefix1"); + auto get_value_set_result = cache->GetKeyValueSet( + GetRequestContext(), {"prefix1-key", "prefix2-key"}); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix1-key"), + UnorderedElementsAre("v1", "v2")); + EXPECT_THAT(get_value_set_result->GetValueSet("prefix2-key"), + UnorderedElementsAre("v3", "v4")); + EXPECT_EQ(KeyValueCacheTestPeer::GetDeletedSetNodesMapSize(*cache, "prefix1"), + 0); + EXPECT_EQ(KeyValueCacheTestPeer::GetDeletedSetNodesMapSize(*cache, "prefix2"), + 0); +} + +TEST_F(CacheTest, MultiplePrefixTimestampKeyValueCleanUps) { + std::unique_ptr cache = std::make_unique(); + cache->UpdateKeyValue("prefix1-key", "value", 2, "prefix1"); + cache->DeleteKey("prefix1-key", 3, "prefix1"); + cache->UpdateKeyValue("prefix2-key", "value", 2, "prefix2"); + cache->DeleteKey("prefix2-key", 5, "prefix2"); + auto deleted_nodes_for_prefix1 = + KeyValueCacheTestPeer::ReadDeletedNodes(*cache, "prefix1"); + EXPECT_EQ(deleted_nodes_for_prefix1.size(), 1); + cache->RemoveDeletedKeys(4, "prefix1"); + cache->RemoveDeletedKeys(4, "prefix2"); + deleted_nodes_for_prefix1 = + KeyValueCacheTestPeer::ReadDeletedNodes(*cache, "prefix1"); + EXPECT_EQ(deleted_nodes_for_prefix1.size(), 0); + auto deleted_nodes_for_prefix2 = + KeyValueCacheTestPeer::ReadDeletedNodes(*cache, "prefix2"); + EXPECT_EQ(deleted_nodes_for_prefix2.size(), 1); +} +TEST_F(CacheTest, MultiplePrefixTimestampKeyValueSetCleanUps) { + std::unique_ptr cache = std::make_unique(); + std::vector values = {"v1", "v2"}; + std::vector values_to_delete = {"v1"}; + cache->UpdateKeyValueSet("prefix1-key", absl::Span(values), + 2, "prefix1"); + cache->UpdateKeyValueSet("prefix2-key", absl::Span(values), + 2, "prefix2"); + cache->DeleteValuesInSet("prefix1-key", + absl::Span(values_to_delete), 3, + "prefix1"); + cache->DeleteValuesInSet("prefix2-key", + absl::Span(values_to_delete), 5, + "prefix2"); + cache->RemoveDeletedKeys(4, "prefix1"); + cache->RemoveDeletedKeys(4, "prefix2"); + EXPECT_EQ(KeyValueCacheTestPeer::GetDeletedSetNodesMapSize(*cache, "prefix1"), + 0); + EXPECT_EQ(KeyValueCacheTestPeer::GetDeletedSetNodesMapSize(*cache, "prefix2"), + 1); +} } // namespace } // namespace kv_server diff --git a/components/data_server/cache/mocks.h b/components/data_server/cache/mocks.h index aec1d60a..134c71fc 100644 --- a/components/data_server/cache/mocks.h +++ b/components/data_server/cache/mocks.h @@ -33,24 +33,30 @@ MATCHER_P2(KVPairEq, key, value, "") { class MockCache : public Cache { public: MOCK_METHOD((absl::flat_hash_map), GetKeyValuePairs, - (const absl::flat_hash_set&), + (const RequestContext& request_context, + const absl::flat_hash_set&), (const, override)); MOCK_METHOD((std::unique_ptr), GetKeyValueSet, - (const absl::flat_hash_set&), + (const RequestContext& request_context, + const absl::flat_hash_set&), (const, override)); MOCK_METHOD(void, UpdateKeyValue, - (std::string_view key, std::string_view value, int64_t ts), + (std::string_view key, std::string_view value, int64_t ts, + std::string_view prefix), (override)); MOCK_METHOD(void, UpdateKeyValueSet, (std::string_view key, absl::Span value_set, - int64_t logical_commit_time), + int64_t logical_commit_time, std::string_view prefix), (override)); MOCK_METHOD(void, DeleteValuesInSet, (std::string_view key, absl::Span value_set, - int64_t logical_commit_time), + int64_t logical_commit_time, std::string_view prefix), + (override)); + MOCK_METHOD(void, DeleteKey, + (std::string_view key, int64_t ts, std::string_view prefix), + (override)); + MOCK_METHOD(void, RemoveDeletedKeys, (int64_t ts, std::string_view prefix), (override)); - MOCK_METHOD(void, DeleteKey, (std::string_view key, int64_t ts), (override)); - MOCK_METHOD(void, RemoveDeletedKeys, (int64_t ts), (override)); }; class MockGetKeyValueSetResult : public GetKeyValueSetResult { diff --git a/components/data_server/cache/noop_key_value_cache.h b/components/data_server/cache/noop_key_value_cache.h index b26b3fda..5fc2afd9 100644 --- a/components/data_server/cache/noop_key_value_cache.h +++ b/components/data_server/cache/noop_key_value_cache.h @@ -26,23 +26,30 @@ namespace kv_server { class NoOpKeyValueCache : public Cache { public: absl::flat_hash_map GetKeyValuePairs( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const override { return {}; }; std::unique_ptr GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const override { return std::make_unique(); } void UpdateKeyValue(std::string_view key, std::string_view value, - int64_t logical_commit_time) override {} + int64_t logical_commit_time, + std::string_view prefix) override {} void UpdateKeyValueSet(std::string_view key, absl::Span value_set, - int64_t logical_commit_time) override {} - void DeleteKey(std::string_view key, int64_t logical_commit_time) override {} + int64_t logical_commit_time, + std::string_view prefix) override {} + void DeleteKey(std::string_view key, int64_t logical_commit_time, + std::string_view prefix) override {} void DeleteValuesInSet(std::string_view key, absl::Span value_set, - int64_t logical_commit_time) override {} - void RemoveDeletedKeys(int64_t logical_commit_time) override {} + int64_t logical_commit_time, + std::string_view prefix) override {} + void RemoveDeletedKeys(int64_t logical_commit_time, + std::string_view prefix) override {} static std::unique_ptr Create() { return std::make_unique(); } diff --git a/components/data_server/data_loading/BUILD.bazel b/components/data_server/data_loading/BUILD.bazel index e33a3ed4..a15a8102 100644 --- a/components/data_server/data_loading/BUILD.bazel +++ b/components/data_server/data_loading/BUILD.bazel @@ -27,9 +27,11 @@ cc_library( "data_orchestrator.h", ], deps = [ + "//components/data/blob_storage:blob_prefix_allowlist", "//components/data/blob_storage:blob_storage_change_notifier", "//components/data/blob_storage:blob_storage_client", "//components/data/blob_storage:delta_file_notifier", + "//components/data/file_group:file_group_search_utils", "//components/data/realtime:realtime_notifier", "//components/data/realtime:realtime_thread_pool_manager", "//components/data_server/cache", @@ -42,13 +44,14 @@ cc_library( "//public/data_loading/readers:riegeli_stream_io", "//public/data_loading/readers:stream_record_reader_factory", "//public/sharding:key_sharder", - "@com_github_google_glog//:glog", "@com_google_absl//absl/functional:any_invocable", "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/synchronization", - "@google_privacysandbox_servers_common//src/cpp/telemetry:tracing", + "@google_privacysandbox_servers_common//src/telemetry:tracing", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) @@ -68,9 +71,8 @@ cc_test( "//public/data_loading:records_utils", "//public/test_util:mocks", "//public/test_util:proto_matcher", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) diff --git a/components/data_server/data_loading/data_orchestrator.cc b/components/data_server/data_loading/data_orchestrator.cc index dca1ffcd..f787b309 100644 --- a/components/data_server/data_loading/data_orchestrator.cc +++ b/components/data_server/data_loading/data_orchestrator.cc @@ -19,19 +19,27 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/functional/bind_front.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" +#include "components/data/file_group/file_group_search_utils.h" #include "components/errors/retry.h" -#include "glog/logging.h" #include "public/constants.h" #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/filename_utils.h" #include "public/data_loading/records_utils.h" #include "public/sharding/sharding_function.h" -#include "src/cpp/telemetry/tracing.h" +#include "src/telemetry/tracing.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server { namespace { +// TODO(b/321716836): use the default prefix to apply cache updates for realtime +// for now. This needs to be removed after we are done with directory support +// for file updates. +constexpr std::string_view kDefaultPrefixForRealTimeUpdates = ""; +constexpr std::string_view kDefaultDataSourceForRealtimeUpdates = "realtime"; using privacy_sandbox::server_common::TraceWithStatusOr; @@ -46,36 +54,41 @@ class BlobRecordStream : public RecordStream { std::unique_ptr blob_reader_; }; -void LogDataLoadingMetrics(const DataLoadingStats& data_loading_stats) { - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - static_cast(data_loading_stats.total_updated_records))); - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - static_cast(data_loading_stats.total_deleted_records))); - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogUpDownCounter( - static_cast(data_loading_stats.total_dropped_records))); +void LogDataLoadingMetrics(std::string_view source, + const DataLoadingStats& data_loading_stats) { + LogIfError(KVServerContextMap() + ->SafeMetric() + .LogUpDownCounter( + {{std::string(source), + static_cast( + data_loading_stats.total_updated_records)}})); + LogIfError(KVServerContextMap() + ->SafeMetric() + .LogUpDownCounter( + {{std::string(source), + static_cast( + data_loading_stats.total_deleted_records)}})); + LogIfError(KVServerContextMap() + ->SafeMetric() + .LogUpDownCounter( + {{std::string(source), + static_cast( + data_loading_stats.total_dropped_records)}})); } -absl::Status ApplyUpdateMutation(const KeyValueMutationRecord& record, +absl::Status ApplyUpdateMutation(std::string_view prefix, + const KeyValueMutationRecord& record, Cache& cache) { if (record.value_type() == Value::StringValue) { cache.UpdateKeyValue(record.key()->string_view(), GetRecordValue(record), - record.logical_commit_time()); + record.logical_commit_time(), prefix); return absl::OkStatus(); } if (record.value_type() == Value::StringSet) { auto values = GetRecordValue>(record); cache.UpdateKeyValueSet(record.key()->string_view(), absl::MakeSpan(values), - record.logical_commit_time()); + record.logical_commit_time(), prefix); return absl::OkStatus(); } return absl::InvalidArgumentError( @@ -83,16 +96,18 @@ absl::Status ApplyUpdateMutation(const KeyValueMutationRecord& record, " has unsupported value type: ", record.value_type())); } -absl::Status ApplyDeleteMutation(const KeyValueMutationRecord& record, +absl::Status ApplyDeleteMutation(std::string_view prefix, + const KeyValueMutationRecord& record, Cache& cache) { if (record.value_type() == Value::StringValue) { - cache.DeleteKey(record.key()->string_view(), record.logical_commit_time()); + cache.DeleteKey(record.key()->string_view(), record.logical_commit_time(), + prefix); return absl::OkStatus(); } if (record.value_type() == Value::StringSet) { auto values = GetRecordValue>(record); cache.DeleteValuesInSet(record.key()->string_view(), absl::MakeSpan(values), - record.logical_commit_time()); + record.logical_commit_time(), prefix); return absl::OkStatus(); } return absl::InvalidArgumentError( @@ -123,11 +138,12 @@ bool ShouldProcessRecord(const KeyValueMutationRecord& record, } absl::Status ApplyKeyValueMutationToCache( - const KeyValueMutationRecord& record, Cache& cache, int64_t& max_timestamp, - DataLoadingStats& data_loading_stats) { + std::string_view prefix, const KeyValueMutationRecord& record, Cache& cache, + int64_t& max_timestamp, DataLoadingStats& data_loading_stats) { switch (record.mutation_type()) { case KeyValueMutationType::Update: { - if (auto status = ApplyUpdateMutation(record, cache); !status.ok()) { + if (auto status = ApplyUpdateMutation(prefix, record, cache); + !status.ok()) { return status; } max_timestamp = std::max(max_timestamp, record.logical_commit_time()); @@ -135,7 +151,8 @@ absl::Status ApplyKeyValueMutationToCache( break; } case KeyValueMutationType::Delete: { - if (auto status = ApplyDeleteMutation(record, cache); !status.ok()) { + if (auto status = ApplyDeleteMutation(prefix, record, cache); + !status.ok()) { return status; } max_timestamp = std::max(max_timestamp, record.logical_commit_time()); @@ -151,12 +168,13 @@ absl::Status ApplyKeyValueMutationToCache( } absl::StatusOr LoadCacheWithData( + std::string_view data_source, std::string_view prefix, StreamRecordReader& record_reader, Cache& cache, int64_t& max_timestamp, const int32_t server_shard_num, const int32_t num_shards, UdfClient& udf_client, const KeySharder& key_sharder) { DataLoadingStats data_loading_stats; const auto process_data_record_fn = - [&cache, &max_timestamp, &data_loading_stats, server_shard_num, + [prefix, &cache, &max_timestamp, &data_loading_stats, server_shard_num, num_shards, &udf_client, &key_sharder](const DataRecord& data_record) { if (data_record.record_type() == Record::KeyValueMutationRecord) { const auto* record = data_record.record_as_KeyValueMutationRecord(); @@ -166,8 +184,8 @@ absl::StatusOr LoadCacheWithData( // this will get us in a loop return absl::OkStatus(); } - return ApplyKeyValueMutationToCache(*record, cache, max_timestamp, - data_loading_stats); + return ApplyKeyValueMutationToCache( + prefix, *record, cache, max_timestamp, data_loading_stats); } else if (data_record.record_type() == Record::UserDefinedFunctionsConfig) { const auto* udf_config = @@ -180,18 +198,16 @@ absl::StatusOr LoadCacheWithData( .logical_commit_time = udf_config->logical_commit_time(), .version = udf_config->version()}); } - LOG(ERROR) << "Received unsupported record "; - return absl::InvalidArgumentError("Record type not supported."); + return absl::InvalidArgumentError("Received unsupported record."); }; - - auto status = record_reader.ReadStreamRecords( + // TODO(b/314302953): ReadStreamRecords will skip over individual records that + // have errors. We should pass the file name to the function so that it will + // appear in error logs. + PS_RETURN_IF_ERROR(record_reader.ReadStreamRecords( [&process_data_record_fn](std::string_view raw) { return DeserializeDataRecord(raw, process_data_record_fn); - }); - if (!status.ok()) { - return status; - } - LogDataLoadingMetrics(data_loading_stats); + })); + LogDataLoadingMetrics(data_source, data_loading_stats); return data_loading_stats; } @@ -208,14 +224,12 @@ absl::StatusOr LoadCacheWithDataFromFile( return std::make_unique( options.blob_client.GetBlobReader(location)); }); - auto metadata = record_reader->GetKVFileMetadata(); - if (!metadata.ok()) { - return metadata.status(); - } - if (metadata->has_sharding_metadata() && - metadata->sharding_metadata().shard_num() != options.shard_num) { + PS_ASSIGN_OR_RETURN(auto metadata, record_reader->GetKVFileMetadata(), + _ << "Blob " << location); + if (metadata.has_sharding_metadata() && + metadata.sharding_metadata().shard_num() != options.shard_num) { LOG(INFO) << "Blob " << location << " belongs to shard num " - << metadata->sharding_metadata().shard_num() + << metadata.sharding_metadata().shard_num() << " but server shard num is " << options.shard_num << " Skipping it."; return DataLoadingStats{ @@ -224,14 +238,20 @@ absl::StatusOr LoadCacheWithDataFromFile( .total_dropped_records = 0, }; } - auto status = LoadCacheWithData(*record_reader, cache, max_timestamp, - options.shard_num, options.num_shards, - options.udf_client, options.key_sharder); - if (status.ok()) { - cache.RemoveDeletedKeys(max_timestamp); - } - return status; + std::string file_name = + location.prefix.empty() + ? location.key + : absl::StrCat(location.prefix, "/", location.key); + PS_ASSIGN_OR_RETURN( + auto data_loading_stats, + LoadCacheWithData(file_name, location.prefix, *record_reader, cache, + max_timestamp, options.shard_num, options.num_shards, + options.udf_client, options.key_sharder), + _ << "Blob: " << location); + cache.RemoveDeletedKeys(max_timestamp, location.prefix); + return data_loading_stats; } + absl::StatusOr TraceLoadCacheWithDataFromFile( BlobStorageClient::DataLocation location, const DataOrchestrator::Options& options) { @@ -241,6 +261,7 @@ absl::StatusOr TraceLoadCacheWithDataFromFile( }, "LoadCacheWithDataFromFile", {{"bucket", std::move(location.bucket)}, + {"prefix", std::move(location.prefix)}, {"key", std::move(location.key)}}); } @@ -248,9 +269,11 @@ class DataOrchestratorImpl : public DataOrchestrator { public: // `last_basename` is the last file seen during init. The cache is up to // date until this file. - DataOrchestratorImpl(Options options, std::string last_basename) + DataOrchestratorImpl( + Options options, + absl::flat_hash_map prefix_last_basenames) : options_(std::move(options)), - last_basename_of_init_(std::move(last_basename)) {} + prefix_last_basenames_(std::move(prefix_last_basenames)) {} ~DataOrchestratorImpl() override { if (!data_loader_thread_) return; @@ -270,38 +293,43 @@ class DataOrchestratorImpl : public DataOrchestrator { LOG(INFO) << "Stopped loading new data"; } - static absl::StatusOr Init(Options& options) { - auto ending_delta_file = LoadSnapshotFiles(options); - if (!ending_delta_file.ok()) { - return ending_delta_file.status(); - } - auto maybe_filenames = options.blob_client.ListBlobs( - {.bucket = options.data_bucket}, - {.prefix = std::string(FilePrefix()), - .start_after = *ending_delta_file}); - if (!maybe_filenames.ok()) { - return maybe_filenames.status(); + static absl::StatusOr> Init( + Options& options) { + auto ending_delta_files = LoadSnapshotFiles(options); + if (!ending_delta_files.ok()) { + return ending_delta_files.status(); } - LOG(INFO) << "Initializing cache with " << maybe_filenames->size() - << " delta files from " << options.data_bucket; - - std::string last_basename = std::move(*ending_delta_file); - for (auto&& basename : std::move(*maybe_filenames)) { - if (!IsDeltaFilename(basename)) { - LOG(WARNING) << "Saw a file " << basename - << " not in delta file format. Skipping it."; - continue; + for (const auto& prefix : options.blob_prefix_allowlist.Prefixes()) { + auto location = BlobStorageClient::DataLocation{ + .bucket = options.data_bucket, .prefix = prefix}; + auto iter = ending_delta_files->find(prefix); + auto maybe_filenames = options.blob_client.ListBlobs( + location, + {.prefix = std::string(FilePrefix()), + .start_after = + iter != ending_delta_files->end() ? iter->second : ""}); + if (!maybe_filenames.ok()) { + return maybe_filenames.status(); } - last_basename = basename; - if (const auto s = TraceLoadCacheWithDataFromFile( - {.bucket = options.data_bucket, .key = std::move(basename)}, - options); - !s.ok()) { - return s.status(); + LOG(INFO) << "Initializing cache with " << maybe_filenames->size() + << " delta files from " << location; + for (auto&& basename : std::move(*maybe_filenames)) { + auto blob = BlobStorageClient::DataLocation{ + .bucket = options.data_bucket, .prefix = prefix, .key = basename}; + if (!IsDeltaFilename(blob.key)) { + LOG(WARNING) << "Saw a file " << blob + << " not in delta file format. Skipping it."; + continue; + } + (*ending_delta_files)[prefix] = blob.key; + if (const auto s = TraceLoadCacheWithDataFromFile(blob, options); + !s.ok()) { + return s.status(); + } + LOG(INFO) << "Done loading " << blob; } - LOG(INFO) << "Done loading " << last_basename; } - return last_basename; + return ending_delta_files; } absl::Status Start() override { @@ -309,9 +337,10 @@ class DataOrchestratorImpl : public DataOrchestrator { return absl::OkStatus(); } LOG(INFO) << "Transitioning to state ContinuouslyLoadNewData"; + auto prefix_last_basenames = prefix_last_basenames_; absl::Status status = options_.delta_notifier.Start( options_.change_notifier, {.bucket = options_.data_bucket}, - last_basename_of_init_, + std::move(prefix_last_basenames), absl::bind_front(&DataOrchestratorImpl::EnqueueNewFilesToProcess, this)); if (!status.ok()) { @@ -324,8 +353,10 @@ class DataOrchestratorImpl : public DataOrchestrator { [this, &cache = options_.cache, &delta_stream_reader_factory = options_.delta_stream_reader_factory]( const std::string& message_body) { - return LoadCacheWithHighPriorityUpdates(delta_stream_reader_factory, - message_body, cache); + return LoadCacheWithHighPriorityUpdates( + kDefaultDataSourceForRealtimeUpdates, + kDefaultPrefixForRealTimeUpdates, delta_stream_reader_factory, + message_body, cache); }); } @@ -354,16 +385,25 @@ class DataOrchestratorImpl : public DataOrchestrator { unprocessed_basenames_.pop_back(); } LOG(INFO) << "Loading " << basename; - if (!IsDeltaFilename(basename)) { + auto blob = ParseBlobName(basename); + if (!IsDeltaFilename(blob.key)) { LOG(WARNING) << "Received file with invalid name: " << basename; continue; } + if (!options_.blob_prefix_allowlist.Contains(blob.prefix)) { + LOG(WARNING) << "Received file with prefix not allowlisted: " + << basename; + continue; + } RetryUntilOk( - [this, &basename] { + [this, &basename, &blob] { // TODO: distinguish status. Some can be retried while others // are fatal. return TraceLoadCacheWithDataFromFile( - {.bucket = options_.data_bucket, .key = basename}, options_); + {.bucket = options_.data_bucket, + .prefix = blob.prefix, + .key = blob.key}, + options_); }, "LoadNewFile", LogStatusSafeMetricsFn()); } @@ -379,67 +419,68 @@ class DataOrchestratorImpl : public DataOrchestrator { // Loads snapshot files if there are any. // Returns the latest delta file to be included in a snapshot. - static absl::StatusOr LoadSnapshotFiles(const Options& options) { - absl::StatusOr> snapshots = - options.blob_client.ListBlobs( - {.bucket = options.data_bucket}, - {.prefix = FilePrefix().data()}); - if (!snapshots.ok()) { - return snapshots.status(); - } - LOG(INFO) << "Initializing cache with snapshot file(s) from: " - << options.data_bucket; - std::string ending_delta_file; - for (int64_t s = snapshots->size() - 1; s >= 0; s--) { - std::string_view snapshot = snapshots->at(s); - if (!IsSnapshotFilename(snapshot)) { - LOG(WARNING) << "Saw a file " << snapshot - << " not in snapshot file format. Skipping it."; - continue; - } - BlobStorageClient::DataLocation location{.bucket = options.data_bucket, - .key = snapshot.data()}; - auto record_reader = - options.delta_stream_reader_factory.CreateConcurrentReader( - /*stream_factory=*/[&location, &options]() { - return std::make_unique( - options.blob_client.GetBlobReader(location)); - }); - auto metadata = record_reader->GetKVFileMetadata(); - if (!metadata.ok()) { - return metadata.status(); - } - if (metadata->has_sharding_metadata() && - metadata->sharding_metadata().shard_num() != options.shard_num) { - LOG(INFO) << "Snapshot " << location << " belongs to shard num " - << metadata->sharding_metadata().shard_num() - << " but server shard num is " << options.shard_num - << ". Skipping it."; + static absl::StatusOr> + LoadSnapshotFiles(const Options& options) { + absl::flat_hash_map ending_delta_files; + for (const auto& prefix : options.blob_prefix_allowlist.Prefixes()) { + auto location = BlobStorageClient::DataLocation{ + .bucket = options.data_bucket, .prefix = prefix}; + LOG(INFO) << "Initializing cache with snapshot file(s) from: " + << location; + PS_ASSIGN_OR_RETURN( + auto snapshot_group, + FindMostRecentFileGroup( + location, + FileGroupFilter{.file_type = FileType::SNAPSHOT, + .status = FileGroup::FileStatus::kComplete}, + options.blob_client)); + if (!snapshot_group.has_value()) { + LOG(INFO) << "No snapshot files found in: " << location; continue; } - LOG(INFO) << "Loading snapshot file: " << location; - if (auto status = TraceLoadCacheWithDataFromFile(location, options); - !status.ok()) { - return status.status(); - } - if (metadata->snapshot().ending_delta_file() > ending_delta_file) { - ending_delta_file = std::move(metadata->snapshot().ending_delta_file()); + for (const auto& snapshot : snapshot_group->Filenames()) { + auto snapshot_blob = BlobStorageClient::DataLocation{ + .bucket = options.data_bucket, .prefix = prefix, .key = snapshot}; + auto record_reader = + options.delta_stream_reader_factory.CreateConcurrentReader( + /*stream_factory=*/[&snapshot_blob, &options]() { + return std::make_unique( + options.blob_client.GetBlobReader(snapshot_blob)); + }); + PS_ASSIGN_OR_RETURN(auto metadata, record_reader->GetKVFileMetadata()); + if (metadata.has_sharding_metadata() && + metadata.sharding_metadata().shard_num() != options.shard_num) { + LOG(INFO) << "Snapshot " << snapshot_blob << " belongs to shard num " + << metadata.sharding_metadata().shard_num() + << " but server shard num is " << options.shard_num + << ". Skipping it."; + continue; + } + LOG(INFO) << "Loading snapshot file: " << snapshot_blob; + PS_ASSIGN_OR_RETURN( + auto stats, TraceLoadCacheWithDataFromFile(snapshot_blob, options)); + if (auto iter = ending_delta_files.find(prefix); + iter == ending_delta_files.end() || + metadata.snapshot().ending_delta_file() > iter->second) { + ending_delta_files[prefix] = metadata.snapshot().ending_delta_file(); + } + LOG(INFO) << "Done loading snapshot file: " << snapshot_blob; } - LOG(INFO) << "Done loading snapshot file: " << location; - break; } - return ending_delta_file; + return ending_delta_files; } absl::StatusOr LoadCacheWithHighPriorityUpdates( + std::string_view data_source, std::string_view prefix, StreamRecordReaderFactory& delta_stream_reader_factory, const std::string& record_string, Cache& cache) { std::istringstream is(record_string); int64_t max_timestamp = 0; auto record_reader = delta_stream_reader_factory.CreateReader(is); - return LoadCacheWithData(*record_reader, cache, max_timestamp, - options_.shard_num, options_.num_shards, - options_.udf_client, options_.key_sharder); + return LoadCacheWithData(data_source, prefix, *record_reader, cache, + max_timestamp, options_.shard_num, + options_.num_shards, options_.udf_client, + options_.key_sharder); } const Options options_; @@ -448,19 +489,19 @@ class DataOrchestratorImpl : public DataOrchestrator { std::unique_ptr data_loader_thread_; bool stop_ ABSL_GUARDED_BY(mu_) = false; // last basename of file in initialization. - const std::string last_basename_of_init_; + absl::flat_hash_map prefix_last_basenames_; }; } // namespace absl::StatusOr> DataOrchestrator::TryCreate( Options options) { - const auto maybe_last_basename = DataOrchestratorImpl::Init(options); - if (!maybe_last_basename.ok()) { - return maybe_last_basename.status(); + const auto prefix_last_basenames = DataOrchestratorImpl::Init(options); + if (!prefix_last_basenames.ok()) { + return prefix_last_basenames.status(); } auto orchestrator = std::make_unique( - std::move(options), std::move(maybe_last_basename.value())); + std::move(options), std::move(prefix_last_basenames.value())); return orchestrator; } } // namespace kv_server diff --git a/components/data_server/data_loading/data_orchestrator.h b/components/data_server/data_loading/data_orchestrator.h index 769f4917..d9a17c51 100644 --- a/components/data_server/data_loading/data_orchestrator.h +++ b/components/data_server/data_loading/data_orchestrator.h @@ -22,6 +22,7 @@ #include "absl/functional/any_invocable.h" #include "absl/status/status.h" +#include "components/data/blob_storage/blob_prefix_allowlist.h" #include "components/data/blob_storage/blob_storage_change_notifier.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/blob_storage/delta_file_notifier.h" @@ -56,6 +57,7 @@ class DataOrchestrator { const int32_t shard_num = 0; const int32_t num_shards = 1; const KeySharder key_sharder; + BlobPrefixAllowlist blob_prefix_allowlist; }; // Creates initial state. Scans the bucket and initializes the cache with data diff --git a/components/data_server/data_loading/data_orchestrator_test.cc b/components/data_server/data_loading/data_orchestrator_test.cc index a1ce263c..913e6324 100644 --- a/components/data_server/data_loading/data_orchestrator_test.cc +++ b/components/data_server/data_loading/data_orchestrator_test.cc @@ -18,6 +18,7 @@ #include #include +#include "absl/log/log.h" #include "absl/synchronization/notification.h" #include "components/data/common/mocks.h" #include "components/data/realtime/realtime_notifier.h" @@ -25,7 +26,6 @@ #include "components/data_server/cache/mocks.h" #include "components/udf/code_config.h" #include "components/udf/mocks.h" -#include "glog/logging.h" #include "gmock/gmock.h" #include "google/protobuf/text_format.h" #include "gtest/gtest.h" @@ -36,8 +36,8 @@ #include "public/sharding/sharding_function.h" #include "public/test_util/mocks.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/telemetry/mocks.h" +using kv_server::BlobPrefixAllowlist; using kv_server::BlobStorageChangeNotifier; using kv_server::BlobStorageClient; using kv_server::CodeConfig; @@ -70,8 +70,10 @@ using testing::_; using testing::AllOf; using testing::ByMove; using testing::Field; +using testing::Pair; using testing::Return; using testing::ReturnRef; +using testing::UnorderedElementsAre; namespace { // using google::protobuf::TextFormat; @@ -103,8 +105,9 @@ class DataOrchestratorTest : public ::testing::Test { .udf_client = udf_client_, .delta_stream_reader_factory = delta_stream_reader_factory_, .realtime_thread_pool_manager = realtime_thread_pool_manager_, - .key_sharder = kv_server::KeySharder( - kv_server::ShardingFunction{/*seed=*/""})}) {} + .key_sharder = + kv_server::KeySharder(kv_server::ShardingFunction{/*seed=*/""}), + .blob_prefix_allowlist = kv_server::BlobPrefixAllowlist("")}) {} MockBlobStorageClient blob_client_; MockDeltaFileNotifier notifier_; @@ -290,7 +293,9 @@ TEST_F(DataOrchestratorTest, InitCache_SkipsInvalidKVMutation) { ASSERT_TRUE(maybe_orchestrator.ok()); const std::string last_basename = ToDeltaFileName(1).value(); - EXPECT_CALL(notifier_, Start(_, GetTestLocation(), last_basename, _)) + EXPECT_CALL(notifier_, + Start(_, GetTestLocation(), + UnorderedElementsAre(Pair("", last_basename)), _)) .WillOnce(Return(absl::UnknownError(""))); EXPECT_FALSE((*maybe_orchestrator)->Start().ok()); } @@ -352,15 +357,17 @@ TEST_F(DataOrchestratorTest, InitCacheSuccess) { .WillOnce(Return(ByMove(std::move(update_reader)))) .WillOnce(Return(ByMove(std::move(delete_reader)))); - EXPECT_CALL(cache_, UpdateKeyValue("bar", "bar value", 3)).Times(1); - EXPECT_CALL(cache_, DeleteKey("bar", 3)).Times(1); - EXPECT_CALL(cache_, RemoveDeletedKeys(3)).Times(2); + EXPECT_CALL(cache_, UpdateKeyValue("bar", "bar value", 3, _)).Times(1); + EXPECT_CALL(cache_, DeleteKey("bar", 3, _)).Times(1); + EXPECT_CALL(cache_, RemoveDeletedKeys(3, _)).Times(2); auto maybe_orchestrator = DataOrchestrator::TryCreate(options_); ASSERT_TRUE(maybe_orchestrator.ok()); const std::string last_basename = ToDeltaFileName(2).value(); - EXPECT_CALL(notifier_, Start(_, GetTestLocation(), last_basename, _)) + EXPECT_CALL(notifier_, + Start(_, GetTestLocation(), + UnorderedElementsAre(Pair("", last_basename)), _)) .WillOnce(Return(absl::UnknownError(""))); EXPECT_FALSE((*maybe_orchestrator)->Start().ok()); } @@ -410,7 +417,9 @@ TEST_F(DataOrchestratorTest, UpdateUdfCodeSuccess) { ASSERT_TRUE(maybe_orchestrator.ok()); const std::string last_basename = ToDeltaFileName(1).value(); - EXPECT_CALL(notifier_, Start(_, GetTestLocation(), last_basename, _)) + EXPECT_CALL(notifier_, + Start(_, GetTestLocation(), + UnorderedElementsAre(Pair("", last_basename)), _)) .WillOnce(Return(absl::UnknownError(""))); EXPECT_FALSE((*maybe_orchestrator)->Start().ok()); } @@ -460,7 +469,9 @@ TEST_F(DataOrchestratorTest, UpdateUdfCodeFails_OrchestratorContinues) { ASSERT_TRUE(maybe_orchestrator.ok()); const std::string last_basename = ToDeltaFileName(1).value(); - EXPECT_CALL(notifier_, Start(_, GetTestLocation(), last_basename, _)) + EXPECT_CALL(notifier_, + Start(_, GetTestLocation(), + UnorderedElementsAre(Pair("", last_basename)), _)) .WillOnce(Return(absl::UnknownError(""))); EXPECT_FALSE((*maybe_orchestrator)->Start().ok()); } @@ -473,10 +484,11 @@ TEST_F(DataOrchestratorTest, StartLoading) { auto orchestrator = std::move(maybe_orchestrator.value()); const std::string last_basename = ""; - EXPECT_CALL(notifier_, Start(_, GetTestLocation(), last_basename, _)) - .WillOnce([](BlobStorageChangeNotifier& change_notifier, - BlobStorageClient::DataLocation location, - std::string start_after, + EXPECT_CALL(notifier_, + Start(_, GetTestLocation(), + absl::flat_hash_map(), _)) + .WillOnce([](BlobStorageChangeNotifier&, BlobStorageClient::DataLocation, + absl::flat_hash_map, std::function callback) { callback(ToDeltaFileName(6).value()); callback(ToDeltaFileName(7).value()); @@ -528,9 +540,9 @@ TEST_F(DataOrchestratorTest, StartLoading) { .WillOnce(Return(ByMove(std::move(update_reader)))) .WillOnce(Return(ByMove(std::move(delete_reader)))); - EXPECT_CALL(cache_, UpdateKeyValue("bar", "bar value", 3)).Times(1); - EXPECT_CALL(cache_, DeleteKey("bar", 3)).Times(1); - EXPECT_CALL(cache_, RemoveDeletedKeys(3)).Times(2); + EXPECT_CALL(cache_, UpdateKeyValue("bar", "bar value", 3, _)).Times(1); + EXPECT_CALL(cache_, DeleteKey("bar", 3, _)).Times(1); + EXPECT_CALL(cache_, RemoveDeletedKeys(3, _)).Times(2); EXPECT_TRUE(orchestrator->Start().ok()); LOG(INFO) << "Created ContinuouslyLoadNewData"; @@ -605,9 +617,9 @@ TEST_F(DataOrchestratorTest, InitCacheShardedSuccessSkipRecord) { .WillOnce(Return(ByMove(std::move(update_reader)))) .WillOnce(Return(ByMove(std::move(delete_reader)))); - EXPECT_CALL(strict_cache, RemoveDeletedKeys(0)).Times(1); - EXPECT_CALL(strict_cache, DeleteKey("shard2", 3)).Times(1); - EXPECT_CALL(strict_cache, RemoveDeletedKeys(3)).Times(1); + EXPECT_CALL(strict_cache, RemoveDeletedKeys(0, _)).Times(1); + EXPECT_CALL(strict_cache, DeleteKey("shard2", 3, _)).Times(1); + EXPECT_CALL(strict_cache, RemoveDeletedKeys(3, _)).Times(1); auto sharded_options = DataOrchestrator::Options{ .data_bucket = GetTestLocation().bucket, @@ -621,7 +633,8 @@ TEST_F(DataOrchestratorTest, InitCacheShardedSuccessSkipRecord) { .shard_num = 1, .num_shards = 2, .key_sharder = - kv_server::KeySharder(kv_server::ShardingFunction{/*seed=*/""})}; + kv_server::KeySharder(kv_server::ShardingFunction{/*seed=*/""}), + .blob_prefix_allowlist = BlobPrefixAllowlist("")}; auto maybe_orchestrator = DataOrchestrator::TryCreate(sharded_options); ASSERT_TRUE(maybe_orchestrator.ok()); @@ -659,4 +672,24 @@ TEST_F(DataOrchestratorTest, InitCacheSkipsSnapshotFilesForOtherShards) { EXPECT_TRUE(DataOrchestrator::TryCreate(options_).ok()); } +TEST_F(DataOrchestratorTest, VerifyLoadingDataFromPrefixes) { + for (auto file_type : + {FilePrefix(), FilePrefix()}) { + for (auto prefix : {"", "prefix1", "prefix2"}) { + EXPECT_CALL( + blob_client_, + ListBlobs( + BlobStorageClient::DataLocation{.bucket = "testbucket", + .prefix = prefix}, + AllOf(Field(&BlobStorageClient::ListOptions::start_after, ""), + Field(&BlobStorageClient::ListOptions::prefix, file_type)))) + .WillOnce(Return(std::vector({}))); + } + } + auto options = options_; + options.blob_prefix_allowlist = BlobPrefixAllowlist("prefix1,prefix2"); + auto maybe_orchestrator = DataOrchestrator::TryCreate(options); + ASSERT_TRUE(maybe_orchestrator.ok()); +} + } // namespace diff --git a/components/data_server/request_handler/BUILD.bazel b/components/data_server/request_handler/BUILD.bazel index 82926f77..292869da 100644 --- a/components/data_server/request_handler/BUILD.bazel +++ b/components/data_server/request_handler/BUILD.bazel @@ -32,15 +32,14 @@ cc_library( deps = [ ":get_values_adapter", "//components/data_server/cache", + "//components/util:request_context", "//public:base_types_cc_proto", "//public:constants", "//public/query:get_values_cc_grpc", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -60,8 +59,6 @@ cc_test( "//public/test_util:proto_matcher", "@com_github_grpc_grpc//:grpc++", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -77,20 +74,21 @@ cc_library( ":compression", ":ohttp_server_encryptor", "//components/data_server/cache", + "//components/telemetry:server_definition", "//components/udf:udf_client", + "//components/util:request_context", "//public:api_schema_cc_proto", "//public:base_types_cc_proto", "//public/query/v2:get_values_v2_cc_grpc", - "@com_github_google_glog//:glog", "@com_github_google_quiche//quiche:binary_http_unstable_api", "@com_github_grpc_grpc//:grpc++", "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/telemetry", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) @@ -109,8 +107,8 @@ cc_library( deps = [ "@brotli//:brotlidec", "@brotli//:brotlienc", - "@com_github_google_glog//:glog", "@com_github_google_quiche//quiche:quiche_unstable_api", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", ], ) @@ -129,7 +127,7 @@ cc_test( srcs = ["compression_brotli_test.cc"], deps = [ ":compression", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest_main", ], ) @@ -140,7 +138,6 @@ cc_test( srcs = [ "get_values_v2_handler_test.cc", ], - env = {"GLOG_v": "9"}, linkstatic = True, deps = [ ":get_values_v2_handler", @@ -150,14 +147,12 @@ cc_test( "//components/udf:udf_client", "//public/query/v2:get_values_v2_cc_grpc", "//public/test_util:proto_matcher", - "@com_github_google_glog//:glog", "@com_github_google_quiche//quiche:binary_http_unstable_api", "@com_github_google_quiche//quiche:oblivious_http_unstable_api", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", "@nlohmann_json//:lib", ], ) @@ -178,10 +173,10 @@ cc_library( "//public/applications/pa:response_utils", "//public/query:get_values_cc_grpc", "//public/query/v2:get_values_v2_cc_grpc", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) @@ -201,9 +196,7 @@ cc_test( "//public/test_util:proto_matcher", "@com_github_grpc_grpc//:grpc++", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", "@nlohmann_json//:lib", ], ) @@ -264,7 +257,7 @@ cc_library( "@com_github_google_quiche//quiche:oblivious_http_unstable_api", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:key_fetcher_manager", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:key_fetcher_manager", ], ) @@ -281,7 +274,7 @@ cc_library( "@com_github_google_quiche//quiche:oblivious_http_unstable_api", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:key_fetcher_manager", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:key_fetcher_manager", ], ) @@ -295,6 +288,6 @@ cc_test( ":ohttp_client_encryptor", ":ohttp_server_encryptor", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", ], ) diff --git a/components/data_server/request_handler/compression.cc b/components/data_server/request_handler/compression.cc index 3b3924cc..7defc60e 100644 --- a/components/data_server/request_handler/compression.cc +++ b/components/data_server/request_handler/compression.cc @@ -13,9 +13,9 @@ // limitations under the License. #include "components/data_server/request_handler/compression.h" +#include "absl/log/log.h" #include "components/data_server/request_handler/compression_brotli.h" #include "components/data_server/request_handler/uncompressed.h" -#include "glog/logging.h" #include "quiche/common/quiche_data_writer.h" namespace kv_server { diff --git a/components/data_server/request_handler/compression_brotli.cc b/components/data_server/request_handler/compression_brotli.cc index 31afe4ae..ba902ca4 100644 --- a/components/data_server/request_handler/compression_brotli.cc +++ b/components/data_server/request_handler/compression_brotli.cc @@ -18,10 +18,10 @@ #include #include +#include "absl/log/log.h" #include "absl/strings/str_join.h" #include "brotli/decode.h" #include "brotli/encode.h" -#include "glog/logging.h" #include "quiche/common/quiche_data_writer.h" namespace kv_server { diff --git a/components/data_server/request_handler/compression_brotli_test.cc b/components/data_server/request_handler/compression_brotli_test.cc index 1f474aae..3c5daf5b 100644 --- a/components/data_server/request_handler/compression_brotli_test.cc +++ b/components/data_server/request_handler/compression_brotli_test.cc @@ -17,8 +17,8 @@ #include #include +#include "absl/log/log.h" #include "components/data_server/request_handler/uncompressed.h" -#include "glog/logging.h" #include "gmock/gmock.h" #include "gtest/gtest.h" diff --git a/components/data_server/request_handler/get_values_adapter.cc b/components/data_server/request_handler/get_values_adapter.cc index 2703297a..0c86b5c9 100644 --- a/components/data_server/request_handler/get_values_adapter.cc +++ b/components/data_server/request_handler/get_values_adapter.cc @@ -21,13 +21,13 @@ #include #include +#include "absl/log/log.h" #include "components/data_server/request_handler/v2_response_data.pb.h" -#include "glog/logging.h" #include "google/protobuf/util/json_util.h" #include "public/api_schema.pb.h" #include "public/applications/pa/api_overlay.pb.h" #include "public/applications/pa/response_utils.h" -#include "src/cpp/util/status_macro/status_macros.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server { namespace { diff --git a/components/data_server/request_handler/get_values_adapter_test.cc b/components/data_server/request_handler/get_values_adapter_test.cc index aba170e7..15d77b47 100644 --- a/components/data_server/request_handler/get_values_adapter_test.cc +++ b/components/data_server/request_handler/get_values_adapter_test.cc @@ -26,16 +26,13 @@ #include "public/applications/pa/api_overlay.pb.h" #include "public/applications/pa/response_utils.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { using google::protobuf::TextFormat; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::Return; @@ -54,8 +51,9 @@ class GetValuesAdapterTest : public ::testing::Test { protected: void SetUp() override { v2_handler_ = std::make_unique( - mock_udf_client_, mock_metrics_recorder_, fake_key_fetcher_manager_); + mock_udf_client_, fake_key_fetcher_manager_); get_values_adapter_ = GetValuesAdapter::Create(std::move(v2_handler_)); + InitMetricsContextMap(); } privacy_sandbox::server_common::FakeKeyFetcherManager @@ -63,7 +61,6 @@ class GetValuesAdapterTest : public ::testing::Test { std::unique_ptr get_values_adapter_; std::unique_ptr v2_handler_; MockUdfClient mock_udf_client_; - MockMetricsRecorder mock_metrics_recorder_; }; TEST_F(GetValuesAdapterTest, EmptyRequestReturnsEmptyResponse) { @@ -71,8 +68,9 @@ TEST_F(GetValuesAdapterTest, EmptyRequestReturnsEmptyResponse) { TextFormat::ParseFromString(kEmptyMetadata, &udf_metadata); nlohmann::json output = nlohmann::json::parse(R"({"keyGroupOutputs": {}})"); - EXPECT_CALL(mock_udf_client_, - ExecuteCode(EqualsProto(udf_metadata), testing::IsEmpty())) + EXPECT_CALL( + mock_udf_client_, + ExecuteCode(testing::_, EqualsProto(udf_metadata), testing::IsEmpty())) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -133,7 +131,7 @@ data { )", &key_group_outputs); EXPECT_CALL(mock_udf_client_, - ExecuteCode(EqualsProto(udf_metadata), + ExecuteCode(testing::_, EqualsProto(udf_metadata), testing::ElementsAre(EqualsProto(arg)))) .WillOnce(Return( application_pa::KeyGroupOutputsToJson(key_group_outputs).value())); @@ -236,7 +234,7 @@ data { &key_group_outputs); EXPECT_CALL( mock_udf_client_, - ExecuteCode(EqualsProto(udf_metadata), + ExecuteCode(testing::_, EqualsProto(udf_metadata), testing::ElementsAre(EqualsProto(arg1), EqualsProto(arg2)))) .WillOnce(Return( application_pa::KeyGroupOutputsToJson(key_group_outputs).value())); @@ -265,7 +263,7 @@ TEST_F(GetValuesAdapterTest, V2ResponseIsNullReturnsError) { nlohmann::json output = R"({ "keyGroupOutpus": [] })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -286,7 +284,7 @@ TEST_F(GetValuesAdapterTest, KeyGroupOutputWithEmptyKVsReturnsOk) { }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -307,7 +305,7 @@ TEST_F(GetValuesAdapterTest, KeyGroupOutputWithInvalidNamespaceTagIsIgnored) { }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -328,7 +326,7 @@ TEST_F(GetValuesAdapterTest, KeyGroupOutputWithNoCustomTagIsIgnored) { }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -349,7 +347,7 @@ TEST_F(GetValuesAdapterTest, KeyGroupOutputWithNoNamespaceTagIsIgnored) { }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -375,7 +373,7 @@ TEST_F(GetValuesAdapterTest, }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -409,7 +407,7 @@ TEST_F(GetValuesAdapterTest, KeyGroupOutputHasDifferentValueTypesReturnsOk) { }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; @@ -489,7 +487,7 @@ TEST_F(GetValuesAdapterTest, ValueWithStatusSuccess) { }], "udfOutputApiVersion": 1 })"_json; - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _)) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, _)) .WillOnce(Return(output.dump())); v1::GetValuesRequest v1_request; diff --git a/components/data_server/request_handler/get_values_handler.cc b/components/data_server/request_handler/get_values_handler.cc index 319a5fed..f8cce490 100644 --- a/components/data_server/request_handler/get_values_handler.cc +++ b/components/data_server/request_handler/get_values_handler.cc @@ -19,20 +19,16 @@ #include #include +#include "absl/log/log.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" #include "components/data_server/request_handler/get_values_adapter.h" -#include "glog/logging.h" #include "grpcpp/grpcpp.h" #include "public/constants.h" #include "public/query/get_values.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" #include "src/google/protobuf/message.h" #include "src/google/protobuf/struct.pb.h" - -constexpr char* kCacheKeyHit = "CacheKeyHit"; -constexpr char* kCacheKeyMiss = "CacheKeyMiss"; +#include "src/telemetry/telemetry.h" namespace kv_server { namespace { @@ -40,8 +36,6 @@ using google::protobuf::RepeatedPtrField; using google::protobuf::Struct; using google::protobuf::Value; using grpc::StatusCode; -using privacy_sandbox::server_common::GetTracer; -using privacy_sandbox::server_common::MetricsRecorder; using v1::GetValuesRequest; using v1::GetValuesResponse; using v1::KeyValueService; @@ -58,25 +52,25 @@ absl::flat_hash_set GetKeys( return key_list; } -void ProcessKeys(const RepeatedPtrField& keys, const Cache& cache, - MetricsRecorder& metrics_recorder, - google::protobuf::Map& - result_struct) { +void ProcessKeys( + const RequestContext& request_context, + const RepeatedPtrField& keys, const Cache& cache, + google::protobuf::Map& result_struct, + bool add_missing_keys_v1) { if (keys.empty()) return; auto actual_keys = GetKeys(keys); - auto kv_pairs = cache.GetKeyValuePairs(actual_keys); - - if (kv_pairs.empty()) - metrics_recorder.IncrementEventCounter(kCacheKeyMiss); - else - metrics_recorder.IncrementEventCounter(kCacheKeyHit); + auto kv_pairs = cache.GetKeyValuePairs(request_context, actual_keys); + // TODO(b/326118416): Record cache hit and miss metrics for (const auto& key : actual_keys) { v1::V1SingleLookupResult result; const auto key_iter = kv_pairs.find(key); if (key_iter == kv_pairs.end()) { - auto status = result.mutable_status(); - status->set_code(static_cast(absl::StatusCode::kNotFound)); - status->set_message("Key not found"); + if (add_missing_keys_v1) { + auto status = result.mutable_status(); + status->set_code(static_cast(absl::StatusCode::kNotFound)); + status->set_message("Key not found"); + result_struct[key] = std::move(result); + } } else { Value value_proto; absl::Status status = google::protobuf::util::JsonStringToMessage( @@ -90,40 +84,41 @@ void ProcessKeys(const RepeatedPtrField& keys, const Cache& cache, value.set_string_value(std::move(key_iter->second)); *result.mutable_value() = std::move(value); } + result_struct[key] = std::move(result); } - result_struct[key] = std::move(result); } } } // namespace -grpc::Status GetValuesHandler::GetValues(const GetValuesRequest& request, +grpc::Status GetValuesHandler::GetValues(const RequestContext& request_context, + const GetValuesRequest& request, GetValuesResponse* response) const { if (use_v2_) { VLOG(5) << "Using V2 adapter for " << request.DebugString(); return adapter_.CallV2Handler(request, *response); } - if (!request.kv_internal().empty()) { VLOG(5) << "Processing kv_internal for " << request.DebugString(); - ProcessKeys(request.kv_internal(), cache_, metrics_recorder_, - *response->mutable_kv_internal()); + ProcessKeys(request_context, request.kv_internal(), cache_, + *response->mutable_kv_internal(), add_missing_keys_v1_); } if (!request.keys().empty()) { VLOG(5) << "Processing keys for " << request.DebugString(); - ProcessKeys(request.keys(), cache_, metrics_recorder_, - *response->mutable_keys()); + ProcessKeys(request_context, request.keys(), cache_, + *response->mutable_keys(), add_missing_keys_v1_); } if (!request.render_urls().empty()) { VLOG(5) << "Processing render_urls for " << request.DebugString(); - ProcessKeys(request.render_urls(), cache_, metrics_recorder_, - *response->mutable_render_urls()); + ProcessKeys(request_context, request.render_urls(), cache_, + *response->mutable_render_urls(), add_missing_keys_v1_); } if (!request.ad_component_render_urls().empty()) { VLOG(5) << "Processing ad_component_render_urls for " << request.DebugString(); - ProcessKeys(request.ad_component_render_urls(), cache_, metrics_recorder_, - *response->mutable_ad_component_render_urls()); + ProcessKeys(request_context, request.ad_component_render_urls(), cache_, + *response->mutable_ad_component_render_urls(), + add_missing_keys_v1_); } return grpc::Status::OK; } diff --git a/components/data_server/request_handler/get_values_handler.h b/components/data_server/request_handler/get_values_handler.h index 880eed06..ef2be6cf 100644 --- a/components/data_server/request_handler/get_values_handler.h +++ b/components/data_server/request_handler/get_values_handler.h @@ -24,7 +24,6 @@ #include "components/data_server/request_handler/get_values_adapter.h" #include "grpcpp/grpcpp.h" #include "public/query/get_values.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" #include "src/google/protobuf/struct.pb.h" namespace kv_server { @@ -33,26 +32,24 @@ namespace kv_server { // See the Service proto definition for details. class GetValuesHandler { public: - explicit GetValuesHandler( - const Cache& cache, const GetValuesAdapter& adapter, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder, - bool use_v2) + explicit GetValuesHandler(const Cache& cache, const GetValuesAdapter& adapter, + bool use_v2, bool add_missing_keys_v1 = true) : cache_(std::move(cache)), adapter_(std::move(adapter)), - metrics_recorder_(metrics_recorder), - use_v2_(use_v2) {} + use_v2_(use_v2), + add_missing_keys_v1_(add_missing_keys_v1) {} // TODO: Implement hostname, ad/render url lookups. - grpc::Status GetValues(const v1::GetValuesRequest& request, + grpc::Status GetValues(const RequestContext& request_context, + const v1::GetValuesRequest& request, v1::GetValuesResponse* response) const; private: const Cache& cache_; const GetValuesAdapter& adapter_; - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; - // If true, routes requests through V2 (UDF). Otherwise, calls cache. const bool use_v2_; + const bool add_missing_keys_v1_; }; } // namespace kv_server diff --git a/components/data_server/request_handler/get_values_handler_test.cc b/components/data_server/request_handler/get_values_handler_test.cc index 3197839b..76f0d40a 100644 --- a/components/data_server/request_handler/get_values_handler_test.cc +++ b/components/data_server/request_handler/get_values_handler_test.cc @@ -28,15 +28,12 @@ #include "grpcpp/grpcpp.h" #include "gtest/gtest.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { using google::protobuf::TextFormat; using grpc::StatusCode; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::DoAll; using testing::Return; @@ -48,13 +45,21 @@ using v1::GetValuesResponse; class GetValuesHandlerTest : public ::testing::Test { protected: + GetValuesHandlerTest() { + InitMetricsContextMap(); + scope_metrics_context_ = std::make_unique(); + request_context_ = + std::make_unique(*scope_metrics_context_); + } MockCache mock_cache_; - MockMetricsRecorder mock_metrics_recorder_; MockGetValuesAdapter mock_get_values_adapter_; + RequestContext& GetRequestContext() { return *request_context_; } + std::unique_ptr scope_metrics_context_; + std::unique_ptr request_context_; }; TEST_F(GetValuesHandlerTest, ReturnsExistingKeyTwice) { - EXPECT_CALL(mock_cache_, GetKeyValuePairs(UnorderedElementsAre("my_key"))) + EXPECT_CALL(mock_cache_, GetKeyValuePairs(_, UnorderedElementsAre("my_key"))) .Times(2) .WillRepeatedly(Return(absl::flat_hash_map{ {"my_key", "my_value"}})); @@ -62,9 +67,9 @@ TEST_F(GetValuesHandlerTest, ReturnsExistingKeyTwice) { request.add_keys("my_key"); GetValuesResponse response; GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, - mock_metrics_recorder_, /*use_v2=*/false); - const auto result = handler.GetValues(request, &response); + const auto result = + handler.GetValues(GetRequestContext(), request, &response); ASSERT_TRUE(result.ok()) << "code: " << result.error_code() << ", msg: " << result.error_message(); @@ -77,13 +82,13 @@ TEST_F(GetValuesHandlerTest, ReturnsExistingKeyTwice) { &expected); EXPECT_THAT(response, EqualsProto(expected)); - ASSERT_TRUE(handler.GetValues(request, &response).ok()); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); EXPECT_THAT(response, EqualsProto(expected)); } TEST_F(GetValuesHandlerTest, RepeatedKeys) { EXPECT_CALL(mock_cache_, - GetKeyValuePairs(UnorderedElementsAre("key1", "key2", "key3"))) + GetKeyValuePairs(_, UnorderedElementsAre("key1", "key2", "key3"))) .Times(1) .WillRepeatedly(Return( absl::flat_hash_map{{"key1", "value1"}})); @@ -91,9 +96,8 @@ TEST_F(GetValuesHandlerTest, RepeatedKeys) { request.add_keys("key1,key2,key3"); GetValuesResponse response; GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, - mock_metrics_recorder_, /*use_v2=*/false); - ASSERT_TRUE(handler.GetValues(request, &response).ok()); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); GetValuesResponse expected; TextFormat::ParseFromString( @@ -115,9 +119,34 @@ TEST_F(GetValuesHandlerTest, RepeatedKeys) { EXPECT_THAT(response, EqualsProto(expected)); } +TEST_F(GetValuesHandlerTest, RepeatedKeysSkipEmpty) { + EXPECT_CALL(mock_cache_, + GetKeyValuePairs(_, UnorderedElementsAre("key1", "key2", "key3"))) + .Times(1) + .WillRepeatedly(Return( + absl::flat_hash_map{{"key1", "value1"}})); + GetValuesRequest request; + request.add_keys("key1,key2,key3"); + GetValuesResponse response; + GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, + /*use_v2=*/false, /*add_missing_keys_v1=*/false); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); + + GetValuesResponse expected; + TextFormat::ParseFromString( + R"pb( + keys { + key: "key1" + value { value { string_value: "value1" } } + } + )pb", + &expected); + EXPECT_THAT(response, EqualsProto(expected)); +} + TEST_F(GetValuesHandlerTest, ReturnsMultipleExistingKeysSameNamespace) { EXPECT_CALL(mock_cache_, - GetKeyValuePairs(UnorderedElementsAre("key1", "key2"))) + GetKeyValuePairs(_, UnorderedElementsAre("key1", "key2"))) .Times(1) .WillOnce(Return(absl::flat_hash_map{ {"key1", "value1"}, {"key2", "value2"}})); @@ -126,9 +155,8 @@ TEST_F(GetValuesHandlerTest, ReturnsMultipleExistingKeysSameNamespace) { request.add_keys("key2"); GetValuesResponse response; GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, - mock_metrics_recorder_, /*use_v2=*/false); - ASSERT_TRUE(handler.GetValues(request, &response).ok()); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); GetValuesResponse expected; TextFormat::ParseFromString(R"pb( @@ -145,11 +173,11 @@ TEST_F(GetValuesHandlerTest, ReturnsMultipleExistingKeysSameNamespace) { } TEST_F(GetValuesHandlerTest, ReturnsMultipleExistingKeysDifferentNamespace) { - EXPECT_CALL(mock_cache_, GetKeyValuePairs(UnorderedElementsAre("key1"))) + EXPECT_CALL(mock_cache_, GetKeyValuePairs(_, UnorderedElementsAre("key1"))) .Times(1) .WillOnce(Return( absl::flat_hash_map{{"key1", "value1"}})); - EXPECT_CALL(mock_cache_, GetKeyValuePairs(UnorderedElementsAre("key2"))) + EXPECT_CALL(mock_cache_, GetKeyValuePairs(_, UnorderedElementsAre("key2"))) .Times(1) .WillOnce(Return( absl::flat_hash_map{{"key2", "value2"}})); @@ -158,9 +186,8 @@ TEST_F(GetValuesHandlerTest, ReturnsMultipleExistingKeysDifferentNamespace) { request.add_ad_component_render_urls("key2"); GetValuesResponse response; GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, - mock_metrics_recorder_, /*use_v2=*/false); - ASSERT_TRUE(handler.GetValues(request, &response).ok()); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); GetValuesResponse expected; TextFormat::ParseFromString(R"pb(render_urls { @@ -252,7 +279,7 @@ TEST_F(GetValuesHandlerTest, TestResponseOnDifferentValueFormats) { })json"; EXPECT_CALL(mock_cache_, - GetKeyValuePairs(UnorderedElementsAre("key1", "key2", "key3"))) + GetKeyValuePairs(_, UnorderedElementsAre("key1", "key2", "key3"))) .Times(1) .WillOnce(Return(absl::flat_hash_map{ {"key1", value1}, {"key2", value2}, {"key3", value3}})); @@ -263,9 +290,8 @@ TEST_F(GetValuesHandlerTest, TestResponseOnDifferentValueFormats) { request.add_keys("key3"); GetValuesResponse response; GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, - mock_metrics_recorder_, /*use_v2=*/false); - ASSERT_TRUE(handler.GetValues(request, &response).ok()); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); GetValuesResponse expected_from_pb; TextFormat::ParseFromString(response_pb_string, &expected_from_pb); EXPECT_THAT(response, EqualsProto(expected_from_pb)); @@ -292,9 +318,8 @@ TEST_F(GetValuesHandlerTest, CallsV2Adapter) { request.add_keys("key1"); GetValuesResponse response; GetValuesHandler handler(mock_cache_, mock_get_values_adapter_, - mock_metrics_recorder_, /*use_v2=*/true); - ASSERT_TRUE(handler.GetValues(request, &response).ok()); + ASSERT_TRUE(handler.GetValues(GetRequestContext(), request, &response).ok()); EXPECT_THAT(response, EqualsProto(adapter_response)); } diff --git a/components/data_server/request_handler/get_values_v2_handler.cc b/components/data_server/request_handler/get_values_v2_handler.cc index 3b33aab2..06d3b5c7 100644 --- a/components/data_server/request_handler/get_values_v2_handler.cc +++ b/components/data_server/request_handler/get_values_v2_handler.cc @@ -21,10 +21,11 @@ #include #include "absl/algorithm/container.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "components/data_server/request_handler/ohttp_server_encryptor.h" -#include "glog/logging.h" +#include "components/telemetry/server_definition.h" #include "google/protobuf/util/json_util.h" #include "grpcpp/grpcpp.h" #include "public/base_types.pb.h" @@ -33,11 +34,8 @@ #include "quiche/binary_http/binary_http_message.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" #include "quiche/oblivious_http/oblivious_http_gateway.h" -#include "src/cpp/telemetry/telemetry.h" -#include "src/cpp/util/status_macro/status_macros.h" - -constexpr char* kCacheKeyV2Hit = "CacheKeyHit"; -constexpr char* kCacheKeyV2Miss = "CacheKeyMiss"; +#include "src/telemetry/telemetry.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server { namespace { @@ -75,16 +73,35 @@ grpc::Status GetValuesV2Handler::GetValuesHttp( GetValuesHttp(request.raw_body().data(), *response->mutable_data())); } -absl::Status GetValuesV2Handler::GetValuesHttp( - std::string_view request, std::string& json_response) const { +absl::Status GetValuesV2Handler::GetValuesHttp(std::string_view request, + std::string& response, + ContentType content_type) const { v2::GetValuesRequest request_proto; - PS_RETURN_IF_ERROR( - google::protobuf::util::JsonStringToMessage(request, &request_proto)); + if (content_type == ContentType::kJson) { + PS_RETURN_IF_ERROR( + google::protobuf::util::JsonStringToMessage(request, &request_proto)); + } else { // proto + if (!request_proto.ParseFromString(request)) { + auto error_message = + "Cannot parse request as a valid serilized proto object."; + VLOG(4) << error_message; + return absl::InvalidArgumentError(error_message); + } + } VLOG(9) << "Converted the http request to proto: " << request_proto.DebugString(); v2::GetValuesResponse response_proto; PS_RETURN_IF_ERROR(GetValues(request_proto, &response_proto)); - return MessageToJsonString(response_proto, &json_response); + if (content_type == ContentType::kJson) { + return MessageToJsonString(response_proto, &response); + } + // content_type == proto + if (!response_proto.SerializeToString(&response)) { + auto error_message = "Cannot serialize the response as a proto."; + VLOG(4) << error_message; + return absl::InvalidArgumentError(error_message); + } + return absl::OkStatus(); } grpc::Status GetValuesV2Handler::BinaryHttpGetValues( @@ -94,22 +111,38 @@ grpc::Status GetValuesV2Handler::BinaryHttpGetValues( *response->mutable_data())); } +GetValuesV2Handler::ContentType GetValuesV2Handler::GetContentType( + const quiche::BinaryHttpRequest& deserialized_req) const { + for (const auto& header : deserialized_req.GetHeaderFields()) { + if (absl::AsciiStrToLower(header.name) == kContentTypeHeader && + absl::AsciiStrToLower(header.value) == + kContentEncodingProtoHeaderValue) { + return ContentType::kProto; + } + } + return ContentType::kJson; +} + absl::StatusOr GetValuesV2Handler::BuildSuccessfulGetValuesBhttpResponse( std::string_view bhttp_request_body) const { VLOG(9) << "Handling the binary http layer"; - PS_ASSIGN_OR_RETURN(quiche::BinaryHttpRequest maybe_deserialized_req, + PS_ASSIGN_OR_RETURN(quiche::BinaryHttpRequest deserialized_req, quiche::BinaryHttpRequest::Create(bhttp_request_body), _ << "Failed to deserialize binary http request"); - VLOG(3) << "BinaryHttpGetValues request: " - << maybe_deserialized_req.DebugString(); - - std::string json_response; + VLOG(3) << "BinaryHttpGetValues request: " << deserialized_req.DebugString(); + std::string response; + auto content_type = GetContentType(deserialized_req); PS_RETURN_IF_ERROR( - GetValuesHttp(maybe_deserialized_req.body(), json_response)); - + GetValuesHttp(deserialized_req.body(), response, content_type)); quiche::BinaryHttpResponse bhttp_response(200); - bhttp_response.set_body(std::move(json_response)); + if (content_type == ContentType::kProto) { + bhttp_response.AddHeaderField({ + .name = std::string(kContentTypeHeader), + .value = std::string(kContentEncodingProtoHeaderValue), + }); + } + bhttp_response.set_body(std::move(response)); return bhttp_response; } @@ -159,6 +192,7 @@ grpc::Status GetValuesV2Handler::ObliviousGetValues( } void GetValuesV2Handler::ProcessOnePartition( + RequestContext request_context, const google::protobuf::Struct& req_metadata, const v2::RequestPartition& req_partition, v2::ResponsePartition& resp_partition) const { @@ -166,7 +200,8 @@ void GetValuesV2Handler::ProcessOnePartition( UDFExecutionMetadata udf_metadata; *udf_metadata.mutable_request_metadata() = req_metadata; const auto maybe_output_string = udf_client_.ExecuteCode( - std::move(udf_metadata), req_partition.arguments()); + std::move(request_context), std::move(udf_metadata), + req_partition.arguments()); if (!maybe_output_string.ok()) { resp_partition.mutable_status()->set_code( static_cast(maybe_output_string.status().code())); @@ -181,8 +216,11 @@ void GetValuesV2Handler::ProcessOnePartition( grpc::Status GetValuesV2Handler::GetValues( const v2::GetValuesRequest& request, v2::GetValuesResponse* response) const { + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); if (request.partitions().size() == 1) { - ProcessOnePartition(request.metadata(), request.partitions(0), + ProcessOnePartition(std::move(request_context), request.metadata(), + request.partitions(0), *response->mutable_single_partition()); return grpc::Status::OK; } diff --git a/components/data_server/request_handler/get_values_v2_handler.h b/components/data_server/request_handler/get_values_v2_handler.h index 32960e29..52f1c0d6 100644 --- a/components/data_server/request_handler/get_values_v2_handler.h +++ b/components/data_server/request_handler/get_values_v2_handler.h @@ -26,15 +26,26 @@ #include "absl/strings/escaping.h" #include "components/data_server/cache/cache.h" #include "components/data_server/request_handler/compression.h" +#include "components/telemetry/server_definition.h" #include "components/udf/udf_client.h" +#include "components/util/request_context.h" #include "grpcpp/grpcpp.h" #include "public/query/v2/get_values_v2.grpc.pb.h" #include "quiche/binary_http/binary_http_message.h" -#include "src/cpp/encryption/key_fetcher/src/key_fetcher_manager.h" -#include "src/cpp/telemetry/metrics_recorder.h" +#include "src/encryption/key_fetcher/key_fetcher_manager.h" namespace kv_server { +// Content Type Header Name. Can be set for bhttp request to proto or json +// values below. +inline constexpr std::string_view kContentTypeHeader = "content-type"; +// Protobuf Content Type Header Value. +inline constexpr std::string_view kContentEncodingProtoHeaderValue = + "application/protobuf"; +// Json Content Type Header Value. +inline constexpr std::string_view kContentEncodingJsonHeaderValue = + "application/json"; + // Handles the request family of *GetValues. // See the Service proto definition for details. class GetValuesV2Handler { @@ -42,14 +53,12 @@ class GetValuesV2Handler { // Accepts a functor to create compression blob builder for testing purposes. explicit GetValuesV2Handler( const UdfClient& udf_client, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder, privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager, std::function create_compression_group_concatenator = &CompressionGroupConcatenator::Create) : udf_client_(udf_client), - metrics_recorder_(metrics_recorder), create_compression_group_concatenator_( std::move(create_compression_group_concatenator)), key_fetcher_manager_(key_fetcher_manager) {} @@ -81,8 +90,16 @@ class GetValuesV2Handler { google::api::HttpBody* response) const; private: - absl::Status GetValuesHttp(std::string_view request, - std::string& json_response) const; + enum class ContentType { + kJson = 0, + kProto, + }; + ContentType GetContentType( + const quiche::BinaryHttpRequest& deserialized_req) const; + + absl::Status GetValuesHttp( + std::string_view request, std::string& json_response, + ContentType content_type = ContentType::kJson) const; // On success, returns a BinaryHttpResponse with a successful response. The // reason that this is a separate function is so that the error status @@ -99,14 +116,14 @@ class GetValuesV2Handler { std::string& response) const; // Invokes UDF to process one partition. - void ProcessOnePartition(const google::protobuf::Struct& req_metadata, + void ProcessOnePartition(RequestContext request_context, + const google::protobuf::Struct& req_metadata, const v2::RequestPartition& req_partition, v2::ResponsePartition& resp_partition) const; const UdfClient& udf_client_; std::function create_compression_group_concatenator_; - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager_; }; diff --git a/components/data_server/request_handler/get_values_v2_handler_test.cc b/components/data_server/request_handler/get_values_v2_handler_test.cc index f351bb7c..9c7376f7 100644 --- a/components/data_server/request_handler/get_values_v2_handler_test.cc +++ b/components/data_server/request_handler/get_values_v2_handler_test.cc @@ -19,10 +19,10 @@ #include #include +#include "absl/log/log.h" #include "components/data_server/cache/cache.h" #include "components/data_server/cache/mocks.h" #include "components/udf/mocks.h" -#include "glog/logging.h" #include "gmock/gmock.h" #include "google/protobuf/text_format.h" #include "grpcpp/grpcpp.h" @@ -33,16 +33,13 @@ #include "quiche/binary_http/binary_http_message.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" #include "quiche/oblivious_http/oblivious_http_client.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { using google::protobuf::TextFormat; using grpc::StatusCode; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::Return; using testing::ReturnRef; @@ -57,13 +54,25 @@ enum class ProtocolType { kObliviousHttp, }; +struct TestingParameters { + ProtocolType protocol_type; + const std::string_view content_type; +}; + class GetValuesHandlerTest : public ::testing::Test, - public ::testing::WithParamInterface { + public ::testing::WithParamInterface { protected: + void SetUp() override { InitMetricsContextMap(); } template bool IsUsing() { - return GetParam() == protocol_type; + auto param = GetParam(); + return param.protocol_type == protocol_type; + } + + bool IsProtobufContent() { + auto param = GetParam(); + return param.content_type == kContentEncodingProtoHeaderValue; } class PlainRequest { @@ -85,8 +94,15 @@ class GetValuesHandlerTest class BHTTPRequest { public: - explicit BHTTPRequest(PlainRequest plain_request) { + explicit BHTTPRequest(PlainRequest plain_request, + bool is_protobuf_content) { quiche::BinaryHttpRequest req_bhttp_layer({}); + if (is_protobuf_content) { + req_bhttp_layer.AddHeaderField({ + .name = std::string(kContentTypeHeader), + .value = std::string(kContentEncodingProtoHeaderValue), + }); + } req_bhttp_layer.set_body(plain_request.RequestBody()); auto maybe_serialized = req_bhttp_layer.Serialize(); EXPECT_TRUE(maybe_serialized.ok()); @@ -119,16 +135,32 @@ class GetValuesHandlerTest return maybe_res_bhttp_layer->status_code(); } - std::string Unwrap() const { + std::string Unwrap(bool is_protobuf_content) const { const absl::StatusOr maybe_res_bhttp_layer = quiche::BinaryHttpResponse::Create(response_.data()); EXPECT_TRUE(maybe_res_bhttp_layer.ok()) << "quiche::BinaryHttpResponse::Create failed: " << maybe_res_bhttp_layer.status(); + if (maybe_res_bhttp_layer->status_code() == 200 & is_protobuf_content) { + EXPECT_TRUE(HasHeader(*maybe_res_bhttp_layer, kContentTypeHeader, + kContentEncodingProtoHeaderValue)); + } return std::string(maybe_res_bhttp_layer->body()); } private: + bool HasHeader(const quiche::BinaryHttpResponse& response, + const std::string_view header_key, + const std::string_view header_value) const { + for (const auto& header : response.GetHeaderFields()) { + if (absl::AsciiStrToLower(header.name) == header_key && + absl::AsciiStrToLower(header.value) == header_value) { + return true; + } + } + return false; + } + google::api::HttpBody response_; }; @@ -176,7 +208,7 @@ class GetValuesHandlerTest // ../encryption/key_fetcher/src/fake_key_fetcher_manager.h uint8_t key_id = 64; auto maybe_config = quiche::ObliviousHttpHeaderKeyConfig::Create( - key_id, 0x0020, 0x0001, 0x0001); + key_id, kKEMParameter, kKDFParameter, kAEADParameter); EXPECT_TRUE(maybe_config.ok()); auto client = @@ -212,7 +244,7 @@ class GetValuesHandlerTest return handler->GetValuesHttp(plain_request.Build(), response); } - BHTTPRequest bhttp_request(std::move(plain_request)); + BHTTPRequest bhttp_request(std::move(plain_request), IsProtobufContent()); BHTTPResponse bresponse; if (IsUsing()) { @@ -237,20 +269,38 @@ class GetValuesHandlerTest *bhttp_response_code = bresponse.ResponseCode(); } - response->set_data(bresponse.Unwrap()); + response->set_data(bresponse.Unwrap(IsProtobufContent())); return grpc::Status::OK; } MockUdfClient mock_udf_client_; - MockMetricsRecorder mock_metrics_recorder_; privacy_sandbox::server_common::FakeKeyFetcherManager fake_key_fetcher_manager_; }; -INSTANTIATE_TEST_SUITE_P(GetValuesHandlerTest, GetValuesHandlerTest, - testing::Values(ProtocolType::kPlain, - ProtocolType::kBinaryHttp, - ProtocolType::kObliviousHttp)); +INSTANTIATE_TEST_SUITE_P( + GetValuesHandlerTest, GetValuesHandlerTest, + testing::Values( + TestingParameters{ + .protocol_type = ProtocolType::kPlain, + .content_type = kContentEncodingJsonHeaderValue, + }, + TestingParameters{ + .protocol_type = ProtocolType::kBinaryHttp, + .content_type = kContentEncodingJsonHeaderValue, + }, + TestingParameters{ + .protocol_type = ProtocolType::kObliviousHttp, + .content_type = kContentEncodingJsonHeaderValue, + }, + TestingParameters{ + .protocol_type = ProtocolType::kBinaryHttp, + .content_type = kContentEncodingProtoHeaderValue, + }, + TestingParameters{ + .protocol_type = ProtocolType::kObliviousHttp, + .content_type = kContentEncodingProtoHeaderValue, + })); TEST_P(GetValuesHandlerTest, Success) { UDFExecutionMetadata udf_metadata; @@ -326,11 +376,11 @@ data { )"); EXPECT_CALL( mock_udf_client_, - ExecuteCode(EqualsProto(udf_metadata), + ExecuteCode(_, EqualsProto(udf_metadata), testing::ElementsAre(EqualsProto(arg1), EqualsProto(arg2)))) .WillOnce(Return(output.dump())); - const std::string core_request_body = R"( + std::string core_request_body = R"( { "metadata": { "hostname": "example.com" @@ -365,9 +415,16 @@ data { )"; google::api::HttpBody response; - GetValuesV2Handler handler(mock_udf_client_, mock_metrics_recorder_, - fake_key_fetcher_manager_); + GetValuesV2Handler handler(mock_udf_client_, fake_key_fetcher_manager_); int16_t bhttp_response_code = 0; + if (IsProtobufContent()) { + v2::GetValuesRequest request_proto; + ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(core_request_body, + &request_proto) + .ok()); + ASSERT_TRUE(request_proto.SerializeToString(&core_request_body)); + } + const auto result = GetValuesBasedOnProtocol(core_request_body, &response, &bhttp_response_code, &handler); ASSERT_EQ(bhttp_response_code, 200); @@ -377,24 +434,34 @@ data { v2::GetValuesResponse actual_response, expected_response; expected_response.mutable_single_partition()->set_string_output( output.dump()); - - ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(response.data(), - &actual_response) - .ok()); + if (IsProtobufContent()) { + ASSERT_TRUE(actual_response.ParseFromString(response.data())); + } else { + ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(response.data(), + &actual_response) + .ok()); + } EXPECT_THAT(actual_response, EqualsProto(expected_response)); } TEST_P(GetValuesHandlerTest, NoPartition) { - const std::string core_request_body = R"( + std::string core_request_body = R"( { "metadata": { "hostname": "example.com" } })"; google::api::HttpBody response; - GetValuesV2Handler handler(mock_udf_client_, mock_metrics_recorder_, - fake_key_fetcher_manager_); + GetValuesV2Handler handler(mock_udf_client_, fake_key_fetcher_manager_); int16_t bhttp_response_code = 0; + + if (IsProtobufContent()) { + v2::GetValuesRequest request_proto; + ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(core_request_body, + &request_proto) + .ok()); + ASSERT_TRUE(request_proto.SerializeToString(&core_request_body)); + } const auto result = GetValuesBasedOnProtocol(core_request_body, &response, &bhttp_response_code, &handler); if (IsUsing()) { @@ -407,10 +474,10 @@ TEST_P(GetValuesHandlerTest, NoPartition) { } TEST_P(GetValuesHandlerTest, UdfFailureForOnePartition) { - EXPECT_CALL(mock_udf_client_, ExecuteCode(_, testing::IsEmpty())) + EXPECT_CALL(mock_udf_client_, ExecuteCode(_, _, testing::IsEmpty())) .WillOnce(Return(absl::InternalError("UDF execution error"))); - const std::string core_request_body = R"( + std::string core_request_body = R"( { "partitions": [ { @@ -421,9 +488,17 @@ TEST_P(GetValuesHandlerTest, UdfFailureForOnePartition) { )"; google::api::HttpBody response; - GetValuesV2Handler handler(mock_udf_client_, mock_metrics_recorder_, - fake_key_fetcher_manager_); + GetValuesV2Handler handler(mock_udf_client_, fake_key_fetcher_manager_); int16_t bhttp_response_code = 0; + + if (IsProtobufContent()) { + v2::GetValuesRequest request_proto; + ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(core_request_body, + &request_proto) + .ok()); + ASSERT_TRUE(request_proto.SerializeToString(&core_request_body)); + } + const auto result = GetValuesBasedOnProtocol(core_request_body, &response, &bhttp_response_code, &handler); ASSERT_EQ(bhttp_response_code, 200); @@ -436,9 +511,13 @@ TEST_P(GetValuesHandlerTest, UdfFailureForOnePartition) { resp_status->set_code(13); resp_status->set_message("UDF execution error"); - ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(response.data(), - &actual_response) - .ok()); + if (IsProtobufContent()) { + ASSERT_TRUE(actual_response.ParseFromString(response.data())); + } else { + ASSERT_TRUE(google::protobuf::util::JsonStringToMessage(response.data(), + &actual_response) + .ok()); + } EXPECT_THAT(actual_response, EqualsProto(expected_response)); } @@ -450,11 +529,11 @@ TEST_F(GetValuesHandlerTest, PureGRPCTest) { arguments { data { string_value: "ECHO" } } })pb", &req); - GetValuesV2Handler handler(mock_udf_client_, mock_metrics_recorder_, - fake_key_fetcher_manager_); + GetValuesV2Handler handler(mock_udf_client_, fake_key_fetcher_manager_); EXPECT_CALL(mock_udf_client_, - ExecuteCode(testing::_, testing::ElementsAre(EqualsProto( - req.partitions(0).arguments(0))))) + ExecuteCode(_, _, + testing::ElementsAre( + EqualsProto(req.partitions(0).arguments(0))))) .WillOnce(Return("ECHO")); v2::GetValuesResponse resp; const auto result = handler.GetValues(req, &resp); @@ -475,11 +554,11 @@ TEST_F(GetValuesHandlerTest, PureGRPCTestFailure) { arguments { data { string_value: "ECHO" } } })pb", &req); - GetValuesV2Handler handler(mock_udf_client_, mock_metrics_recorder_, - fake_key_fetcher_manager_); + GetValuesV2Handler handler(mock_udf_client_, fake_key_fetcher_manager_); EXPECT_CALL(mock_udf_client_, - ExecuteCode(testing::_, testing::ElementsAre(EqualsProto( - req.partitions(0).arguments(0))))) + ExecuteCode(_, _, + testing::ElementsAre( + EqualsProto(req.partitions(0).arguments(0))))) .WillOnce(Return(absl::InternalError("UDF execution error"))); v2::GetValuesResponse resp; const auto result = handler.GetValues(req, &resp); diff --git a/components/data_server/request_handler/ohttp_client_encryptor.cc b/components/data_server/request_handler/ohttp_client_encryptor.cc index cb42717d..81f33d0c 100644 --- a/components/data_server/request_handler/ohttp_client_encryptor.cc +++ b/components/data_server/request_handler/ohttp_client_encryptor.cc @@ -16,8 +16,8 @@ #include +#include "absl/log/log.h" #include "absl/strings/escaping.h" -#include "glog/logging.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace kv_server { @@ -77,8 +77,8 @@ absl::StatusOr OhttpClientEncryptor::EncryptRequest( return serialized_encrypted_req; } -absl::StatusOr -OhttpClientEncryptor::DecryptResponse(std::string encrypted_payload) { +absl::StatusOr OhttpClientEncryptor::DecryptResponse( + std::string encrypted_payload) { if (!http_client_.has_value() || !http_request_context_.has_value()) { return absl::InternalError( "Emtpy `http_client_` or `http_request_context_`. You should call " @@ -89,6 +89,6 @@ OhttpClientEncryptor::DecryptResponse(std::string encrypted_payload) { if (!decrypted_response.ok()) { return decrypted_response.status(); } - return *decrypted_response; + return std::move(*decrypted_response).ConsumePlaintextData(); } } // namespace kv_server diff --git a/components/data_server/request_handler/ohttp_client_encryptor.h b/components/data_server/request_handler/ohttp_client_encryptor.h index 773fea32..1e023516 100644 --- a/components/data_server/request_handler/ohttp_client_encryptor.h +++ b/components/data_server/request_handler/ohttp_client_encryptor.h @@ -22,7 +22,7 @@ #include "absl/strings/escaping.h" #include "public/constants.h" #include "quiche/oblivious_http/oblivious_http_client.h" -#include "src/cpp/encryption/key_fetcher/src/key_fetcher_manager.h" +#include "src/encryption/key_fetcher/key_fetcher_manager.h" namespace kv_server { @@ -44,12 +44,7 @@ class OhttpClientEncryptor { absl::StatusOr EncryptRequest(std::string payload); // Decrypts incoming reponse. Since OHTTP is stateful, this method should be // called after EncryptRequest. - // In order to avoid an extra copy, leaking the `ObliviousHttpResponse`. - // Note that we have a CL for the underlying library that might allow us to - // not do leak this object and not do the copy. If/when that's merged, we - // should refactor this back to returning a string. - absl::StatusOr DecryptResponse( - std::string encrypted_payload); + absl::StatusOr DecryptResponse(std::string encrypted_payload); private: ::privacy_sandbox::server_common::CloudPlatform cloud_platform_ = diff --git a/components/data_server/request_handler/ohttp_encryptor_test.cc b/components/data_server/request_handler/ohttp_encryptor_test.cc index ce54f4f4..73cdd15f 100644 --- a/components/data_server/request_handler/ohttp_encryptor_test.cc +++ b/components/data_server/request_handler/ohttp_encryptor_test.cc @@ -17,8 +17,8 @@ #include "components/data_server/request_handler/ohttp_client_encryptor.h" #include "components/data_server/request_handler/ohttp_server_encryptor.h" #include "gtest/gtest.h" -#include "src/cpp/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" namespace kv_server { namespace { @@ -43,7 +43,7 @@ TEST(OhttpEncryptorTest, FullCircleSuccess) { auto response_decrypted_status = client_encryptor.DecryptResponse(*response_encrypted_status); ASSERT_TRUE(response_decrypted_status.ok()); - EXPECT_EQ(kTestResponse, response_decrypted_status->GetPlaintextData()); + EXPECT_EQ(kTestResponse, *response_decrypted_status); } TEST(OhttpEncryptorTest, ServerDecryptRequestFails) { diff --git a/components/data_server/request_handler/ohttp_server_encryptor.cc b/components/data_server/request_handler/ohttp_server_encryptor.cc index 689022f5..8280e845 100644 --- a/components/data_server/request_handler/ohttp_server_encryptor.cc +++ b/components/data_server/request_handler/ohttp_server_encryptor.cc @@ -16,7 +16,7 @@ #include -#include "glog/logging.h" +#include "absl/log/log.h" #include "quiche/oblivious_http/common/oblivious_http_header_key_config.h" namespace kv_server { diff --git a/components/data_server/request_handler/ohttp_server_encryptor.h b/components/data_server/request_handler/ohttp_server_encryptor.h index b35595c6..f888284f 100644 --- a/components/data_server/request_handler/ohttp_server_encryptor.h +++ b/components/data_server/request_handler/ohttp_server_encryptor.h @@ -22,7 +22,7 @@ #include "absl/strings/escaping.h" #include "public/constants.h" #include "quiche/oblivious_http/oblivious_http_gateway.h" -#include "src/cpp/encryption/key_fetcher/src/key_fetcher_manager.h" +#include "src/encryption/key_fetcher/key_fetcher_manager.h" namespace kv_server { diff --git a/components/data_server/request_handler/uncompressed.cc b/components/data_server/request_handler/uncompressed.cc index 7a190f00..cbbe32d5 100644 --- a/components/data_server/request_handler/uncompressed.cc +++ b/components/data_server/request_handler/uncompressed.cc @@ -15,7 +15,7 @@ #include -#include "glog/logging.h" +#include "absl/log/log.h" #include "quiche/common/quiche_data_writer.h" namespace kv_server { diff --git a/components/data_server/server/BUILD.bazel b/components/data_server/server/BUILD.bazel index 32b3ea06..c1cb1072 100644 --- a/components/data_server/server/BUILD.bazel +++ b/components/data_server/server/BUILD.bazel @@ -28,7 +28,6 @@ cc_library( "//components/data_server/request_handler:get_values_handler", "//public/query:get_values_cc_grpc", "@com_github_grpc_grpc//:grpc++", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -72,7 +71,6 @@ cc_test( "@com_google_absl//absl/flags:parse", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -84,8 +82,8 @@ cc_library( "//components/cloud_config:instance_client", "//components/data_server/server:parameter_fetcher", "//components/errors:retry", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", ], ) @@ -102,7 +100,6 @@ cc_test( "//components/cloud_config:parameter_client", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -119,7 +116,6 @@ cc_library( "//components/data_server/request_handler:get_values_v2_handler", "//public/query/v2:get_values_v2_cc_grpc", "@com_github_grpc_grpc//:grpc++", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -153,6 +149,7 @@ cc_library( "//components/internal_server:sharded_lookup", "//components/sharding:cluster_mappings_manager", "//components/telemetry:kv_telemetry", + "//components/telemetry:open_telemetry_sink", "//components/telemetry:server_definition", "//components/udf:udf_client", "//components/udf:udf_config_builder", @@ -168,17 +165,16 @@ cc_library( "//public/query:get_values_cc_grpc", "//public/sharding:key_sharder", "//public/udf:constants", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", "@com_github_grpc_grpc//:grpc++_reflection", # for grpc_cli "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:init", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry", + "@google_privacysandbox_servers_common//src/telemetry:init", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) @@ -198,13 +194,13 @@ cc_test( "@com_google_absl//absl/flags:parse", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) cc_binary( name = "server", srcs = ["main.cc"], + malloc = "@com_google_tcmalloc//tcmalloc", visibility = [ "//google_internal/production/packaging:__subpackages__", "//production/packaging:__subpackages__", @@ -214,12 +210,15 @@ cc_binary( ":server_lib", "//components/sharding:shard_manager", "//components/util:version_linkstamp", - "@com_github_google_glog//:glog", "@com_google_absl//absl/debugging:failure_signal_handler", "@com_google_absl//absl/debugging:symbolize", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/strings", + "@google_privacysandbox_servers_common//src/util:rlimit_core_config", ], ) @@ -258,33 +257,59 @@ cc_library( cc_library( name = "key_fetcher_factory", srcs = select({ - "//:aws_platform": [ + "//:aws_nonprod": [ + "key_fetcher_factory_cloud.cc", + "nonprod_key_fetcher_factory_aws.cc", + "nonprod_key_fetcher_factory_cloud.cc", + ], + "//:aws_prod": [ "key_fetcher_factory_aws.cc", "key_fetcher_factory_cloud.cc", ], - "//:gcp_platform": [ + "//:gcp_nonprod": [ + "key_fetcher_factory_cloud.cc", + "key_fetcher_utils_gcp.cc", + "nonprod_key_fetcher_factory_cloud.cc", + "nonprod_key_fetcher_factory_gcp.cc", + ], + "//:gcp_prod": [ "key_fetcher_factory_cloud.cc", "key_fetcher_factory_gcp.cc", + "key_fetcher_utils_gcp.cc", ], "//:local_platform": [ "key_fetcher_factory_local.cc", ], }), - hdrs = [ + hdrs = select({ + "//:gcp_platform": [ + "key_fetcher_utils_gcp.h", + ], + "//:nonprod_mode": [ + "nonprod_key_fetcher_factory_cloud.h", + ], + "//conditions:default": [], + }) + [ "key_fetcher_factory.h", ], - deps = [ - ":parameter_fetcher", - "@com_google_absl//absl/flags:flag", - "@com_google_absl//absl/flags:parse", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/time", - "@google_privacysandbox_servers_common//src/cpp/concurrent:executor", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:private_key_fetcher", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:public_key_fetcher", - ], + deps = + select({ + "//:gcp_platform": [ + ":key_fetcher_utils_gcp", + ], + "//conditions:default": [], + }) + [ + ":parameter_fetcher", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@google_privacysandbox_servers_common//src/concurrent:executor", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:key_fetcher_manager", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:private_key_fetcher", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:public_key_fetcher", + ], ) cc_library( @@ -306,7 +331,16 @@ cc_library( "//components/udf/hooks:get_values_hook", "//components/udf/hooks:run_query_hook", "//public/sharding:key_sharder", - "@com_github_google_glog//:glog", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", + "@com_google_absl//absl/log", + ], +) + +cc_library( + name = "key_fetcher_utils_gcp", + srcs = ["key_fetcher_utils_gcp.cc"], + hdrs = ["key_fetcher_utils_gcp.h"], + deps = [ + ":parameter_fetcher", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:key_fetcher_manager", ], ) diff --git a/components/data_server/server/key_fetcher_factory.h b/components/data_server/server/key_fetcher_factory.h index be8ae41b..e5f61f73 100644 --- a/components/data_server/server/key_fetcher_factory.h +++ b/components/data_server/server/key_fetcher_factory.h @@ -15,12 +15,14 @@ */ #include +#include +#include #ifndef COMPONENTS_DATA_SERVER_SERVER_KEY_FETCHER_FACTORY_H_ #define COMPONENTS_DATA_SERVER_SERVER_KEY_FETCHER_FACTORY_H_ #include "components/data_server/server/parameter_fetcher.h" -#include "src/cpp/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" namespace kv_server { // Constructs KeyFetcherManager. @@ -56,6 +58,8 @@ class CloudKeyFetcherFactory : public KeyFetcherFactory { virtual google::scp::cpio::PrivateKeyVendingEndpoint GetSecondaryKeyFetchingEndpoint( const ParameterFetcher& parameter_fetcher) const; + virtual std::vector GetPublicKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const; virtual ::privacy_sandbox::server_common::CloudPlatform GetCloudPlatform() const; }; diff --git a/components/data_server/server/key_fetcher_factory_cloud.cc b/components/data_server/server/key_fetcher_factory_cloud.cc index d655904c..810b17ed 100644 --- a/components/data_server/server/key_fetcher_factory_cloud.cc +++ b/components/data_server/server/key_fetcher_factory_cloud.cc @@ -20,22 +20,14 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" +#include "absl/log/log.h" #include "components/data_server/server/key_fetcher_factory.h" -#include "glog/logging.h" +#include "src/concurrent/event_engine_executor.h" #include "src/core/lib/event_engine/default_event_engine.h" -#include "src/cpp/concurrent/event_engine_executor.h" -#include "src/cpp/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" ABSL_FLAG(std::string, public_key_endpoint, "", "Public key endpoint."); -ABSL_FLAG(std::string, primary_coordinator_private_key_endpoint, "", - "Primary coordinator private key endpoint."); -ABSL_FLAG(std::string, secondary_coordinator_private_key_endpoint, "", - "Secondary coordinator private key endpoint."); -ABSL_FLAG(std::string, primary_coordinator_region, "", - "Primary coordinator region."); -ABSL_FLAG(std::string, secondary_coordinator_region, "", - "Secondary coordinator region."); namespace kv_server { using ::google::scp::cpio::PrivateKeyVendingEndpoint; @@ -56,6 +48,17 @@ constexpr std::string_view kPrimaryCoordinatorAccountIdentityParameterSuffix = "primary-coordinator-account-identity"; constexpr std::string_view kSecondaryCoordinatorAccountIdentityParameterSuffix = "secondary-coordinator-account-identity"; +constexpr std::string_view + kPrimaryCoordinatorPrivateKeyEndpointParameterSuffix = + "primary-coordinator-private-key-endpoint"; +constexpr std::string_view kPrimaryCoordinatorRegionParameterSuffix = + "primary-coordinator-region"; +constexpr std::string_view + kSecondaryCoordinatoPrivateKeyEndpointParameterSuffix = + "secondary-coordinator-private-key-endpoint"; +constexpr std::string_view kSecondaryCoordinatorRegionParameterSuffix = + "secondary-coordinator-region"; + // Setting these to match // ..fledge/servers/bidding-auction-server/+/main:services/common/constants/common_service_flags.cc constexpr absl::Duration kPrivateKeyCacheTtl = absl::Hours(24 * 45); // 45 days @@ -63,17 +66,20 @@ constexpr absl::Duration kKeyRefreshFlowRunFrequency = absl::Hours(3); PrivateKeyVendingEndpoint GetKeyFetchingEndpoint( const ParameterFetcher& parameter_fetcher, - std::string_view account_identity_prefix, std::string_view service_endpoint, - absl::string_view region) { + std::string_view account_identity_prefix, + std::string_view private_key_endpoint_prefix, + absl::string_view region_prefix) { PrivateKeyVendingEndpoint endpoint; endpoint.account_identity = parameter_fetcher.GetParameter(account_identity_prefix); LOG(INFO) << "Retrieved " << account_identity_prefix << " parameter: " << endpoint.account_identity; - endpoint.private_key_vending_service_endpoint = service_endpoint; - LOG(INFO) << "Service endpoint: " << service_endpoint; - endpoint.service_region = region; - LOG(INFO) << "Region: " << region; + endpoint.private_key_vending_service_endpoint = + parameter_fetcher.GetParameter(private_key_endpoint_prefix); + LOG(INFO) << "Service endpoint: " + << endpoint.private_key_vending_service_endpoint; + endpoint.service_region = parameter_fetcher.GetParameter(region_prefix); + LOG(INFO) << "Region: " << endpoint.service_region; return endpoint; } } // namespace @@ -87,10 +93,8 @@ CloudKeyFetcherFactory::CreateKeyFetcherManager( "and private keys"; return std::make_unique(); } - auto publicKeyEndpointParameter = absl::GetFlag(FLAGS_public_key_endpoint); - LOG(INFO) << "Retrieved public_key_endpoint parameter: " - << publicKeyEndpointParameter; - std::vector endpoints = {publicKeyEndpointParameter}; + std::vector endpoints = + GetPublicKeyFetchingEndpoint(parameter_fetcher); std::unique_ptr public_key_fetcher = PublicKeyFetcherFactory::Create({{GetCloudPlatform(), endpoints}}); auto primary = GetPrimaryKeyFetchingEndpoint(parameter_fetcher); @@ -109,12 +113,21 @@ CloudKeyFetcherFactory::CreateKeyFetcherManager( return manager; } +std::vector CloudKeyFetcherFactory::GetPublicKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const { + auto publicKeyEndpointParameter = absl::GetFlag(FLAGS_public_key_endpoint); + LOG(INFO) << "Retrieved public_key_endpoint parameter: " + << publicKeyEndpointParameter; + std::vector endpoints = {publicKeyEndpointParameter}; + return endpoints; +} + PrivateKeyVendingEndpoint CloudKeyFetcherFactory::GetPrimaryKeyFetchingEndpoint( const ParameterFetcher& parameter_fetcher) const { return GetKeyFetchingEndpoint( parameter_fetcher, kPrimaryCoordinatorAccountIdentityParameterSuffix, - absl::GetFlag(FLAGS_primary_coordinator_private_key_endpoint), - absl::GetFlag(FLAGS_primary_coordinator_region)); + kPrimaryCoordinatorPrivateKeyEndpointParameterSuffix, + kPrimaryCoordinatorRegionParameterSuffix); } PrivateKeyVendingEndpoint @@ -122,8 +135,8 @@ CloudKeyFetcherFactory::GetSecondaryKeyFetchingEndpoint( const ParameterFetcher& parameter_fetcher) const { return GetKeyFetchingEndpoint( parameter_fetcher, kSecondaryCoordinatorAccountIdentityParameterSuffix, - absl::GetFlag(FLAGS_secondary_coordinator_private_key_endpoint), - absl::GetFlag(FLAGS_secondary_coordinator_region)); + kPrimaryCoordinatorAccountIdentityParameterSuffix, + kSecondaryCoordinatorRegionParameterSuffix); } CloudPlatform CloudKeyFetcherFactory::GetCloudPlatform() const { diff --git a/components/data_server/server/key_fetcher_factory_gcp.cc b/components/data_server/server/key_fetcher_factory_gcp.cc index 9e6a62df..d4fe2ada 100644 --- a/components/data_server/server/key_fetcher_factory_gcp.cc +++ b/components/data_server/server/key_fetcher_factory_gcp.cc @@ -12,45 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "absl/log/log.h" #include "components/data_server/server/key_fetcher_factory.h" -#include "glog/logging.h" +#include "components/data_server/server/key_fetcher_utils_gcp.h" namespace kv_server { namespace { using ::google::scp::cpio::PrivateKeyVendingEndpoint; using ::privacy_sandbox::server_common::CloudPlatform; -constexpr std::string_view kPrimaryKeyServiceCloudFunctionUrlSuffix = - "primary-key-service-cloud-function-url"; -constexpr std::string_view kPrimaryWorkloadIdentityPoolProviderSuffix = - "primary-workload-identity-pool-provider"; -constexpr std::string_view kSecondaryKeyServiceCloudFunctionUrlSuffix = - "secondary-key-service-cloud-function-url"; -constexpr std::string_view kSecondaryWorkloadIdentityPoolProviderSuffix = - "secondary-workload-identity-pool-provider"; - -void SetGcpSpecificParameters(PrivateKeyVendingEndpoint& endpoint, - const ParameterFetcher& parameter_fetcher, - const std::string_view cloudfunction_prefix, - const std::string_view wip_provider) { - endpoint.gcp_private_key_vending_service_cloudfunction_url = - parameter_fetcher.GetParameter(cloudfunction_prefix); - LOG(INFO) << "Retrieved " << cloudfunction_prefix << " parameter: " - << endpoint.gcp_private_key_vending_service_cloudfunction_url; - endpoint.gcp_wip_provider = parameter_fetcher.GetParameter(wip_provider); - LOG(INFO) << "Retrieved " << wip_provider - << " parameter: " << endpoint.gcp_wip_provider; -} - class KeyFetcherFactoryGcp : public CloudKeyFetcherFactory { PrivateKeyVendingEndpoint GetPrimaryKeyFetchingEndpoint( const ParameterFetcher& parameter_fetcher) const override { PrivateKeyVendingEndpoint endpoint = CloudKeyFetcherFactory::GetPrimaryKeyFetchingEndpoint( parameter_fetcher); - SetGcpSpecificParameters(endpoint, parameter_fetcher, - kPrimaryKeyServiceCloudFunctionUrlSuffix, - kPrimaryWorkloadIdentityPoolProviderSuffix); + UpdatePrimaryGcpEndpoint(endpoint, parameter_fetcher); return endpoint; } @@ -59,9 +36,7 @@ class KeyFetcherFactoryGcp : public CloudKeyFetcherFactory { PrivateKeyVendingEndpoint endpoint = CloudKeyFetcherFactory::GetSecondaryKeyFetchingEndpoint( parameter_fetcher); - SetGcpSpecificParameters(endpoint, parameter_fetcher, - kSecondaryKeyServiceCloudFunctionUrlSuffix, - kSecondaryWorkloadIdentityPoolProviderSuffix); + UpdateSecondaryGcpEndpoint(endpoint, parameter_fetcher); return endpoint; } diff --git a/components/data_server/server/key_fetcher_factory_local.cc b/components/data_server/server/key_fetcher_factory_local.cc index 5ae07dc3..9c1545b0 100644 --- a/components/data_server/server/key_fetcher_factory_local.cc +++ b/components/data_server/server/key_fetcher_factory_local.cc @@ -17,7 +17,7 @@ #include "absl/flags/flag.h" #include "components/data_server/server/key_fetcher_factory.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" ABSL_FLAG(std::string, public_key_endpoint, "", "Public key endpoint."); ABSL_FLAG(std::string, primary_coordinator_private_key_endpoint, "", diff --git a/components/data_server/server/key_fetcher_utils_gcp.cc b/components/data_server/server/key_fetcher_utils_gcp.cc new file mode 100644 index 00000000..8fedbfa8 --- /dev/null +++ b/components/data_server/server/key_fetcher_utils_gcp.cc @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "components/data_server/server/key_fetcher_utils_gcp.h" + +#include "absl/log/log.h" + +namespace kv_server { + +void SetGcpSpecificParameters(PrivateKeyVendingEndpoint& endpoint, + const ParameterFetcher& parameter_fetcher, + const std::string_view cloudfunction_prefix, + const std::string_view wip_provider) { + endpoint.gcp_private_key_vending_service_cloudfunction_url = + parameter_fetcher.GetParameter(cloudfunction_prefix); + LOG(INFO) << "Retrieved " << cloudfunction_prefix << " parameter: " + << endpoint.gcp_private_key_vending_service_cloudfunction_url; + endpoint.gcp_wip_provider = parameter_fetcher.GetParameter(wip_provider); + LOG(INFO) << "Retrieved " << wip_provider + << " parameter: " << endpoint.gcp_wip_provider; +} + +void UpdatePrimaryGcpEndpoint(PrivateKeyVendingEndpoint& endpoint, + const ParameterFetcher& parameter_fetcher) { + SetGcpSpecificParameters(endpoint, parameter_fetcher, + kPrimaryKeyServiceCloudFunctionUrlSuffix, + kPrimaryWorkloadIdentityPoolProviderSuffix); +} + +void UpdateSecondaryGcpEndpoint(PrivateKeyVendingEndpoint& endpoint, + const ParameterFetcher& parameter_fetcher) { + SetGcpSpecificParameters(endpoint, parameter_fetcher, + kSecondaryKeyServiceCloudFunctionUrlSuffix, + kSecondaryWorkloadIdentityPoolProviderSuffix); +} + +} // namespace kv_server diff --git a/components/data_server/server/key_fetcher_utils_gcp.h b/components/data_server/server/key_fetcher_utils_gcp.h new file mode 100644 index 00000000..1b508aae --- /dev/null +++ b/components/data_server/server/key_fetcher_utils_gcp.h @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_DATA_SERVER_KEY_FETCHER_UTILS_GCP_H_ +#define COMPONENTS_DATA_SERVER_KEY_FETCHER_UTILS_GCP_H_ + +#include "components/data_server/server/parameter_fetcher.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" + +namespace kv_server { + +using ::google::scp::cpio::PrivateKeyVendingEndpoint; + +constexpr std::string_view kPrimaryKeyServiceCloudFunctionUrlSuffix = + "primary-key-service-cloud-function-url"; +constexpr std::string_view kPrimaryWorkloadIdentityPoolProviderSuffix = + "primary-workload-identity-pool-provider"; +constexpr std::string_view kSecondaryKeyServiceCloudFunctionUrlSuffix = + "secondary-key-service-cloud-function-url"; +constexpr std::string_view kSecondaryWorkloadIdentityPoolProviderSuffix = + "secondary-workload-identity-pool-provider"; + +void UpdatePrimaryGcpEndpoint(PrivateKeyVendingEndpoint& endpoint, + const ParameterFetcher& parameter_fetcher); + +void UpdateSecondaryGcpEndpoint(PrivateKeyVendingEndpoint& endpoint, + const ParameterFetcher& parameter_fetcher); + +} // namespace kv_server + +#endif // COMPONENTS_DATA_SERVER_KEY_FETCHER_UTILS_GCP_H_ diff --git a/components/data_server/server/key_value_service_impl.cc b/components/data_server/server/key_value_service_impl.cc index f29b4963..35fc0e08 100644 --- a/components/data_server/server/key_value_service_impl.cc +++ b/components/data_server/server/key_value_service_impl.cc @@ -20,18 +20,12 @@ #include "components/data_server/request_handler/get_values_handler.h" #include "public/query/get_values.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" - -constexpr char* kGetValuesSuccess = "GetValuesSuccess"; namespace kv_server { using google::protobuf::Struct; using google::protobuf::Value; using grpc::CallbackServerContext; -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::ScopeLatencyRecorder; using v1::GetValuesRequest; using v1::GetValuesResponse; using v1::KeyValueService; @@ -39,25 +33,13 @@ using v1::KeyValueService; grpc::ServerUnaryReactor* KeyValueServiceImpl::GetValues( CallbackServerContext* context, const GetValuesRequest* request, GetValuesResponse* response) { - ScopeLatencyRecorder latency_recorder(std::string(kGetValuesV1Latency), - metrics_recorder_); - - grpc::Status status = handler_.GetValues(*request, response); - - if (status.ok()) { - metrics_recorder_.IncrementEventStatus(kGetValuesSuccess, absl::OkStatus()); - } else { - // TODO: use implicit conversion when it becomes available externally - // https://g3doc.corp.google.com/net/grpc/g3doc/grpc_prod/cpp/status_mapping.md?cl=head - absl::StatusCode absl_status_code = - static_cast(status.error_code()); - absl::Status absl_status = - absl::Status(absl_status_code, status.error_message()); - metrics_recorder_.IncrementEventStatus(kGetValuesSuccess, absl_status); - } - + auto request_received_time = absl::Now(); + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); + grpc::Status status = handler_.GetValues(request_context, *request, response); auto* reactor = context->DefaultReactor(); reactor->Finish(status); + LogRequestCommonSafeMetrics(request, response, status, request_received_time); return reactor; } diff --git a/components/data_server/server/key_value_service_impl.h b/components/data_server/server/key_value_service_impl.h index de9d0bca..a57818d5 100644 --- a/components/data_server/server/key_value_service_impl.h +++ b/components/data_server/server/key_value_service_impl.h @@ -24,23 +24,15 @@ #include "components/data_server/request_handler/get_values_handler.h" #include "grpcpp/grpcpp.h" #include "public/query/get_values.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { -constexpr char* kGetValuesV1Latency = "GetValuesV1Latency"; - // Implements Key-Value service. class KeyValueServiceImpl final : public kv_server::v1::KeyValueService::CallbackService { public: - explicit KeyValueServiceImpl( - GetValuesHandler handler, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) - : handler_(std::move(handler)), metrics_recorder_(metrics_recorder) { - metrics_recorder_.RegisterHistogram( - kGetValuesV1Latency, "GetValues V1 service latency", "nanosecond"); - } + explicit KeyValueServiceImpl(GetValuesHandler handler) + : handler_(std::move(handler)) {} grpc::ServerUnaryReactor* GetValues( grpc::CallbackServerContext* context, @@ -49,7 +41,6 @@ class KeyValueServiceImpl final private: GetValuesHandler handler_; - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; }; } // namespace kv_server diff --git a/components/data_server/server/key_value_service_v2_impl.cc b/components/data_server/server/key_value_service_v2_impl.cc index 35934e31..36932962 100644 --- a/components/data_server/server/key_value_service_v2_impl.cc +++ b/components/data_server/server/key_value_service_v2_impl.cc @@ -17,8 +17,7 @@ #include #include "public/query/v2/get_values_v2.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/telemetry/telemetry.h" namespace kv_server { namespace { @@ -37,10 +36,11 @@ grpc::ServerUnaryReactor* HandleRequest( CallbackServerContext* context, const RequestT* request, ResponseT* response, const GetValuesV2Handler& handler, HandlerFunctionT handler_function) { + auto request_received_time = absl::Now(); grpc::Status status = (handler.*handler_function)(*request, response); - auto* reactor = context->DefaultReactor(); reactor->Finish(status); + LogRequestCommonSafeMetrics(request, response, status, request_received_time); return reactor; } diff --git a/components/data_server/server/key_value_service_v2_impl.h b/components/data_server/server/key_value_service_v2_impl.h index 3f20ba53..c95d7b3b 100644 --- a/components/data_server/server/key_value_service_v2_impl.h +++ b/components/data_server/server/key_value_service_v2_impl.h @@ -24,7 +24,6 @@ #include "components/data_server/request_handler/get_values_v2_handler.h" #include "grpcpp/grpcpp.h" #include "public/query/v2/get_values_v2.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { @@ -32,10 +31,8 @@ namespace kv_server { class KeyValueServiceV2Impl final : public v2::KeyValueService::CallbackService { public: - explicit KeyValueServiceV2Impl( - GetValuesV2Handler handler, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) - : handler_(std::move(handler)), metrics_recorder_(metrics_recorder) {} + explicit KeyValueServiceV2Impl(GetValuesV2Handler handler) + : handler_(std::move(handler)) {} grpc::ServerUnaryReactor* GetValuesHttp( grpc::CallbackServerContext* context, @@ -58,7 +55,6 @@ class KeyValueServiceV2Impl final private: const GetValuesV2Handler handler_; - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; }; } // namespace kv_server diff --git a/components/data_server/server/lifecycle_heartbeat.cc b/components/data_server/server/lifecycle_heartbeat.cc index 52998d19..593faf80 100644 --- a/components/data_server/server/lifecycle_heartbeat.cc +++ b/components/data_server/server/lifecycle_heartbeat.cc @@ -17,8 +17,8 @@ #include #include +#include "absl/log/log.h" #include "components/errors/retry.h" -#include "glog/logging.h" namespace kv_server { diff --git a/components/data_server/server/lifecycle_heartbeat_test.cc b/components/data_server/server/lifecycle_heartbeat_test.cc index 2e4e3a76..936a1f7b 100644 --- a/components/data_server/server/lifecycle_heartbeat_test.cc +++ b/components/data_server/server/lifecycle_heartbeat_test.cc @@ -21,7 +21,6 @@ #include "components/data_server/server/mocks.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { diff --git a/components/data_server/server/main.cc b/components/data_server/server/main.cc index e260cc74..15caa8aa 100644 --- a/components/data_server/server/main.cc +++ b/components/data_server/server/main.cc @@ -17,10 +17,13 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" #include "components/data_server/server/server.h" #include "components/util/build_info.h" -#include "glog/logging.h" +#include "src/util/rlimit_core_config.h" ABSL_FLAG(bool, buildinfo, false, "Print build info."); @@ -35,12 +38,15 @@ int main(int argc, char** argv) { // 3. Production versions of the K/V Server run inside Trusted Execution // Environments, which restrict where STDOUT and STDERR are visible to. absl::InitializeSymbolizer(argv[0]); + privacysandbox::server_common::SetRLimits({ + .enable_core_dumps = true, + }); { absl::FailureSignalHandlerOptions options; absl::InstallFailureSignalHandler(options); } - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); absl::SetProgramUsageMessage(absl::StrCat( "FLEDGE Key/Value Server. Sample usage:\n", argv[0], " --port=50051")); absl::ParseCommandLine(argc, argv); diff --git a/components/data_server/server/mocks.h b/components/data_server/server/mocks.h index 4ba36d19..8734fce2 100644 --- a/components/data_server/server/mocks.h +++ b/components/data_server/server/mocks.h @@ -25,7 +25,6 @@ #include "components/data_server/server/parameter_fetcher.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { diff --git a/components/data_server/server/nonprod_key_fetcher_factory_aws.cc b/components/data_server/server/nonprod_key_fetcher_factory_aws.cc new file mode 100644 index 00000000..d50eb289 --- /dev/null +++ b/components/data_server/server/nonprod_key_fetcher_factory_aws.cc @@ -0,0 +1,21 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/data_server/server/nonprod_key_fetcher_factory_cloud.h" + +namespace kv_server { +std::unique_ptr KeyFetcherFactory::Create() { + return std::make_unique(); +} +} // namespace kv_server diff --git a/components/data_server/server/nonprod_key_fetcher_factory_cloud.cc b/components/data_server/server/nonprod_key_fetcher_factory_cloud.cc new file mode 100644 index 00000000..a877ec57 --- /dev/null +++ b/components/data_server/server/nonprod_key_fetcher_factory_cloud.cc @@ -0,0 +1,48 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/data_server/server/nonprod_key_fetcher_factory_cloud.h" + +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/flags/usage.h" +#include "absl/log/log.h" +#include "components/data_server/server/key_fetcher_factory.h" +#include "src/concurrent/event_engine_executor.h" +#include "src/core/lib/event_engine/default_event_engine.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" + +namespace kv_server { +using ::google::scp::cpio::PrivateKeyVendingEndpoint; + +constexpr std::string_view kPublicKeyEndpointParameterSuffix = + "public-key-endpoint"; + +std::vector +NonprodCloudKeyFetcherFactory::GetPublicKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const { + auto publicKeyEndpointParameter = + parameter_fetcher.GetParameter(kPublicKeyEndpointParameterSuffix); + LOG(INFO) << "Retrieved public_key_endpoint parameter: " + << publicKeyEndpointParameter; + std::vector endpoints = {publicKeyEndpointParameter}; + return endpoints; +} +} // namespace kv_server diff --git a/components/data_server/server/nonprod_key_fetcher_factory_cloud.h b/components/data_server/server/nonprod_key_fetcher_factory_cloud.h new file mode 100644 index 00000000..8f9bb4a5 --- /dev/null +++ b/components/data_server/server/nonprod_key_fetcher_factory_cloud.h @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#ifndef COMPONENTS_DATA_SERVER_SERVER_NONPROD_KEY_FETCHER_FACTORY_CLOUD_H_ +#define COMPONENTS_DATA_SERVER_SERVER_NONPROD_KEY_FETCHER_FACTORY_CLOUD_H_ + +#include "components/data_server/server/key_fetcher_factory.h" +#include "components/data_server/server/parameter_fetcher.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" + +namespace kv_server { + +// Constructs KeyFetcherManager. This factory allows to override the public key +// endpoint. This is a security risk for produciton build. Which is why this +// implementation is only allowed in `nonprod` build mode. +class NonprodCloudKeyFetcherFactory : public CloudKeyFetcherFactory { + protected: + google::scp::cpio::PrivateKeyVendingEndpoint GetPrimaryKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const override; + google::scp::cpio::PrivateKeyVendingEndpoint GetSecondaryKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const override; + std::vector GetPublicKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const override; + ::privacy_sandbox::server_common::CloudPlatform GetCloudPlatform() + const override; +}; + +} // namespace kv_server +#endif // COMPONENTS_DATA_SERVER_SERVER_NONPROD_KEY_FETCHER_FACTORY_CLOUD_H_ diff --git a/components/data_server/server/nonprod_key_fetcher_factory_gcp.cc b/components/data_server/server/nonprod_key_fetcher_factory_gcp.cc new file mode 100644 index 00000000..47880038 --- /dev/null +++ b/components/data_server/server/nonprod_key_fetcher_factory_gcp.cc @@ -0,0 +1,53 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "absl/log/log.h" +#include "components/data_server/server/key_fetcher_factory.h" +#include "components/data_server/server/key_fetcher_utils_gcp.h" +#include "components/data_server/server/nonprod_key_fetcher_factory_cloud.h" + +namespace kv_server { +namespace { +using ::google::scp::cpio::PrivateKeyVendingEndpoint; +using ::privacy_sandbox::server_common::CloudPlatform; + +class KeyFetcherFactoryGcpNonProd : public NonprodCloudKeyFetcherFactory { + PrivateKeyVendingEndpoint GetPrimaryKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const override { + PrivateKeyVendingEndpoint endpoint = + NonprodCloudKeyFetcherFactory::GetPrimaryKeyFetchingEndpoint( + parameter_fetcher); + UpdatePrimaryGcpEndpoint(endpoint, parameter_fetcher); + return endpoint; + } + + PrivateKeyVendingEndpoint GetSecondaryKeyFetchingEndpoint( + const ParameterFetcher& parameter_fetcher) const override { + PrivateKeyVendingEndpoint endpoint = + NonprodCloudKeyFetcherFactory::GetSecondaryKeyFetchingEndpoint( + parameter_fetcher); + UpdateSecondaryGcpEndpoint(endpoint, parameter_fetcher); + return endpoint; + } + + CloudPlatform GetCloudPlatform() const override { + return CloudPlatform::kGcp; + } +}; +} // namespace + +std::unique_ptr KeyFetcherFactory::Create() { + return std::make_unique(); +} +} // namespace kv_server diff --git a/components/data_server/server/parameter_fetcher_aws.cc b/components/data_server/server/parameter_fetcher_aws.cc index b1cb3d9c..6d4f0e55 100644 --- a/components/data_server/server/parameter_fetcher_aws.cc +++ b/components/data_server/server/parameter_fetcher_aws.cc @@ -14,8 +14,8 @@ #include +#include "absl/log/log.h" #include "components/data_server/server/parameter_fetcher.h" -#include "glog/logging.h" namespace kv_server { diff --git a/components/data_server/server/parameter_fetcher_gcp.cc b/components/data_server/server/parameter_fetcher_gcp.cc index e3cd546b..5bcc9fbf 100644 --- a/components/data_server/server/parameter_fetcher_gcp.cc +++ b/components/data_server/server/parameter_fetcher_gcp.cc @@ -17,9 +17,9 @@ #include +#include "absl/log/log.h" #include "absl/strings/str_format.h" #include "components/data_server/server/parameter_fetcher.h" -#include "glog/logging.h" namespace kv_server { constexpr std::string_view kEnvironment = "environment"; diff --git a/components/data_server/server/parameter_fetcher_gcp_test.cc b/components/data_server/server/parameter_fetcher_gcp_test.cc index 824c3371..62418fa3 100644 --- a/components/data_server/server/parameter_fetcher_gcp_test.cc +++ b/components/data_server/server/parameter_fetcher_gcp_test.cc @@ -19,7 +19,6 @@ #include "components/data_server/server/parameter_fetcher.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { diff --git a/components/data_server/server/parameter_fetcher_local.cc b/components/data_server/server/parameter_fetcher_local.cc index ef47dc44..b22e1e1c 100644 --- a/components/data_server/server/parameter_fetcher_local.cc +++ b/components/data_server/server/parameter_fetcher_local.cc @@ -14,8 +14,8 @@ #include +#include "absl/log/log.h" #include "components/data_server/server/parameter_fetcher.h" -#include "glog/logging.h" namespace kv_server { diff --git a/components/data_server/server/parameter_fetcher_local_test.cc b/components/data_server/server/parameter_fetcher_local_test.cc index 1783162b..8a844bcf 100644 --- a/components/data_server/server/parameter_fetcher_local_test.cc +++ b/components/data_server/server/parameter_fetcher_local_test.cc @@ -21,7 +21,6 @@ #include "components/data_server/server/parameter_fetcher.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { diff --git a/components/data_server/server/server.cc b/components/data_server/server/server.cc index 53b7ea0b..2b88b059 100644 --- a/components/data_server/server/server.cc +++ b/components/data_server/server/server.cc @@ -20,8 +20,11 @@ #include "absl/flags/parse.h" #include "absl/flags/usage.h" #include "absl/functional/bind_front.h" +#include "absl/log/log.h" +#include "absl/log/log_sink_registry.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" +#include "components/data/blob_storage/blob_prefix_allowlist.h" #include "components/data_server/request_handler/get_values_adapter.h" #include "components/data_server/request_handler/get_values_handler.h" #include "components/data_server/request_handler/get_values_v2_handler.h" @@ -40,7 +43,7 @@ #include "components/udf/hooks/run_query_hook.h" #include "components/udf/udf_config_builder.h" #include "components/util/build_info.h" -#include "glog/logging.h" +#include "google/protobuf/text_format.h" #include "grpcpp/ext/proto_server_reflection_plugin.h" #include "grpcpp/health_check_service_interface.h" #include "public/constants.h" @@ -48,10 +51,10 @@ #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" #include "public/data_loading/readers/stream_record_reader_factory.h" #include "public/udf/constants.h" -#include "src/cpp/telemetry/init.h" -#include "src/cpp/telemetry/telemetry.h" -#include "src/cpp/telemetry/telemetry_provider.h" #include "src/google/protobuf/struct.pb.h" +#include "src/telemetry/init.h" +#include "src/telemetry/telemetry.h" +#include "src/telemetry/telemetry_provider.h" ABSL_FLAG(uint16_t, port, 50051, "Port the server is listening on. Defaults to 50051."); @@ -91,14 +94,22 @@ constexpr absl::string_view kLoggingVerbosityLevelParameterSuffix = "logging-verbosity-level"; constexpr absl::string_view kUdfTimeoutMillisParameterSuffix = "udf-timeout-millis"; +constexpr absl::string_view kUdfMinLogLevelParameterSuffix = + "udf-min-log-level"; constexpr absl::string_view kUseShardingKeyRegexParameterSuffix = "use-sharding-key-regex"; constexpr absl::string_view kShardingKeyRegexParameterSuffix = "sharding-key-regex"; constexpr absl::string_view kRouteV1ToV2Suffix = "route-v1-to-v2"; +constexpr absl::string_view kAddMissingKeysV1Suffix = "add-missing-keys-v1"; constexpr absl::string_view kAutoscalerHealthcheck = "autoscaler-healthcheck"; constexpr absl::string_view kLoadbalancerHealthcheck = "loadbalancer-healthcheck"; +constexpr absl::string_view kEnableOtelLoggerParameterSuffix = + "enable-otel-logger"; +constexpr std::string_view kDataLoadingBlobPrefixAllowlistSuffix = + "data-loading-blob-prefix-allowlist"; +constexpr std::string_view kTelemetryConfigSuffix = "telemetry-config"; opentelemetry::sdk::metrics::PeriodicExportingMetricReaderOptions GetMetricsOptions(const ParameterClient& parameter_client, @@ -124,19 +135,18 @@ GetMetricsOptions(const ParameterClient& parameter_client, return metrics_options; } -absl::Status CheckMetricsCollectorEndPointConnection( +void CheckMetricsCollectorEndPointConnection( std::string_view collector_endpoint) { auto channel = grpc::CreateChannel(std::string(collector_endpoint), grpc::InsecureChannelCredentials()); - // TODO(b/300137699): make the connection timeout a parameter - if (!channel->WaitForConnected( - gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), - gpr_time_from_seconds(120, GPR_TIMESPAN)))) { - return absl::DeadlineExceededError( - "Timeout waiting for metrics collector connection"); - } - LOG(INFO) << "Metrics collector is connected"; - return absl::OkStatus(); + RetryUntilOk( + [channel]() { + if (channel->GetState(true) != GRPC_CHANNEL_READY) { + return absl::UnavailableError("metrics collector is not connected"); + } + return absl::OkStatus(); + }, + "Checking connection to metrics collector", LogMetricsNoOpCallback()); } absl::optional GetMetricsCollectorEndPoint( @@ -159,29 +169,41 @@ absl::optional GetMetricsCollectorEndPoint( privacy_sandbox::server_common::telemetry::TelemetryConfig GetServerTelemetryConfig(const ParameterClient& parameter_client, const std::string& environment) { - // TODO(b/304306398): Read telemetry config from parameter + ParameterFetcher parameter_fetcher(environment, parameter_client); + auto config_string = parameter_fetcher.GetParameter(kTelemetryConfigSuffix); privacy_sandbox::server_common::telemetry::TelemetryConfig config; - config.set_mode( - privacy_sandbox::server_common::telemetry::TelemetryConfig::EXPERIMENT); + if (!google::protobuf::TextFormat::ParseFromString(config_string, &config)) { + LOG(ERROR) << "Invalid proto format for telemetry config " << config_string + << ", fall back to prod config mode"; + config.set_mode( + privacy_sandbox::server_common::telemetry::TelemetryConfig::PROD); + } return config; } +BlobPrefixAllowlist GetBlobPrefixAllowlist( + const ParameterFetcher& parameter_fetcher) { + const auto prefix_allowlist = parameter_fetcher.GetParameter( + kDataLoadingBlobPrefixAllowlistSuffix, /*default_value=*/""); + LOG(INFO) << "Retrieved " << kDataLoadingBlobPrefixAllowlistSuffix + << " parameter: " << prefix_allowlist; + return BlobPrefixAllowlist(prefix_allowlist); +} + } // namespace Server::Server() - : metrics_recorder_( - TelemetryProvider::GetInstance().CreateMetricsRecorder()), - string_get_values_hook_( + : string_get_values_hook_( GetValuesHook::Create(GetValuesHook::OutputType::kString)), binary_get_values_hook_( GetValuesHook::Create(GetValuesHook::OutputType::kBinary)), run_query_hook_(RunQueryHook::Create()) {} -// Because the cache relies on metrics_recorder_, this function needs to be +// Because the cache relies on telemetry, this function needs to be // called right after telemetry has been initialized but before anything that // requires the cache has been initialized. void Server::InitializeKeyValueCache() { - cache_ = KeyValueCache::Create(*metrics_recorder_); + cache_ = KeyValueCache::Create(); cache_->UpdateKeyValue( "hi", "Hello, world! If you are seeing this, it means you can " @@ -189,6 +211,24 @@ void Server::InitializeKeyValueCache() { /*logical_commit_time = */ 1); } +void Server::InitOtelLogger( + ::opentelemetry::sdk::resource::Resource server_info, + absl::optional collector_endpoint, + const ParameterFetcher& parameter_fetcher) { + const bool enable_otel_logger = + parameter_fetcher.GetBoolParameter(kEnableOtelLoggerParameterSuffix); + LOG(INFO) << "Retrieved " << kEnableOtelLoggerParameterSuffix + << " parameter: " << enable_otel_logger; + if (!enable_otel_logger) { + return; + } + log_provider_ = privacy_sandbox::server_common::ConfigurePrivateLogger( + server_info, collector_endpoint); + open_telemetry_sink_ = std::make_unique( + log_provider_->GetLogger(kServiceName.data())); + absl::AddLogSink(open_telemetry_sink_.get()); +} + void Server::InitializeTelemetry(const ParameterClient& parameter_client, InstanceClient& instance_client) { std::string instance_id = RetryUntilOk( @@ -200,11 +240,7 @@ void Server::InitializeTelemetry(const ParameterClient& parameter_client, auto metrics_collector_endpoint = GetMetricsCollectorEndPoint(parameter_client, environment_); if (metrics_collector_endpoint.has_value()) { - if (const absl::Status status = CheckMetricsCollectorEndPointConnection( - metrics_collector_endpoint.value()); - !status.ok()) { - LOG(ERROR) << "Error in connecting metrics collector: " << status; - } + CheckMetricsCollectorEndPointConnection(metrics_collector_endpoint.value()); } LOG(INFO) << "Done retrieving metrics collector endpoint"; BuildDependentConfig telemetry_config( @@ -216,16 +252,26 @@ void Server::InitializeTelemetry(const ParameterClient& parameter_client, environment_), metrics_options, metrics_collector_endpoint)); AddSystemMetric(context_map); + + auto* internal_lookup_context_map = InternalLookupServerContextMap( + telemetry_config, + ConfigurePrivateMetrics( + CreateKVAttributes(instance_id, std::to_string(shard_num_), + environment_), + metrics_options, metrics_collector_endpoint)); + // TODO(b/300137699): Deprecate ConfigureMetrics once all metrics are migrated // to new telemetry API ConfigureMetrics( CreateKVAttributes(instance_id, std::to_string(shard_num_), environment_), metrics_options, metrics_collector_endpoint); - ConfigureTracer(CreateKVAttributes(std::move(instance_id), - std::to_string(shard_num_), environment_), - metrics_collector_endpoint); - - metrics_recorder_ = TelemetryProvider::GetInstance().CreateMetricsRecorder(); + ConfigureTracer( + CreateKVAttributes(instance_id, std::to_string(shard_num_), environment_), + metrics_collector_endpoint); + ParameterFetcher parameter_fetcher(environment_, parameter_client); + InitOtelLogger(CreateKVAttributes(std::move(instance_id), + std::to_string(shard_num_), environment_), + metrics_collector_endpoint, parameter_fetcher); LOG(INFO) << "Done init telemetry"; } @@ -247,13 +293,13 @@ absl::Status Server::CreateDefaultInstancesIfNecessaryAndGetEnvironment( parameter_fetcher.GetInt32Parameter(kUdfNumWorkersParameterSuffix); int32_t udf_timeout_ms = parameter_fetcher.GetInt32Parameter(kUdfTimeoutMillisParameterSuffix); + int32_t udf_min_log_level = + parameter_fetcher.GetInt32Parameter(kUdfMinLogLevelParameterSuffix); // updating verbosity level flag as early as we can, as it affects all logging // downstream. - // see - // https://github.com/google/glog/blob/931323df212c46e3a01b743d761c6ab8dc9f0d09/README.rst#setting-flags - FLAGS_v = parameter_fetcher.GetInt32Parameter( - kLoggingVerbosityLevelParameterSuffix); + absl::SetGlobalVLogLevel(parameter_fetcher.GetInt32Parameter( + kLoggingVerbosityLevelParameterSuffix)); if (udf_client != nullptr) { udf_client_ = std::move(udf_client); return absl::OkStatus(); @@ -267,10 +313,10 @@ absl::Status Server::CreateDefaultInstancesIfNecessaryAndGetEnvironment( .RegisterStringGetValuesHook(*string_get_values_hook_) .RegisterBinaryGetValuesHook(*binary_get_values_hook_) .RegisterRunQueryHook(*run_query_hook_) - .RegisterLoggingHook() + .RegisterLoggingFunction() .SetNumberOfWorkers(number_of_workers) .Config()), - absl::Milliseconds(udf_timeout_ms)); + absl::Milliseconds(udf_timeout_ms), udf_min_log_level); if (udf_client_or_status.ok()) { udf_client_ = std::move(*udf_client_or_status); } @@ -346,7 +392,11 @@ absl::Status Server::InitOnceInstancesAreCreated() { return status; } - SetDefaultUdfCodeObject(); + if (absl::Status status = SetDefaultUdfCodeObject(); !status.ok()) { + return absl::InternalError( + "Error setting default UDF. Please contact Google to fix the default " + "UDF or retry starting the server."); + } num_shards_ = parameter_fetcher.GetInt32Parameter(kNumShardsParameterSuffix); LOG(INFO) << "Retrieved " << kNumShardsParameterSuffix @@ -368,12 +418,11 @@ absl::Status Server::InitOnceInstancesAreCreated() { SetQueueManager(metadata, message_service_blob_.get()); grpc_server_ = CreateAndStartGrpcServer(); - local_lookup_ = CreateLocalLookup(*cache_, *metrics_recorder_); + local_lookup_ = CreateLocalLookup(*cache_); auto key_sharder = GetKeySharder(parameter_fetcher); auto server_initializer = GetServerInitializer( - num_shards_, *metrics_recorder_, *key_fetcher_manager_, *local_lookup_, - environment_, shard_num_, *instance_client_, *cache_, parameter_fetcher, - key_sharder); + num_shards_, *key_fetcher_manager_, *local_lookup_, environment_, + shard_num_, *instance_client_, *cache_, parameter_fetcher, key_sharder); remote_lookup_ = server_initializer->CreateAndStartRemoteLookupServer(); { auto status_or_notifier = @@ -564,6 +613,7 @@ std::unique_ptr Server::CreateDataOrchestrator( .shard_num = shard_num_, .num_shards = num_shards_, .key_sharder = std::move(key_sharder), + .blob_prefix_allowlist = GetBlobPrefixAllowlist(parameter_fetcher), }); }, "CreateDataOrchestrator", metrics_callback); @@ -571,18 +621,19 @@ std::unique_ptr Server::CreateDataOrchestrator( void Server::CreateGrpcServices(const ParameterFetcher& parameter_fetcher) { const bool use_v2 = parameter_fetcher.GetBoolParameter(kRouteV1ToV2Suffix); + const bool add_missing_keys_v1 = + parameter_fetcher.GetBoolParameter(kAddMissingKeysV1Suffix); LOG(INFO) << "Retrieved " << kRouteV1ToV2Suffix << " parameter: " << use_v2; get_values_adapter_ = GetValuesAdapter::Create(std::make_unique( - *udf_client_, *metrics_recorder_, *key_fetcher_manager_)); - GetValuesHandler handler(*cache_, *get_values_adapter_, *metrics_recorder_, - use_v2); - grpc_services_.push_back(std::make_unique( - std::move(handler), *metrics_recorder_)); - GetValuesV2Handler v2handler(*udf_client_, *metrics_recorder_, - *key_fetcher_manager_); - grpc_services_.push_back(std::make_unique( - std::move(v2handler), *metrics_recorder_)); + *udf_client_, *key_fetcher_manager_)); + GetValuesHandler handler(*cache_, *get_values_adapter_, use_v2, + add_missing_keys_v1); + grpc_services_.push_back( + std::make_unique(std::move(handler))); + GetValuesV2Handler v2handler(*udf_client_, *key_fetcher_manager_); + grpc_services_.push_back( + std::make_unique(std::move(v2handler))); } std::unique_ptr Server::CreateAndStartGrpcServer() { @@ -595,6 +646,13 @@ std::unique_ptr Server::CreateAndStartGrpcServer() { builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); // Register "service" as the instance through which we'll communicate with // clients. In this case it corresponds to a *synchronous* service. + + // Increase metadata size, this includes, for example the HTTP URL. Default is + // 8KB. + builder.AddChannelArgument(GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE, + 32 * 1024); // Set to 32KB + builder.AddChannelArgument(GRPC_ARG_MAX_METADATA_SIZE, + 32 * 1024); // Set to 32KB for (auto& service : grpc_services_) { builder.RegisterService(service.get()); } @@ -608,15 +666,13 @@ std::unique_ptr Server::CreateAndStartGrpcServer() { return std::move(server); } -void Server::SetDefaultUdfCodeObject() { +absl::Status Server::SetDefaultUdfCodeObject() { const absl::Status status = udf_client_->SetCodeObject( CodeConfig{.js = kDefaultUdfCodeSnippet, .udf_handler_name = kDefaultUdfHandlerName, .logical_commit_time = kDefaultLogicalCommitTime, .version = kDefaultVersion}); - if (!status.ok()) { - LOG(ERROR) << "Error setting code object: " << status; - } + return status; } std::unique_ptr Server::CreateDeltaFileNotifier( @@ -627,7 +683,8 @@ std::unique_ptr Server::CreateDeltaFileNotifier( << " parameter: " << backup_poll_frequency_secs; return DeltaFileNotifier::Create(*blob_client_, - absl::Seconds(backup_poll_frequency_secs)); + absl::Seconds(backup_poll_frequency_secs), + GetBlobPrefixAllowlist(parameter_fetcher)); } } // namespace kv_server diff --git a/components/data_server/server/server.h b/components/data_server/server/server.h index 47c4ed40..494a2dd4 100644 --- a/components/data_server/server/server.h +++ b/components/data_server/server/server.h @@ -37,6 +37,7 @@ #include "components/internal_server/lookup.h" #include "components/sharding/cluster_mappings_manager.h" #include "components/sharding/shard_manager.h" +#include "components/telemetry/open_telemetry_sink.h" #include "components/udf/hooks/get_values_hook.h" #include "components/udf/hooks/run_query_hook.h" #include "components/udf/udf_client.h" @@ -45,8 +46,7 @@ #include "public/base_types.pb.h" #include "public/query/get_values.grpc.pb.h" #include "public/sharding/key_sharder.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/telemetry/telemetry.h" namespace kv_server { @@ -98,11 +98,14 @@ class Server { absl::Status InitializeUdfHooks(); std::unique_ptr CreateAndStartRemoteLookupServer(); - void SetDefaultUdfCodeObject(); + absl::Status SetDefaultUdfCodeObject(); void InitializeTelemetry(const ParameterClient& parameter_client, InstanceClient& instance_client); absl::Status CreateShardManager(); + void InitOtelLogger(::opentelemetry::sdk::resource::Resource server_info, + absl::optional collector_endpoint, + const ParameterFetcher& parameter_fetcher); // This must be first, otherwise the AWS SDK will crash when it's called: PlatformInitializer platform_initializer_; @@ -110,8 +113,6 @@ class Server { std::unique_ptr parameter_client_; std::unique_ptr instance_client_; std::string environment_; - std::unique_ptr - metrics_recorder_; std::vector> grpc_services_; std::unique_ptr grpc_server_; std::unique_ptr cache_; @@ -153,6 +154,8 @@ class Server { std::unique_ptr key_fetcher_manager_; + std::unique_ptr log_provider_; + std::unique_ptr open_telemetry_sink_; }; } // namespace kv_server diff --git a/components/data_server/server/server_initializer.cc b/components/data_server/server/server_initializer.cc index f3550dc6..4499017f 100644 --- a/components/data_server/server/server_initializer.cc +++ b/components/data_server/server/server_initializer.cc @@ -18,13 +18,12 @@ #include +#include "absl/log/log.h" #include "components/internal_server/constants.h" #include "components/internal_server/local_lookup.h" #include "components/internal_server/lookup_server_impl.h" -#include "components/internal_server/remote_lookup_client.h" #include "components/internal_server/sharded_lookup.h" -#include "glog/logging.h" -#include "src/cpp/encryption/key_fetcher/src/key_fetcher_manager.h" +#include "src/encryption/key_fetcher/key_fetcher_manager.h" namespace kv_server { namespace { @@ -45,9 +44,7 @@ absl::Status InitializeUdfHooksInternal( class NonshardedServerInitializer : public ServerInitializer { public: - explicit NonshardedServerInitializer(MetricsRecorder& metrics_recorder, - Cache& cache) - : metrics_recorder_(metrics_recorder), cache_(cache) {} + explicit NonshardedServerInitializer(Cache& cache) : cache_(cache) {} RemoteLookup CreateAndStartRemoteLookupServer() override { RemoteLookup remote_lookup; @@ -59,9 +56,8 @@ class NonshardedServerInitializer : public ServerInitializer { GetValuesHook& binary_get_values_hook, RunQueryHook& run_query_hook) override { ShardManagerState shard_manager_state; - auto lookup_supplier = [&cache = cache_, - &metrics_recorder = metrics_recorder_]() { - return CreateLocalLookup(cache, metrics_recorder); + auto lookup_supplier = [&cache = cache_]() { + return CreateLocalLookup(cache); }; InitializeUdfHooksInternal(std::move(lookup_supplier), string_get_values_hook, binary_get_values_hook, @@ -70,20 +66,17 @@ class NonshardedServerInitializer : public ServerInitializer { } private: - MetricsRecorder& metrics_recorder_; Cache& cache_; }; class ShardedServerInitializer : public ServerInitializer { public: explicit ShardedServerInitializer( - MetricsRecorder& metrics_recorder, KeyFetcherManagerInterface& key_fetcher_manager, Lookup& local_lookup, std::string environment, int32_t num_shards, int32_t current_shard_num, InstanceClient& instance_client, ParameterFetcher& parameter_fetcher, KeySharder key_sharder) - : metrics_recorder_(metrics_recorder), - key_fetcher_manager_(key_fetcher_manager), + : key_fetcher_manager_(key_fetcher_manager), local_lookup_(local_lookup), environment_(environment), num_shards_(num_shards), @@ -95,7 +88,7 @@ class ShardedServerInitializer : public ServerInitializer { RemoteLookup CreateAndStartRemoteLookupServer() override { RemoteLookup remote_lookup; remote_lookup.remote_lookup_service = std::make_unique( - local_lookup_, key_fetcher_manager_, metrics_recorder_); + local_lookup_, key_fetcher_manager_); grpc::ServerBuilder remote_lookup_server_builder; auto remoteLookupServerAddress = absl::StrCat(kLocalIp, ":", kRemoteLookupServerPort); @@ -122,10 +115,9 @@ class ShardedServerInitializer : public ServerInitializer { num_shards = num_shards_, current_shard_num = current_shard_num_, &shard_manager = *maybe_shard_state->shard_manager, - &metrics_recorder = metrics_recorder_, &key_sharder = key_sharder_]() { return CreateShardedLookup(local_lookup, num_shards, current_shard_num, - shard_manager, metrics_recorder, key_sharder); + shard_manager, key_sharder); }; InitializeUdfHooksInternal(std::move(lookup_supplier), string_get_values_hook, binary_get_values_hook, @@ -143,8 +135,8 @@ class ShardedServerInitializer : public ServerInitializer { shard_manager_state.shard_manager = TraceRetryUntilOk( [&cluster_mappings_manager = *shard_manager_state.cluster_mappings_manager, - &num_shards = num_shards_, &key_fetcher_manager = key_fetcher_manager_, - &metrics_recorder = metrics_recorder_] { + &num_shards = num_shards_, + &key_fetcher_manager = key_fetcher_manager_] { // It might be that the cluster mappings that are passed don't pass // validation. E.g. a particular cluster might not have any // replicas @@ -153,7 +145,7 @@ class ShardedServerInitializer : public ServerInitializer { // at that point in time might have new replicas spun up. return ShardManager::Create( num_shards, key_fetcher_manager, - cluster_mappings_manager.GetClusterMappings(), metrics_recorder); + cluster_mappings_manager.GetClusterMappings()); }, "GetShardManager", LogStatusSafeMetricsFn()); auto start_status = shard_manager_state.cluster_mappings_manager->Start( @@ -163,8 +155,6 @@ class ShardedServerInitializer : public ServerInitializer { } return std::move(shard_manager_state); } - - MetricsRecorder& metrics_recorder_; KeyFetcherManagerInterface& key_fetcher_manager_; Lookup& local_lookup_; std::string environment_; @@ -178,20 +168,18 @@ class ShardedServerInitializer : public ServerInitializer { } // namespace std::unique_ptr GetServerInitializer( - int64_t num_shards, MetricsRecorder& metrics_recorder, - KeyFetcherManagerInterface& key_fetcher_manager, Lookup& local_lookup, - std::string environment, int32_t current_shard_num, + int64_t num_shards, KeyFetcherManagerInterface& key_fetcher_manager, + Lookup& local_lookup, std::string environment, int32_t current_shard_num, InstanceClient& instance_client, Cache& cache, ParameterFetcher& parameter_fetcher, KeySharder key_sharder) { CHECK_GT(num_shards, 0) << "num_shards must be greater than 0"; if (num_shards == 1) { - return std::make_unique(metrics_recorder, - cache); + return std::make_unique(cache); } return std::make_unique( - metrics_recorder, key_fetcher_manager, local_lookup, environment, - num_shards, current_shard_num, instance_client, parameter_fetcher, + key_fetcher_manager, local_lookup, environment, num_shards, + current_shard_num, instance_client, parameter_fetcher, std::move(key_sharder)); } } // namespace kv_server diff --git a/components/data_server/server/server_initializer.h b/components/data_server/server/server_initializer.h index cae89c93..c490c377 100644 --- a/components/data_server/server/server_initializer.h +++ b/components/data_server/server/server_initializer.h @@ -29,8 +29,7 @@ #include "components/udf/hooks/run_query_hook.h" #include "grpcpp/grpcpp.h" #include "public/sharding/key_sharder.h" -#include "src/cpp/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" -#include "src/cpp/telemetry/metrics_recorder.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" namespace kv_server { @@ -59,7 +58,7 @@ class ServerInitializer { }; std::unique_ptr GetServerInitializer( - int64_t num_shards, MetricsRecorder& metrics_recorder, + int64_t num_shards, privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager, Lookup& local_lookup, std::string environment, int32_t current_shard_num, diff --git a/components/data_server/server/server_local_test.cc b/components/data_server/server/server_local_test.cc index 35d00469..d6ae6cd8 100644 --- a/components/data_server/server/server_local_test.cc +++ b/components/data_server/server/server_local_test.cc @@ -53,6 +53,12 @@ void RegisterRequiredTelemetryExpectations(MockParameterClient& client) { EXPECT_CALL(client, GetInt32Parameter( "kv-server-environment-backup-poll-frequency-secs")) .WillOnce(::testing::Return(123)); + EXPECT_CALL(client, + GetBoolParameter("kv-server-environment-enable-otel-logger")) + .WillOnce(::testing::Return(false)); + EXPECT_CALL(client, GetParameter("kv-server-environment-telemetry-config", + testing::Eq(std::nullopt))) + .WillOnce(::testing::Return("mode: EXPERIMENT")); } void InitializeMetrics() { @@ -109,9 +115,15 @@ TEST(ServerLocalTest, InitFailsWithNoDeltaDirectory) { EXPECT_CALL(*parameter_client, GetInt32Parameter("kv-server-environment-udf-timeout-millis")) .WillOnce(::testing::Return(5000)); + EXPECT_CALL(*parameter_client, + GetInt32Parameter("kv-server-environment-udf-min-log-level")) + .WillOnce(::testing::Return(0)); EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-route-v1-to-v2")) .WillOnce(::testing::Return(false)); + EXPECT_CALL(*parameter_client, + GetBoolParameter("kv-server-environment-add-missing-keys-v1")) + .WillOnce(::testing::Return(false)); EXPECT_CALL( *parameter_client, GetInt32Parameter("kv-server-environment-logging-verbosity-level")) @@ -119,6 +131,11 @@ TEST(ServerLocalTest, InitFailsWithNoDeltaDirectory) { EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-use-sharding-key-regex")) .WillOnce(::testing::Return(false)); + EXPECT_CALL( + *parameter_client, + GetParameter("kv-server-environment-data-loading-blob-prefix-allowlist", + ::testing::Eq(""))) + .WillOnce(::testing::Return("")); kv_server::Server server; absl::Status status = server.Init(std::move(parameter_client), std::move(instance_client), @@ -167,9 +184,15 @@ TEST(ServerLocalTest, InitPassesWithDeltaDirectoryAndRealtimeDirectory) { EXPECT_CALL(*parameter_client, GetInt32Parameter("kv-server-environment-udf-timeout-millis")) .WillOnce(::testing::Return(5000)); + EXPECT_CALL(*parameter_client, + GetInt32Parameter("kv-server-environment-udf-min-log-level")) + .WillOnce(::testing::Return(0)); EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-route-v1-to-v2")) .WillOnce(::testing::Return(false)); + EXPECT_CALL(*parameter_client, + GetBoolParameter("kv-server-environment-add-missing-keys-v1")) + .WillOnce(::testing::Return(false)); EXPECT_CALL( *parameter_client, GetInt32Parameter("kv-server-environment-logging-verbosity-level")) @@ -179,7 +202,12 @@ TEST(ServerLocalTest, InitPassesWithDeltaDirectoryAndRealtimeDirectory) { .WillOnce(::testing::Return(false)); EXPECT_CALL(*mock_udf_client, SetCodeObject(_)) .WillOnce(testing::Return(absl::OkStatus())); - + EXPECT_CALL( + *parameter_client, + GetParameter("kv-server-environment-data-loading-blob-prefix-allowlist", + ::testing::Eq(""))) + .Times(2) + .WillRepeatedly(::testing::Return("")); kv_server::Server server; absl::Status status = server.Init(std::move(parameter_client), std::move(instance_client), @@ -231,15 +259,26 @@ TEST(ServerLocalTest, GracefulServerShutdown) { EXPECT_CALL(*parameter_client, GetInt32Parameter("kv-server-environment-udf-timeout-millis")) .WillOnce(::testing::Return(5000)); + EXPECT_CALL(*parameter_client, + GetInt32Parameter("kv-server-environment-udf-min-log-level")) + .WillOnce(::testing::Return(0)); EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-route-v1-to-v2")) .WillOnce(::testing::Return(false)); + EXPECT_CALL(*parameter_client, + GetBoolParameter("kv-server-environment-add-missing-keys-v1")) + .WillOnce(::testing::Return(false)); EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-use-sharding-key-regex")) .WillOnce(::testing::Return(false)); EXPECT_CALL(*mock_udf_client, SetCodeObject(_)) .WillOnce(testing::Return(absl::OkStatus())); - + EXPECT_CALL( + *parameter_client, + GetParameter("kv-server-environment-data-loading-blob-prefix-allowlist", + ::testing::Eq(""))) + .Times(2) + .WillRepeatedly(::testing::Return("")); kv_server::Server server; absl::Status status = server.Init(std::move(parameter_client), std::move(instance_client), @@ -291,9 +330,15 @@ TEST(ServerLocalTest, ForceServerShutdown) { EXPECT_CALL(*parameter_client, GetInt32Parameter("kv-server-environment-udf-timeout-millis")) .WillOnce(::testing::Return(5000)); + EXPECT_CALL(*parameter_client, + GetInt32Parameter("kv-server-environment-udf-min-log-level")) + .WillOnce(::testing::Return(0)); EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-route-v1-to-v2")) .WillOnce(::testing::Return(false)); + EXPECT_CALL(*parameter_client, + GetBoolParameter("kv-server-environment-add-missing-keys-v1")) + .WillOnce(::testing::Return(false)); EXPECT_CALL( *parameter_client, GetInt32Parameter("kv-server-environment-logging-verbosity-level")) @@ -301,10 +346,14 @@ TEST(ServerLocalTest, ForceServerShutdown) { EXPECT_CALL(*parameter_client, GetBoolParameter("kv-server-environment-use-sharding-key-regex")) .WillOnce(::testing::Return(false)); - EXPECT_CALL(*mock_udf_client, SetCodeObject(_)) .WillOnce(testing::Return(absl::OkStatus())); - + EXPECT_CALL( + *parameter_client, + GetParameter("kv-server-environment-data-loading-blob-prefix-allowlist", + ::testing::Eq(""))) + .Times(2) + .WillRepeatedly(::testing::Return("")); kv_server::Server server; absl::Status status = server.Init(std::move(parameter_client), std::move(instance_client), diff --git a/components/errors/BUILD.bazel b/components/errors/BUILD.bazel index de95448e..29aaccd8 100644 --- a/components/errors/BUILD.bazel +++ b/components/errors/BUILD.bazel @@ -17,6 +17,7 @@ load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") package(default_visibility = [ "//components:__subpackages__", "//production/packaging:__subpackages__", + "//public/data_loading:__subpackages__", ]) cc_library( @@ -86,10 +87,10 @@ cc_library( deps = [ "//components/telemetry:server_definition", "//components/util:sleepfor", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/time", - "@google_privacysandbox_servers_common//src/cpp/telemetry:tracing", + "@google_privacysandbox_servers_common//src/telemetry:tracing", ], ) @@ -103,6 +104,17 @@ cc_test( ":retry", "//components/util:sleepfor_mock", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + ], +) + +cc_library( + name = "error_tag", + srcs = [ + "error_tag.h", + ], + deps = [ + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:cord", ], ) diff --git a/components/errors/error_tag.h b/components/errors/error_tag.h new file mode 100644 index 00000000..0cca4a2e --- /dev/null +++ b/components/errors/error_tag.h @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_ERRORS_ERROR_TAG_H_ +#define COMPONENTS_ERRORS_ERROR_TAG_H_ + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/cord.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" + +namespace kv_server { + +// Sets the payload for an absl::Status +// The payload key is the file_name extracted from file_path +// The payload value is the error_tag_enum +template +inline absl::Status StatusWithErrorTag(absl::Status status, + std::string_view file_path, + T error_tag_enum) { + std::vector file_segments = absl::StrSplit(file_path, "/"); + status.SetPayload(file_segments.back(), + absl::Cord(absl::StrCat(error_tag_enum))); + return status; +} + +} // namespace kv_server +#endif // COMPONENTS_ERRORS_ERROR_TAG_H_ diff --git a/components/errors/retry.h b/components/errors/retry.h index 6879deb1..8f033af0 100644 --- a/components/errors/retry.h +++ b/components/errors/retry.h @@ -19,12 +19,12 @@ #include #include +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/time/time.h" #include "components/telemetry/server_definition.h" #include "components/util/sleepfor.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/tracing.h" +#include "src/telemetry/tracing.h" namespace kv_server { @@ -91,7 +91,7 @@ class RetryableWithMax { // Retries functors that return an absl::StatusOr until they are `ok`. // The value of type T is returned by this function. -// `metrics_recorder` is optional. +// `metrics_callback` is optional. template typename std::invoke_result_t>::value_type RetryUntilOk( Func&& f, std::string task_name, @@ -105,7 +105,7 @@ typename std::invoke_result_t>::value_type RetryUntilOk( } // Same as above `RetryUntilOk`, wrapped in an `opentelemetry::trace::Span`. // Each individual retry of `func` is also traced. -// `metrics_recorder` is optional. +// `metrics_callback` is optional. template typename std::invoke_result_t>::value_type TraceRetryUntilOk( @@ -126,7 +126,7 @@ TraceRetryUntilOk( } // Retries functors that return an absl::Status until they are `ok`. -// `metrics_recorder` is optional. +// `metrics_callback` is optional. inline void RetryUntilOk( std::function func, std::string task_name, const absl::AnyInvocable& @@ -140,7 +140,7 @@ inline void RetryUntilOk( // Starts and `opentelemetry::trace::Span` and Calls `RetryUntilOk`. // Each individual retry of `func` is also traced. -// `metrics_recorder` is optional. +// `metrics_callback` is optional. void TraceRetryUntilOk(std::function func, std::string task_name, const absl::AnyInvocable func, // Retries functors that return an absl::StatusOr until they are `ok` or // max_attempts is reached. Retry starts at 1. -// `metrics_recorder` is optional. +// `metrics_callback` is optional. template typename std::invoke_result_t> RetryWithMax( Func&& f, std::string task_name, int max_attempts, diff --git a/components/errors/retry_test.cc b/components/errors/retry_test.cc index b9d2f363..d5349bdb 100644 --- a/components/errors/retry_test.cc +++ b/components/errors/retry_test.cc @@ -23,7 +23,6 @@ #include "components/util/sleepfor_mock.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/components/internal_server/BUILD.bazel b/components/internal_server/BUILD.bazel index 1e44b772..ae82b089 100644 --- a/components/internal_server/BUILD.bazel +++ b/components/internal_server/BUILD.bazel @@ -39,8 +39,7 @@ cc_library( "//components/query:scanner", "@com_github_grpc_grpc//:grpc++", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", + "@google_privacysandbox_servers_common//src/telemetry", ], ) @@ -60,8 +59,7 @@ cc_test( "//public/test_util:proto_matcher", "@com_github_grpc_grpc//:grpc++", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", ], ) @@ -93,6 +91,7 @@ proto_library( srcs = ["lookup.proto"], deps = [ "@com_google_googleapis//google/rpc:status_proto", + "@google_privacysandbox_servers_common//src/logger:logger_proto", ], ) @@ -126,6 +125,7 @@ cc_library( hdrs = ["lookup.h"], deps = [ ":internal_lookup_cc_proto", + "//components/util:request_context", "@com_google_absl//absl/status:statusor", ], ) @@ -141,9 +141,8 @@ cc_library( "//components/data_server/cache", "//components/query:driver", "//components/query:scanner", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -161,13 +160,11 @@ cc_library( "//components/query:scanner", "//components/sharding:shard_manager", "//public/sharding:key_sharder", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", "@com_google_absl//absl/status:statusor", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -185,8 +182,7 @@ cc_test( "//components/sharding:mocks", "//public/test_util:proto_matcher", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", ], ) @@ -203,13 +199,12 @@ cc_library( ":internal_lookup_cc_grpc", ":string_padder", "//components/data_server/request_handler:ohttp_client_encryptor", - "@com_github_google_glog//:glog", + "//components/util:request_context", "@com_github_grpc_grpc//:grpc++", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -222,8 +217,8 @@ cc_library( "string_padder.h", ], deps = [ - "@com_github_google_glog//:glog", "@com_github_google_quiche//quiche:quiche_unstable_api", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -256,8 +251,7 @@ cc_test( "//components/data_server/cache:mocks", "//public/test_util:proto_matcher", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", ], ) @@ -273,6 +267,5 @@ cc_test( "//public/test_util:proto_matcher", "@com_google_googletest//:gtest_main", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) diff --git a/components/internal_server/local_lookup.cc b/components/internal_server/local_lookup.cc index 872791b7..45522e49 100644 --- a/components/internal_server/local_lookup.cc +++ b/components/internal_server/local_lookup.cc @@ -20,51 +20,49 @@ #include #include +#include "absl/log/log.h" #include "components/data_server/cache/cache.h" #include "components/internal_server/lookup.h" #include "components/internal_server/lookup.pb.h" #include "components/query/driver.h" #include "components/query/scanner.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { namespace { -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::ScopeLatencyRecorder; - -constexpr char kKeySetNotFound[] = "KeysetNotFound"; -constexpr char kLocalRunQuery[] = "LocalRunQuery"; - class LocalLookup : public Lookup { public: - explicit LocalLookup(const Cache& cache, MetricsRecorder& metrics_recorder) - : cache_(cache), metrics_recorder_(metrics_recorder) {} + explicit LocalLookup(const Cache& cache) : cache_(cache) {} absl::StatusOr GetKeyValues( + const RequestContext& request_context, const absl::flat_hash_set& keys) const override { - return ProcessKeys(keys); + return ProcessKeys(request_context, keys); } absl::StatusOr GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const override { - return ProcessKeysetKeys(key_set); + return ProcessKeysetKeys(request_context, key_set); } absl::StatusOr RunQuery( - std::string query) const override { - return ProcessQuery(query); + const RequestContext& request_context, std::string query) const override { + return ProcessQuery(request_context, query); } private: InternalLookupResponse ProcessKeys( + const RequestContext& request_context, const absl::flat_hash_set& keys) const { + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetInternalLookupMetricsContext()); InternalLookupResponse response; if (keys.empty()) { return response; } - auto kv_pairs = cache_.GetKeyValuePairs(keys); + auto kv_pairs = cache_.GetKeyValuePairs(request_context, keys); for (const auto& key : keys) { SingleLookupResult result; @@ -72,7 +70,7 @@ class LocalLookup : public Lookup { if (key_iter == kv_pairs.end()) { auto status = result.mutable_status(); status->set_code(static_cast(absl::StatusCode::kNotFound)); - status->set_message("Key not found"); + status->set_message(absl::StrCat("Key not found: ", key)); } else { result.set_value(std::move(key_iter->second)); } @@ -82,20 +80,23 @@ class LocalLookup : public Lookup { } absl::StatusOr ProcessKeysetKeys( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const { + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetInternalLookupMetricsContext()); InternalLookupResponse response; if (key_set.empty()) { return response; } - auto key_value_set_result = cache_.GetKeyValueSet(key_set); + auto key_value_set_result = cache_.GetKeyValueSet(request_context, key_set); for (const auto& key : key_set) { SingleLookupResult result; const auto value_set = key_value_set_result->GetValueSet(key); if (value_set.empty()) { auto status = result.mutable_status(); status->set_code(static_cast(absl::StatusCode::kNotFound)); - status->set_message("Key not found"); - metrics_recorder_.IncrementEventCounter(kKeySetNotFound); + status->set_message(absl::StrCat("Key not found: ", key)); } else { auto keyset_values = result.mutable_keyset_values(); keyset_values->mutable_values()->Add(value_set.begin(), @@ -107,9 +108,10 @@ class LocalLookup : public Lookup { } absl::StatusOr ProcessQuery( - std::string query) const { - ScopeLatencyRecorder latency_recorder(std::string(kLocalRunQuery), - metrics_recorder_); + const RequestContext& request_context, std::string query) const { + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetInternalLookupMetricsContext()); if (query.empty()) return absl::OkStatus(); std::unique_ptr get_key_value_set_result; kv_server::Driver driver([&get_key_value_set_result](std::string_view key) { @@ -121,30 +123,32 @@ class LocalLookup : public Lookup { kv_server::Parser parse(driver, scanner); int parse_result = parse(); if (parse_result) { + LogInternalLookupRequestErrorMetric( + request_context.GetInternalLookupMetricsContext(), + kLocalRunQueryParsingFailure); return absl::InvalidArgumentError("Parsing failure."); } get_key_value_set_result = - cache_.GetKeyValueSet(driver.GetRootNode()->Keys()); + cache_.GetKeyValueSet(request_context, driver.GetRootNode()->Keys()); auto result = driver.GetResult(); if (!result.ok()) { + LogInternalLookupRequestErrorMetric( + request_context.GetInternalLookupMetricsContext(), + kLocalRunQueryFailure); return result.status(); } InternalRunQueryResponse response; response.mutable_elements()->Assign(result->begin(), result->end()); return response; } - const Cache& cache_; - MetricsRecorder& metrics_recorder_; }; } // namespace -std::unique_ptr CreateLocalLookup( - const Cache& cache, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) { - return std::make_unique(cache, metrics_recorder); +std::unique_ptr CreateLocalLookup(const Cache& cache) { + return std::make_unique(cache); } } // namespace kv_server diff --git a/components/internal_server/local_lookup.h b/components/internal_server/local_lookup.h index d2027f9a..2bc5138c 100644 --- a/components/internal_server/local_lookup.h +++ b/components/internal_server/local_lookup.h @@ -21,13 +21,10 @@ #include "components/data_server/cache/cache.h" #include "components/internal_server/lookup.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { -std::unique_ptr CreateLocalLookup( - const Cache& cache, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder); +std::unique_ptr CreateLocalLookup(const Cache& cache); } // namespace kv_server diff --git a/components/internal_server/local_lookup_test.cc b/components/internal_server/local_lookup_test.cc index ad78aad3..bddb646b 100644 --- a/components/internal_server/local_lookup_test.cc +++ b/components/internal_server/local_lookup_test.cc @@ -24,30 +24,37 @@ #include "google/protobuf/text_format.h" #include "gtest/gtest.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { using google::protobuf::TextFormat; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::Return; using testing::ReturnRef; class LocalLookupTest : public ::testing::Test { protected: + LocalLookupTest() { + InitMetricsContextMap(); + scope_metrics_context_ = std::make_unique(); + request_context_ = + std::make_unique(*scope_metrics_context_); + } + RequestContext& GetRequestContext() { return *request_context_; } + std::unique_ptr scope_metrics_context_; + std::unique_ptr request_context_; MockCache mock_cache_; - MockMetricsRecorder mock_metrics_recorder_; }; TEST_F(LocalLookupTest, GetKeyValues_KeysFound_Success) { - EXPECT_CALL(mock_cache_, GetKeyValuePairs(_)) + EXPECT_CALL(mock_cache_, GetKeyValuePairs(_, _)) .WillOnce(Return(absl::flat_hash_map{ {"key1", "value1"}, {"key2", "value2"}})); - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValues({"key1", "key2"}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = + local_lookup->GetKeyValues(GetRequestContext(), {"key1", "key2"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -65,12 +72,13 @@ TEST_F(LocalLookupTest, GetKeyValues_KeysFound_Success) { } TEST_F(LocalLookupTest, GetKeyValues_DuplicateKeys_Success) { - EXPECT_CALL(mock_cache_, GetKeyValuePairs(_)) + EXPECT_CALL(mock_cache_, GetKeyValuePairs(_, _)) .WillOnce(Return(absl::flat_hash_map{ {"key1", "value1"}, {"key2", "value2"}})); - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValues({"key1", "key1"}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = + local_lookup->GetKeyValues(GetRequestContext(), {"key1", "key1"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -84,12 +92,13 @@ TEST_F(LocalLookupTest, GetKeyValues_DuplicateKeys_Success) { } TEST_F(LocalLookupTest, GetKeyValues_KeyMissing_ReturnsStatusForKey) { - EXPECT_CALL(mock_cache_, GetKeyValuePairs(_)) + EXPECT_CALL(mock_cache_, GetKeyValuePairs(_, _)) .WillOnce(Return( absl::flat_hash_map{{"key1", "value1"}})); - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValues({"key1", "key2"}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = + local_lookup->GetKeyValues(GetRequestContext(), {"key1", "key2"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -100,7 +109,7 @@ TEST_F(LocalLookupTest, GetKeyValues_KeyMissing_ReturnsStatusForKey) { } kv_pairs { key: "key2" - value { status { code: 5 message: "Key not found" } } + value { status { code: 5 message: "Key not found: key2" } } } )pb", &expected); @@ -108,8 +117,8 @@ TEST_F(LocalLookupTest, GetKeyValues_KeyMissing_ReturnsStatusForKey) { } TEST_F(LocalLookupTest, GetKeyValues_EmptyRequest_ReturnsEmptyResponse) { - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValues({}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = local_lookup->GetKeyValues(GetRequestContext(), {}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -122,11 +131,11 @@ TEST_F(LocalLookupTest, GetKeyValueSets_KeysFound_Success) { EXPECT_CALL(*mock_get_key_value_set_result, GetValueSet("key1")) .WillOnce( Return(absl::flat_hash_set{"value1", "value2"})); - EXPECT_CALL(mock_cache_, GetKeyValueSet(_)) + EXPECT_CALL(mock_cache_, GetKeyValueSet(_, _)) .WillOnce(Return(std::move(mock_get_key_value_set_result))); - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValueSet({"key1"}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = local_lookup->GetKeyValueSet(GetRequestContext(), {"key1"}); EXPECT_TRUE(response.ok()); std::vector expected_resulting_set = {"value1", "value2"}; @@ -139,18 +148,18 @@ TEST_F(LocalLookupTest, GetKeyValueSets_SetEmpty_Success) { std::make_unique(); EXPECT_CALL(*mock_get_key_value_set_result, GetValueSet("key1")) .WillOnce(Return(absl::flat_hash_set{})); - EXPECT_CALL(mock_cache_, GetKeyValueSet(_)) + EXPECT_CALL(mock_cache_, GetKeyValueSet(_, _)) .WillOnce(Return(std::move(mock_get_key_value_set_result))); - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValueSet({"key1"}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = local_lookup->GetKeyValueSet(GetRequestContext(), {"key1"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; TextFormat::ParseFromString( R"pb(kv_pairs { key: "key1" - value { status { code: 5 message: "Key not found" } } + value { status { code: 5 message: "Key not found: key1" } } } )pb", &expected); @@ -158,8 +167,8 @@ TEST_F(LocalLookupTest, GetKeyValueSets_SetEmpty_Success) { } TEST_F(LocalLookupTest, GetKeyValueSet_EmptyRequest_ReturnsEmptyResponse) { - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->GetKeyValueSet({}); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = local_lookup->GetKeyValueSet(GetRequestContext(), {}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -174,12 +183,13 @@ TEST_F(LocalLookupTest, RunQuery_Success) { EXPECT_CALL(*mock_get_key_value_set_result, GetValueSet("someset")) .WillOnce( Return(absl::flat_hash_set{"value1", "value2"})); - EXPECT_CALL(mock_cache_, - GetKeyValueSet(absl::flat_hash_set{"someset"})) + EXPECT_CALL( + mock_cache_, + GetKeyValueSet(_, absl::flat_hash_set{"someset"})) .WillOnce(Return(std::move(mock_get_key_value_set_result))); - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->RunQuery(query); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = local_lookup->RunQuery(GetRequestContext(), query); EXPECT_TRUE(response.ok()); InternalRunQueryResponse expected; @@ -190,8 +200,8 @@ TEST_F(LocalLookupTest, RunQuery_Success) { TEST_F(LocalLookupTest, RunQuery_ParsingError_Error) { std::string query = "someset|("; - auto local_lookup = CreateLocalLookup(mock_cache_, mock_metrics_recorder_); - auto response = local_lookup->RunQuery(query); + auto local_lookup = CreateLocalLookup(mock_cache_); + auto response = local_lookup->RunQuery(GetRequestContext(), query); EXPECT_FALSE(response.ok()); EXPECT_EQ(response.status().code(), absl::StatusCode::kInvalidArgument); } diff --git a/components/internal_server/lookup.h b/components/internal_server/lookup.h index dde3917e..dd3356e7 100644 --- a/components/internal_server/lookup.h +++ b/components/internal_server/lookup.h @@ -24,6 +24,7 @@ #include "absl/container/flat_hash_set.h" #include "absl/status/statusor.h" #include "components/internal_server/lookup.pb.h" +#include "components/util/request_context.h" namespace kv_server { @@ -33,13 +34,15 @@ class Lookup { virtual ~Lookup() = default; virtual absl::StatusOr GetKeyValues( + const RequestContext& request_context, const absl::flat_hash_set& keys) const = 0; virtual absl::StatusOr GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const = 0; virtual absl::StatusOr RunQuery( - std::string query) const = 0; + const RequestContext& request_context, std::string query) const = 0; }; } // namespace kv_server diff --git a/components/internal_server/lookup.proto b/components/internal_server/lookup.proto index bc73389c..11d5c0ee 100644 --- a/components/internal_server/lookup.proto +++ b/components/internal_server/lookup.proto @@ -17,6 +17,7 @@ syntax = "proto3"; package kv_server; import "google/rpc/status.proto"; +import "src/logger/logger.proto"; // Internal Lookup Service API. service InternalLookupService { @@ -39,6 +40,10 @@ message InternalLookupRequest { // False means values are looked up. // True means value sets are looked up. bool lookup_sets = 2; + // Context useful for logging and tracing requests + privacy_sandbox.server_common.LogContext log_context = 3; + // Consented debugging configuration + privacy_sandbox.server_common.ConsentedDebugConfiguration consented_debug_config = 4; } // Encrypted and padded lookup request for internal datastore. @@ -92,6 +97,10 @@ message KeysetValues { message InternalRunQueryRequest { // Query to run. optional string query = 1; + // Context useful for logging and tracing requests + privacy_sandbox.server_common.LogContext log_context = 2; + // Consented debugging configuration + privacy_sandbox.server_common.ConsentedDebugConfiguration consented_debug_config = 3; } // Run Query response. diff --git a/components/internal_server/lookup_server_impl.cc b/components/internal_server/lookup_server_impl.cc index ba5bbaff..ed9b5715 100644 --- a/components/internal_server/lookup_server_impl.cc +++ b/components/internal_server/lookup_server_impl.cc @@ -21,50 +21,45 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "components/data_server/request_handler/ohttp_server_encryptor.h" #include "components/internal_server/lookup.grpc.pb.h" #include "components/internal_server/lookup.h" #include "components/internal_server/string_padder.h" -#include "glog/logging.h" #include "google/protobuf/message.h" #include "grpcpp/grpcpp.h" -#include "src/cpp/telemetry/telemetry.h" namespace kv_server { using google::protobuf::RepeatedPtrField; -using privacy_sandbox::server_common::ScopeLatencyRecorder; using grpc::StatusCode; -constexpr char kKeySetNotFound[] = "KeysetNotFound"; -constexpr char kDecryptionError[] = "DecryptionError"; -constexpr char kUnpaddingError[] = "UnpaddingError"; -constexpr char kEncryptionError[] = "EncryptionError"; -constexpr char kDeserializationError[] = "DeserializationError"; -constexpr char kRunQueryError[] = "RunQueryError"; -constexpr char kSecureLookup[] = "SecureLookup"; grpc::Status LookupServiceImpl::ToInternalGrpcStatus( - const absl::Status& status, const char* eventName) const { - metrics_recorder_.IncrementEventCounter(eventName); + const RequestContext& request_context, const absl::Status& status, + std::string_view error_code) const { + LogInternalLookupRequestErrorMetric( + request_context.GetInternalLookupMetricsContext(), error_code); return grpc::Status(StatusCode::INTERNAL, absl::StrCat(status.code(), " : ", status.message())); } -void LookupServiceImpl::ProcessKeys(const RepeatedPtrField& keys, +void LookupServiceImpl::ProcessKeys(const RequestContext& request_context, + const RepeatedPtrField& keys, InternalLookupResponse& response) const { if (keys.empty()) return; absl::flat_hash_set key_set; for (const auto& key : keys) { key_set.insert(key); } - auto lookup_result = lookup_.GetKeyValues(key_set); + auto lookup_result = lookup_.GetKeyValues(request_context, key_set); if (lookup_result.ok()) { response = *std::move(lookup_result); } } void LookupServiceImpl::ProcessKeysetKeys( + const RequestContext& request_context, const RepeatedPtrField& keys, InternalLookupResponse& response) const { if (keys.empty()) return; @@ -72,7 +67,7 @@ void LookupServiceImpl::ProcessKeysetKeys( for (const auto& key : keys) { key_list.insert(key); } - auto key_value_set_result = lookup_.GetKeyValueSet(key_list); + auto key_value_set_result = lookup_.GetKeyValueSet(request_context, key_list); if (key_value_set_result.ok()) { response = *std::move(key_value_set_result); } @@ -81,11 +76,13 @@ void LookupServiceImpl::ProcessKeysetKeys( grpc::Status LookupServiceImpl::InternalLookup( grpc::ServerContext* context, const InternalLookupRequest* request, InternalLookupResponse* response) { + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); if (context->IsCancelled()) { return grpc::Status(grpc::StatusCode::CANCELLED, "Deadline exceeded or client cancelled, abandoning."); } - ProcessKeys(request->keys(), *response); + ProcessKeys(request_context, request->keys(), *response); return grpc::Status::OK; } @@ -93,8 +90,13 @@ grpc::Status LookupServiceImpl::SecureLookup( grpc::ServerContext* context, const SecureLookupRequest* secure_lookup_request, SecureLookupResponse* secure_response) { - ScopeLatencyRecorder latency_recorder(std::string(kSecureLookup), - metrics_recorder_); + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); + LogIfError(request_context.GetInternalLookupMetricsContext() + .AccumulateMetric(1)); + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetInternalLookupMetricsContext()); if (context->IsCancelled()) { return grpc::Status(grpc::StatusCode::CANCELLED, "Deadline exceeded or client cancelled, abandoning."); @@ -105,17 +107,18 @@ grpc::Status LookupServiceImpl::SecureLookup( auto padded_serialized_request_maybe = encryptor.DecryptRequest(secure_lookup_request->ohttp_request()); if (!padded_serialized_request_maybe.ok()) { - return ToInternalGrpcStatus(padded_serialized_request_maybe.status(), - kDecryptionError); + return ToInternalGrpcStatus(request_context, + padded_serialized_request_maybe.status(), + kRequestDecryptionFailure); } VLOG(9) << "SecureLookup decrypted"; auto serialized_request_maybe = kv_server::Unpad(*padded_serialized_request_maybe); if (!serialized_request_maybe.ok()) { - metrics_recorder_.IncrementEventCounter(kDeserializationError); - return ToInternalGrpcStatus(serialized_request_maybe.status(), - kUnpaddingError); + return ToInternalGrpcStatus(request_context, + serialized_request_maybe.status(), + kRequestUnpaddingError); } VLOG(9) << "SecureLookup unpadded"; @@ -125,7 +128,8 @@ grpc::Status LookupServiceImpl::SecureLookup( "Failed parsing incoming request"); } - auto payload_to_encrypt = GetPayload(request.lookup_sets(), request.keys()); + auto payload_to_encrypt = + GetPayload(request_context, request.lookup_sets(), request.keys()); if (payload_to_encrypt.empty()) { // we cannot encrypt an empty payload. Note, that soon we will add logic // to pad responses, so this branch will never be hit. @@ -134,20 +138,22 @@ grpc::Status LookupServiceImpl::SecureLookup( auto encrypted_response_payload = encryptor.EncryptResponse(payload_to_encrypt); if (!encrypted_response_payload.ok()) { - return ToInternalGrpcStatus(encrypted_response_payload.status(), - kEncryptionError); + return ToInternalGrpcStatus(request_context, + encrypted_response_payload.status(), + kResponseEncryptionFailure); } secure_response->set_ohttp_response(*encrypted_response_payload); return grpc::Status::OK; } std::string LookupServiceImpl::GetPayload( - const bool lookup_sets, const RepeatedPtrField& keys) const { + const RequestContext& request_context, const bool lookup_sets, + const RepeatedPtrField& keys) const { InternalLookupResponse response; if (lookup_sets) { - ProcessKeysetKeys(keys, response); + ProcessKeysetKeys(request_context, keys, response); } else { - ProcessKeys(keys, response); + ProcessKeys(request_context, keys, response); } return response.SerializeAsString(); } @@ -155,13 +161,17 @@ std::string LookupServiceImpl::GetPayload( grpc::Status LookupServiceImpl::InternalRunQuery( grpc::ServerContext* context, const InternalRunQueryRequest* request, InternalRunQueryResponse* response) { + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); if (context->IsCancelled()) { return grpc::Status(grpc::StatusCode::CANCELLED, "Deadline exceeded or client cancelled, abandoning."); } - const auto process_result = lookup_.RunQuery(request->query()); + const auto process_result = + lookup_.RunQuery(request_context, request->query()); if (!process_result.ok()) { - return ToInternalGrpcStatus(process_result.status(), kRunQueryError); + return ToInternalGrpcStatus(request_context, process_result.status(), + kInternalRunQueryRequestFailure); } *response = *std::move(process_result); return grpc::Status::OK; diff --git a/components/internal_server/lookup_server_impl.h b/components/internal_server/lookup_server_impl.h index 4046a9e6..83fb9dfc 100644 --- a/components/internal_server/lookup_server_impl.h +++ b/components/internal_server/lookup_server_impl.h @@ -21,24 +21,20 @@ #include "components/internal_server/lookup.grpc.pb.h" #include "components/internal_server/lookup.h" +#include "components/util/request_context.h" #include "grpcpp/grpcpp.h" -#include "src/cpp/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" +#include "src/telemetry/telemetry.h" namespace kv_server { // Implements the internal lookup service for the data store. class LookupServiceImpl final : public kv_server::InternalLookupService::Service { public: - LookupServiceImpl( - const Lookup& lookup, - privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) - : lookup_(lookup), - key_fetcher_manager_(key_fetcher_manager), - metrics_recorder_(metrics_recorder) {} + LookupServiceImpl(const Lookup& lookup, + privacy_sandbox::server_common::KeyFetcherManagerInterface& + key_fetcher_manager) + : lookup_(lookup), key_fetcher_manager_(key_fetcher_manager) {} ~LookupServiceImpl() override = default; @@ -58,19 +54,21 @@ class LookupServiceImpl final private: std::string GetPayload( - const bool lookup_sets, + const RequestContext& request_context, const bool lookup_sets, const google::protobuf::RepeatedPtrField& keys) const; - void ProcessKeys(const google::protobuf::RepeatedPtrField& keys, + void ProcessKeys(const RequestContext& request_context, + const google::protobuf::RepeatedPtrField& keys, InternalLookupResponse& response) const; void ProcessKeysetKeys( + const RequestContext& request_context, const google::protobuf::RepeatedPtrField& keys, InternalLookupResponse& response) const; - grpc::Status ToInternalGrpcStatus(const absl::Status& status, - const char* eventName) const; + grpc::Status ToInternalGrpcStatus(const RequestContext& request_context, + const absl::Status& status, + std::string_view error_code) const; const Lookup& lookup_; privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager_; - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; }; } // namespace kv_server diff --git a/components/internal_server/lookup_server_impl_test.cc b/components/internal_server/lookup_server_impl_test.cc index 47df74b1..be2118fd 100644 --- a/components/internal_server/lookup_server_impl_test.cc +++ b/components/internal_server/lookup_server_impl_test.cc @@ -29,14 +29,12 @@ #include "grpcpp/grpcpp.h" #include "gtest/gtest.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { using google::protobuf::TextFormat; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::Return; using testing::ReturnRef; @@ -45,15 +43,15 @@ class LookupServiceImplTest : public ::testing::Test { protected: LookupServiceImplTest() { lookup_service_ = std::make_unique( - mock_lookup_, fake_key_fetcher_manager_, mock_metrics_recorder_); + mock_lookup_, fake_key_fetcher_manager_); grpc::ServerBuilder builder; builder.RegisterService(lookup_service_.get()); server_ = (builder.BuildAndStart()); stub_ = InternalLookupService::NewStub( server_->InProcessChannel(grpc::ChannelArguments())); + InitMetricsContextMap(); } - ~LookupServiceImplTest() { server_->Shutdown(); server_->Wait(); @@ -64,7 +62,6 @@ class LookupServiceImplTest : public ::testing::Test { std::unique_ptr lookup_service_; std::unique_ptr server_; std::unique_ptr stub_; - MockMetricsRecorder mock_metrics_recorder_; }; TEST_F(LookupServiceImplTest, InternalLookup_Success) { @@ -82,7 +79,7 @@ TEST_F(LookupServiceImplTest, InternalLookup_Success) { } )pb", &expected); - EXPECT_CALL(mock_lookup_, GetKeyValues(_)).WillOnce(Return(expected)); + EXPECT_CALL(mock_lookup_, GetKeyValues(_, _)).WillOnce(Return(expected)); InternalLookupResponse response; grpc::ClientContext context; @@ -96,7 +93,7 @@ TEST_F(LookupServiceImplTest, InternalLookupRequest request; request.add_keys("key1"); request.add_keys("key2"); - EXPECT_CALL(mock_lookup_, GetKeyValues(_)) + EXPECT_CALL(mock_lookup_, GetKeyValues(_, _)) .WillOnce(Return(absl::UnknownError("Some error"))); InternalLookupResponse response; @@ -114,7 +111,7 @@ TEST_F(LookupServiceImplTest, InternalRunQuery_Success) { InternalRunQueryResponse expected; expected.add_elements("value1"); expected.add_elements("value2"); - EXPECT_CALL(mock_lookup_, RunQuery(_)).WillOnce(Return(expected)); + EXPECT_CALL(mock_lookup_, RunQuery(_, _)).WillOnce(Return(expected)); InternalRunQueryResponse response; grpc::ClientContext context; grpc::Status status = stub_->InternalRunQuery(&context, request, &response); @@ -126,7 +123,7 @@ TEST_F(LookupServiceImplTest, InternalRunQuery_Success) { TEST_F(LookupServiceImplTest, InternalRunQuery_LookupError_Failure) { InternalRunQueryRequest request; request.set_query("fail|||||now"); - EXPECT_CALL(mock_lookup_, RunQuery(_)) + EXPECT_CALL(mock_lookup_, RunQuery(_, _)) .WillOnce(Return(absl::UnknownError("Some error"))); InternalRunQueryResponse response; grpc::ClientContext context; diff --git a/components/internal_server/mocks.h b/components/internal_server/mocks.h index 058273d8..1d049b15 100644 --- a/components/internal_server/mocks.h +++ b/components/internal_server/mocks.h @@ -33,7 +33,8 @@ class MockRemoteLookupClient : public RemoteLookupClient { public: MockRemoteLookupClient() : RemoteLookupClient() {} MOCK_METHOD(absl::StatusOr, GetValues, - (std::string_view serialized_message, int32_t padding_length), + (const RequestContext& request_context, + std::string_view serialized_message, int32_t padding_length), (const, override)); MOCK_METHOD(std::string_view, GetIpAddress, (), (const, override)); }; @@ -41,13 +42,15 @@ class MockRemoteLookupClient : public RemoteLookupClient { class MockLookup : public Lookup { public: MOCK_METHOD(absl::StatusOr, GetKeyValues, - (const absl::flat_hash_set&), + (const RequestContext&, + const absl::flat_hash_set&), (const, override)); MOCK_METHOD(absl::StatusOr, GetKeyValueSet, - (const absl::flat_hash_set&), + (const RequestContext&, + const absl::flat_hash_set&), (const, override)); MOCK_METHOD(absl::StatusOr, RunQuery, - (std::string query), (const, override)); + (const RequestContext&, std::string query), (const, override)); }; } // namespace kv_server diff --git a/components/internal_server/remote_lookup_client.h b/components/internal_server/remote_lookup_client.h index d505c612..ece86fa8 100644 --- a/components/internal_server/remote_lookup_client.h +++ b/components/internal_server/remote_lookup_client.h @@ -23,9 +23,8 @@ #include "absl/status/statusor.h" #include "components/internal_server/lookup.grpc.pb.h" -#include "src/cpp/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" +#include "components/util/request_context.h" +#include "src/encryption/key_fetcher/interface/key_fetcher_manager_interface.h" namespace kv_server { @@ -38,18 +37,17 @@ class RemoteLookupClient { // figure out the correct padding length across multiple requests. That helps // with preventing double serialization. virtual absl::StatusOr GetValues( + const RequestContext& request_context, std::string_view serialized_message, int32_t padding_length) const = 0; virtual std::string_view GetIpAddress() const = 0; static std::unique_ptr Create( std::string ip_address, privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder); + key_fetcher_manager); static std::unique_ptr Create( std::unique_ptr stub, privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder); + key_fetcher_manager); }; } // namespace kv_server diff --git a/components/internal_server/remote_lookup_client_impl.cc b/components/internal_server/remote_lookup_client_impl.cc index 95a21fcd..8640f39d 100644 --- a/components/internal_server/remote_lookup_client_impl.cc +++ b/components/internal_server/remote_lookup_client_impl.cc @@ -14,6 +14,7 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" @@ -22,19 +23,11 @@ #include "components/internal_server/lookup.grpc.pb.h" #include "components/internal_server/remote_lookup_client.h" #include "components/internal_server/string_padder.h" -#include "glog/logging.h" #include "grpcpp/grpcpp.h" namespace kv_server { namespace { -using privacy_sandbox::server_common::ScopeLatencyRecorder; - -constexpr char kEncryptionFailure[] = "EncryptionFailure"; -constexpr char kSecureLookupFailure[] = "SecureLookupFailure"; -constexpr char kDecryptionFailure[] = "DecryptionFailure"; -constexpr char kRemoteLookupGetValues[] = "RemoteLookupGetValues"; - class RemoteLookupClientImpl : public RemoteLookupClient { public: RemoteLookupClientImpl(const RemoteLookupClientImpl&) = delete; @@ -43,34 +36,32 @@ class RemoteLookupClientImpl : public RemoteLookupClient { explicit RemoteLookupClientImpl( std::string ip_address, privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) + key_fetcher_manager) : ip_address_( absl::StrFormat("%s:%s", ip_address, kRemoteLookupServerPort)), stub_(InternalLookupService::NewStub(grpc::CreateChannel( ip_address_, grpc::InsecureChannelCredentials()))), - key_fetcher_manager_(key_fetcher_manager), - metrics_recorder_(metrics_recorder) {} + key_fetcher_manager_(key_fetcher_manager) {} explicit RemoteLookupClientImpl( std::unique_ptr stub, privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) - : stub_(std::move(stub)), - key_fetcher_manager_(key_fetcher_manager), - metrics_recorder_(metrics_recorder) {} + key_fetcher_manager) + : stub_(std::move(stub)), key_fetcher_manager_(key_fetcher_manager) {} absl::StatusOr GetValues( + const RequestContext& request_context, std::string_view serialized_message, int32_t padding_length) const override { - ScopeLatencyRecorder latency_recorder(std::string(kRemoteLookupGetValues), - metrics_recorder_); + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetUdfRequestMetricsContext()); OhttpClientEncryptor encryptor(key_fetcher_manager_); auto encrypted_padded_serialized_request_maybe = encryptor.EncryptRequest(Pad(serialized_message, padding_length)); if (!encrypted_padded_serialized_request_maybe.ok()) { - metrics_recorder_.IncrementEventCounter(kEncryptionFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kRemoteRequestEncryptionFailure); return encrypted_padded_serialized_request_maybe.status(); } SecureLookupRequest secure_lookup_request; @@ -81,7 +72,8 @@ class RemoteLookupClientImpl : public RemoteLookupClient { grpc::Status status = stub_->SecureLookup(&context, secure_lookup_request, &secure_response); if (!status.ok()) { - metrics_recorder_.IncrementEventCounter(kSecureLookupFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kRemoteSecureLookupFailure); LOG(ERROR) << status.error_code() << ": " << status.error_message(); return absl::Status((absl::StatusCode)status.error_code(), status.error_message()); @@ -95,12 +87,11 @@ class RemoteLookupClientImpl : public RemoteLookupClient { auto decrypted_response_maybe = encryptor.DecryptResponse(std::move(secure_response.ohttp_response())); if (!decrypted_response_maybe.ok()) { - metrics_recorder_.IncrementEventCounter(kDecryptionFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kResponseEncryptionFailure); return decrypted_response_maybe.status(); } - - if (!response.ParseFromString( - decrypted_response_maybe->GetPlaintextData())) { + if (!response.ParseFromString(*decrypted_response_maybe)) { return absl::InvalidArgumentError("Failed parsing the response."); } return response; @@ -113,7 +104,6 @@ class RemoteLookupClientImpl : public RemoteLookupClient { std::unique_ptr stub_; privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager_; - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; }; } // namespace @@ -121,18 +111,16 @@ class RemoteLookupClientImpl : public RemoteLookupClient { std::unique_ptr RemoteLookupClient::Create( std::string ip_address, privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) { - return std::make_unique( - std::move(ip_address), key_fetcher_manager, metrics_recorder); + key_fetcher_manager) { + return std::make_unique(std::move(ip_address), + key_fetcher_manager); } std::unique_ptr RemoteLookupClient::Create( std::unique_ptr stub, privacy_sandbox::server_common::KeyFetcherManagerInterface& - key_fetcher_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) { - return std::make_unique( - std::move(stub), key_fetcher_manager, metrics_recorder); + key_fetcher_manager) { + return std::make_unique(std::move(stub), + key_fetcher_manager); } } // namespace kv_server diff --git a/components/internal_server/remote_lookup_client_impl_test.cc b/components/internal_server/remote_lookup_client_impl_test.cc index e9093453..56f9b7fe 100644 --- a/components/internal_server/remote_lookup_client_impl_test.cc +++ b/components/internal_server/remote_lookup_client_impl_test.cc @@ -21,14 +21,12 @@ #include "grpcpp/grpcpp.h" #include "gtest/gtest.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { using google::protobuf::TextFormat; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::Return; @@ -36,27 +34,33 @@ class RemoteLookupClientImplTest : public ::testing::Test { protected: RemoteLookupClientImplTest() { lookup_service_ = std::make_unique( - mock_lookup_, fake_key_fetcher_manager_, mock_metrics_recorder_); + mock_lookup_, fake_key_fetcher_manager_); grpc::ServerBuilder builder; builder.RegisterService(lookup_service_.get()); server_ = (builder.BuildAndStart()); remote_lookup_client_ = RemoteLookupClient::Create( InternalLookupService::NewStub( server_->InProcessChannel(grpc::ChannelArguments())), - fake_key_fetcher_manager_, mock_metrics_recorder_); + fake_key_fetcher_manager_); + InitMetricsContextMap(); + scope_metrics_context_ = std::make_unique(); + request_context_ = + std::make_unique(*scope_metrics_context_); } ~RemoteLookupClientImplTest() { server_->Shutdown(); server_->Wait(); } + RequestContext& GetRequestContext() { return *request_context_; } MockLookup mock_lookup_; - MockMetricsRecorder mock_metrics_recorder_; privacy_sandbox::server_common::FakeKeyFetcherManager fake_key_fetcher_manager_; std::unique_ptr lookup_service_; std::unique_ptr server_; std::unique_ptr remote_lookup_client_; + std::unique_ptr scope_metrics_context_; + std::unique_ptr request_context_; }; TEST_F(RemoteLookupClientImplTest, EncryptedPaddedSuccessfulCall) { @@ -77,10 +81,10 @@ TEST_F(RemoteLookupClientImplTest, EncryptedPaddedSuccessfulCall) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_lookup_, GetKeyValues(_)) + EXPECT_CALL(mock_lookup_, GetKeyValues(_, _)) .WillOnce(Return(local_lookup_response)); - auto response_status = - remote_lookup_client_->GetValues(serialized_message, padding_length); + auto response_status = remote_lookup_client_->GetValues( + GetRequestContext(), serialized_message, padding_length); EXPECT_TRUE(response_status.ok()); InternalLookupResponse response = *response_status; InternalLookupResponse expected; @@ -104,8 +108,8 @@ TEST_F(RemoteLookupClientImplTest, EncryptedPaddedEmptySuccessfulCall) { request.set_lookup_sets(false); std::string serialized_message = request.SerializeAsString(); int32_t padding_length = 10; - auto response_status = - remote_lookup_client_->GetValues(serialized_message, padding_length); + auto response_status = remote_lookup_client_->GetValues( + GetRequestContext(), serialized_message, padding_length); EXPECT_TRUE(response_status.ok()); InternalLookupResponse response = *response_status; InternalLookupResponse expected; @@ -129,11 +133,11 @@ TEST_F(RemoteLookupClientImplTest, EncryptedPaddedSuccessfulKeysettLookup) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); - auto response_status = - remote_lookup_client_->GetValues(serialized_message, padding_length); + auto response_status = remote_lookup_client_->GetValues( + GetRequestContext(), serialized_message, padding_length); EXPECT_TRUE(response_status.ok()); InternalLookupResponse response = *response_status; @@ -159,8 +163,8 @@ TEST_F(RemoteLookupClientImplTest, request.set_lookup_sets(true); std::string serialized_message = request.SerializeAsString(); int32_t padding_length = 10; - auto response_status = - remote_lookup_client_->GetValues(serialized_message, padding_length); + auto response_status = remote_lookup_client_->GetValues( + GetRequestContext(), serialized_message, padding_length); EXPECT_TRUE(response_status.ok()); InternalLookupResponse response = *response_status; diff --git a/components/internal_server/sharded_lookup.cc b/components/internal_server/sharded_lookup.cc index c2c7b00d..b102cdca 100644 --- a/components/internal_server/sharded_lookup.cc +++ b/components/internal_server/sharded_lookup.cc @@ -22,41 +22,20 @@ #include #include "absl/log/check.h" +#include "absl/log/log.h" #include "components/internal_server/lookup.h" #include "components/internal_server/lookup.pb.h" #include "components/internal_server/remote_lookup_client.h" #include "components/query/driver.h" #include "components/query/scanner.h" #include "components/sharding/shard_manager.h" -#include "glog/logging.h" +#include "components/util/request_context.h" #include "pir/hashing/sha256_hash_family.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { namespace { using google::protobuf::RepeatedPtrField; -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::ScopeLatencyRecorder; - -constexpr char kShardedLookupGrpcFailure[] = "ShardedLookupGrpcFailure"; -constexpr char kInternalRunQuery[] = "InternalRunQuery"; -constexpr char kInternalRunQueryQueryFailure[] = "InternalRunQueryQueryFailure"; -constexpr char kInternalRunQueryKeysetRetrievalFailure[] = - "InternalRunQueryKeysetRetrievalFailure"; -constexpr char kInternalRunQueryParsingFailure[] = - "InternalRunQueryParsingFailure"; -constexpr char kInternalRunQueryMissingKeyset[] = - "InternalRunQueryMissingKeyset"; -constexpr char kInternalRunQueryEmtpyQuery[] = "InternalRunQueryEmtpyQuery"; -constexpr char kKeySetNotFound[] = "KeysetNotFound"; -constexpr char kShardedLookupServerKeyCollisionOnCollection[] = - "ShardedLookupServerKeyCollisionOnCollection"; -constexpr char kLookupClientMissing[] = "LookupClientMissing"; -constexpr char kShardedLookupServerRequestFailed[] = - "ShardedLookupServerRequestFailed"; -constexpr char kLookupFuturesCreationFailure[] = "LookupFuturesCreationFailure"; -constexpr char kShardedLookupFailure[] = "ShardedLookupFailure"; void UpdateResponse( const std::vector& key_list, @@ -90,16 +69,14 @@ void SetRequestFailed(const std::vector& key_list, class ShardedLookup : public Lookup { public: - explicit ShardedLookup( - const Lookup& local_lookup, const int32_t num_shards, - const int32_t current_shard_num, const ShardManager& shard_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder, - KeySharder key_sharder) + explicit ShardedLookup(const Lookup& local_lookup, const int32_t num_shards, + const int32_t current_shard_num, + const ShardManager& shard_manager, + KeySharder key_sharder) : local_lookup_(local_lookup), num_shards_(num_shards), current_shard_num_(current_shard_num), shard_manager_(shard_manager), - metrics_recorder_(metrics_recorder), key_sharder_(std::move(key_sharder)) { CHECK_GT(num_shards, 1) << "num_shards for ShardedLookup must be > 1"; } @@ -113,21 +90,30 @@ class ShardedLookup : public Lookup { // return an empty response and `Internal` error as the status for the gRPC // status code. absl::StatusOr GetKeyValues( + const RequestContext& request_context, const absl::flat_hash_set& keys) const override { - return ProcessShardedKeys(keys); + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetUdfRequestMetricsContext()); + return ProcessShardedKeys(request_context, keys); } absl::StatusOr GetKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& keys) const override { + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetUdfRequestMetricsContext()); InternalLookupResponse response; if (keys.empty()) { return response; } absl::flat_hash_map> key_sets; - auto get_key_value_set_result_maybe = GetShardedKeyValueSet(keys); + auto get_key_value_set_result_maybe = + GetShardedKeyValueSet(request_context, keys); if (!get_key_value_set_result_maybe.ok()) { - metrics_recorder_.IncrementEventCounter( - kInternalRunQueryKeysetRetrievalFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedGetKeyValueSetKeySetRetrievalFailure); return get_key_value_set_result_maybe.status(); } key_sets = *std::move(get_key_value_set_result_maybe); @@ -138,7 +124,8 @@ class ShardedLookup : public Lookup { if (key_iter == key_sets.end()) { auto status = result.mutable_status(); status->set_code(static_cast(absl::StatusCode::kNotFound)); - metrics_recorder_.IncrementEventCounter(kKeySetNotFound); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedGetKeyValueSetKeySetNotFound); } else { auto keyset_values = result.mutable_keyset_values(); keyset_values->mutable_values()->Add(key_iter->second.begin(), @@ -150,23 +137,25 @@ class ShardedLookup : public Lookup { } absl::StatusOr RunQuery( - std::string query) const override { - ScopeLatencyRecorder latency_recorder(std::string(kInternalRunQuery), - metrics_recorder_); + const RequestContext& request_context, std::string query) const override { + ScopeLatencyMetricsRecorder + latency_recorder(request_context.GetUdfRequestMetricsContext()); InternalRunQueryResponse response; if (query.empty()) { - metrics_recorder_.IncrementEventCounter(kInternalRunQueryEmtpyQuery); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedRunQueryEmptyQuery); return response; } absl::flat_hash_map> keysets; - auto& metrics_recorder = metrics_recorder_; - kv_server::Driver driver([&keysets, - &metrics_recorder](std::string_view key) { + kv_server::Driver driver([&keysets, this, + &request_context](std::string_view key) { const auto key_iter = keysets.find(key); if (key_iter == keysets.end()) { VLOG(8) << "Driver can't find " << key << "key_set. Returning empty."; - metrics_recorder.IncrementEventCounter(kInternalRunQueryMissingKeyset); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedRunQueryMissingKeySet); absl::flat_hash_set set; return set; } else { @@ -180,20 +169,22 @@ class ShardedLookup : public Lookup { kv_server::Parser parse(driver, scanner); int parse_result = parse(); if (parse_result) { - metrics_recorder_.IncrementEventCounter(kInternalRunQueryParsingFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedRunQueryParsingFailure); return absl::InvalidArgumentError("Parsing failure."); } auto get_key_value_set_result_maybe = - GetShardedKeyValueSet(driver.GetRootNode()->Keys()); + GetShardedKeyValueSet(request_context, driver.GetRootNode()->Keys()); if (!get_key_value_set_result_maybe.ok()) { - metrics_recorder_.IncrementEventCounter( - kInternalRunQueryKeysetRetrievalFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedRunQueryKeySetRetrievalFailure); return get_key_value_set_result_maybe.status(); } keysets = std::move(*get_key_value_set_result_maybe); auto result = driver.GetResult(); if (!result.ok()) { - metrics_recorder_.IncrementEventCounter(kInternalRunQueryQueryFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedRunQueryFailure); return result.status(); } VLOG(8) << "Driver results for query " << query; @@ -267,13 +258,18 @@ class ShardedLookup : public Lookup { absl::StatusOr< std::vector>>> - GetLookupFutures(const std::vector& shard_lookup_inputs, + GetLookupFutures(const RequestContext& request_context, + const std::vector& shard_lookup_inputs, std::function( const std::vector& key_list)> get_local_future) const { std::vector>> responses; for (int shard_num = 0; shard_num < num_shards_; shard_num++) { auto& shard_lookup_input = shard_lookup_inputs[shard_num]; + LogIfError(request_context.GetUdfRequestMetricsContext() + .AccumulateMetric( + (int)shard_lookup_input.keys.size(), + std::to_string(shard_num))); if (shard_num == current_shard_num_) { // Eventually this whole branch will go away. responses.push_back(std::async(std::launch::async, get_local_future, @@ -281,13 +277,17 @@ class ShardedLookup : public Lookup { } else { const auto client = shard_manager_.Get(shard_num); if (client == nullptr) { - metrics_recorder_.IncrementEventCounter(kLookupClientMissing); + LogUdfRequestErrorMetric( + request_context.GetUdfRequestMetricsContext(), + kLookupClientMissing); return absl::InternalError("Internal lookup client is unavailable."); } responses.push_back(std::async( std::launch::async, - [client](std::string_view serialized_request, int32_t padding) { - return client->GetValues(serialized_request, padding); + [client, &request_context](std::string_view serialized_request, + int32_t padding) { + return client->GetValues(request_context, serialized_request, + padding); }, shard_lookup_input.serialized_request, shard_lookup_input.padding)); } @@ -296,14 +296,16 @@ class ShardedLookup : public Lookup { } absl::StatusOr GetLocalValues( + const RequestContext& request_context, const std::vector& key_list) const { InternalLookupResponse response; absl::flat_hash_set keys(key_list.begin(), key_list.end()); - return local_lookup_.GetKeyValues(keys); + return local_lookup_.GetKeyValues(request_context, keys); } absl::StatusOr GetLocalKeyValuesSet( + const RequestContext& request_context, const std::vector& key_list) const { if (key_list.empty()) { InternalLookupResponse response; @@ -317,10 +319,11 @@ class ShardedLookup : public Lookup { // a sepration between UDF and Data servers. absl::flat_hash_set key_list_set(key_list.begin(), key_list.end()); - return local_lookup_.GetKeyValueSet(key_list_set); + return local_lookup_.GetKeyValueSet(request_context, key_list_set); } absl::StatusOr ProcessShardedKeys( + const RequestContext& request_context, const absl::flat_hash_set& keys) const { InternalLookupResponse response; if (keys.empty()) { @@ -328,9 +331,10 @@ class ShardedLookup : public Lookup { } const auto shard_lookup_inputs = ShardKeys(keys, false); auto responses = - GetLookupFutures(shard_lookup_inputs, - [this](const std::vector& key_list) { - return GetLocalValues(key_list); + GetLookupFutures(request_context, shard_lookup_inputs, + [this, &request_context]( + const std::vector& key_list) { + return GetLocalValues(request_context, key_list); }); if (!responses.ok()) { return responses.status(); @@ -341,8 +345,8 @@ class ShardedLookup : public Lookup { auto result = (*responses)[shard_num].get(); if (!result.ok()) { // mark all keys as internal failure - metrics_recorder_.IncrementEventCounter( - kShardedLookupServerRequestFailed); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedKeyValueRequestFailure); SetRequestFailed(shard_lookup_input.keys, response); continue; } @@ -353,6 +357,7 @@ class ShardedLookup : public Lookup { } void CollectKeySets( + const RequestContext& request_context, absl::flat_hash_map>& key_sets, InternalLookupResponse& keysets_lookup_response) const { @@ -371,8 +376,9 @@ class ShardedLookup : public Lookup { auto [_, inserted] = key_sets.insert_or_assign(key, std::move(value_set)); if (!inserted) { - metrics_recorder_.IncrementEventCounter( - kShardedLookupServerKeyCollisionOnCollection); + LogUdfRequestErrorMetric( + request_context.GetUdfRequestMetricsContext(), + kShardedKeyCollisionOnKeySetCollection); LOG(ERROR) << "Key collision, when collecting results from shards: " << key; } @@ -384,15 +390,18 @@ class ShardedLookup : public Lookup { absl::StatusOr< absl::flat_hash_map>> GetShardedKeyValueSet( + const RequestContext& request_context, const absl::flat_hash_set& key_set) const { const auto shard_lookup_inputs = ShardKeys(key_set, true); - auto responses = - GetLookupFutures(shard_lookup_inputs, - [this](const std::vector& key_list) { - return GetLocalKeyValuesSet(key_list); - }); + auto responses = GetLookupFutures( + request_context, shard_lookup_inputs, + [this, + &request_context](const std::vector& key_list) { + return GetLocalKeyValuesSet(request_context, key_list); + }); if (!responses.ok()) { - metrics_recorder_.IncrementEventCounter(kLookupFuturesCreationFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kLookupClientMissing); return responses.status(); } // process responses @@ -401,10 +410,11 @@ class ShardedLookup : public Lookup { auto& shard_lookup_input = shard_lookup_inputs[shard_num]; auto result = (*responses)[shard_num].get(); if (!result.ok()) { - metrics_recorder_.IncrementEventCounter(kShardedLookupFailure); + LogUdfRequestErrorMetric(request_context.GetUdfRequestMetricsContext(), + kShardedKeyValueSetRequestFailure); return result.status(); } - CollectKeySets(key_sets, *result); + CollectKeySets(request_context, key_sets, *result); } return key_sets; } @@ -414,20 +424,19 @@ class ShardedLookup : public Lookup { const int32_t current_shard_num_; const std::string hashing_seed_; const ShardManager& shard_manager_; - MetricsRecorder& metrics_recorder_; KeySharder key_sharder_; }; } // namespace -std::unique_ptr CreateShardedLookup( - const Lookup& local_lookup, const int32_t num_shards, - const int32_t current_shard_num, const ShardManager& shard_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder, - KeySharder key_sharder) { - return std::make_unique( - local_lookup, num_shards, current_shard_num, shard_manager, - metrics_recorder, std::move(key_sharder)); +std::unique_ptr CreateShardedLookup(const Lookup& local_lookup, + const int32_t num_shards, + const int32_t current_shard_num, + const ShardManager& shard_manager, + KeySharder key_sharder) { + return std::make_unique(local_lookup, num_shards, + current_shard_num, shard_manager, + std::move(key_sharder)); } } // namespace kv_server diff --git a/components/internal_server/sharded_lookup.h b/components/internal_server/sharded_lookup.h index 207748b4..2619af0a 100644 --- a/components/internal_server/sharded_lookup.h +++ b/components/internal_server/sharded_lookup.h @@ -23,15 +23,14 @@ #include "components/internal_server/lookup.h" #include "components/sharding/shard_manager.h" #include "public/sharding/key_sharder.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { -std::unique_ptr CreateShardedLookup( - const Lookup& local_lookup, const int32_t num_shards, - const int32_t current_shard_num, const ShardManager& shard_manager, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder, - KeySharder key_sharder); +std::unique_ptr CreateShardedLookup(const Lookup& local_lookup, + const int32_t num_shards, + const int32_t current_shard_num, + const ShardManager& shard_manager, + KeySharder key_sharder); } // namespace kv_server diff --git a/components/internal_server/sharded_lookup_test.cc b/components/internal_server/sharded_lookup_test.cc index 7099c1e0..a1ddda53 100644 --- a/components/internal_server/sharded_lookup_test.cc +++ b/components/internal_server/sharded_lookup_test.cc @@ -26,24 +26,30 @@ #include "google/protobuf/text_format.h" #include "gtest/gtest.h" #include "public/test_util/proto_matcher.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { using google::protobuf::TextFormat; -using privacy_sandbox::server_common::MockMetricsRecorder; using testing::_; using testing::Return; using testing::ReturnRef; class ShardedLookupTest : public ::testing::Test { protected: + ShardedLookupTest() { + InitMetricsContextMap(); + scope_metrics_context_ = std::make_unique(); + request_context_ = + std::make_unique(*scope_metrics_context_); + } + RequestContext& GetRequestContext() { return *request_context_; } + std::unique_ptr scope_metrics_context_; + std::unique_ptr request_context_; int32_t num_shards_ = 2; int32_t shard_num_ = 0; MockLookup mock_local_lookup_; - MockMetricsRecorder mock_metrics_recorder_; KeySharder key_sharder_ = KeySharder(ShardingFunction{/*seed=*/""}); }; @@ -55,7 +61,7 @@ TEST_F(ShardedLookupTest, GetKeyValues_Success) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValues(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValues(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -77,7 +83,7 @@ TEST_F(ShardedLookupTest, GetKeyValues_Success) { key_list_remote.end()); const std::string serialized_request = request.SerializeAsString(); EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(serialized_request, 0)) + GetValues(_, serialized_request, 0)) .WillOnce([&]() { InternalLookupResponse resp; SingleLookupResult result; @@ -88,10 +94,11 @@ TEST_F(ShardedLookupTest, GetKeyValues_Success) { return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValues({"key1", "key4"}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = + sharded_lookup->GetKeyValues(GetRequestContext(), {"key1", "key4"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -117,7 +124,7 @@ TEST_F(ShardedLookupTest, GetKeyValues_KeyMissing_ReturnsStatus) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValues(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValues(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -139,8 +146,9 @@ TEST_F(ShardedLookupTest, GetKeyValues_KeyMissing_ReturnsStatus) { key_list_remote.end()); const std::string serialized_request = request.SerializeAsString(); - EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, 0)) - .WillOnce([=](const std::string_view serialized_message, + EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, _, 0)) + .WillOnce([=](const RequestContext& request_context, + const std::string_view serialized_message, const int32_t padding_length) { InternalLookupRequest request; EXPECT_TRUE(request.ParseFromString(serialized_message)); @@ -161,10 +169,11 @@ TEST_F(ShardedLookupTest, GetKeyValues_KeyMissing_ReturnsStatus) { return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValues({"key1", "key4", "key5"}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->GetKeyValues(GetRequestContext(), + {"key1", "key4", "key5"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -196,10 +205,10 @@ TEST_F(ShardedLookupTest, GetKeyValues_EmptyRequest_ReturnsEmptyResponse) { std::make_unique(), [](const std::string& ip) { return std::make_unique(); }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValues({}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->GetKeyValues(GetRequestContext(), {}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -215,7 +224,7 @@ TEST_F(ShardedLookupTest, GetKeyValues_FailedDownstreamRequest) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValues(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValues(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -236,16 +245,17 @@ TEST_F(ShardedLookupTest, GetKeyValues_FailedDownstreamRequest) { key_list_remote.end()); const std::string serialized_request = request.SerializeAsString(); EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(serialized_request, 0)) + GetValues(_, serialized_request, 0)) .WillOnce([]() { return absl::DeadlineExceededError("too long"); }); return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValues({"key1", "key4"}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = + sharded_lookup->GetKeyValues(GetRequestContext(), {"key1", "key4"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -294,8 +304,9 @@ TEST_F(ShardedLookupTest, GetKeyValues_ReturnsKeysFromCachePadding) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValues(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValues(_, _)) .WillOnce([&key_list, &local_lookup_response]( + const RequestContext& request_context, absl::flat_hash_set key_list_input) { EXPECT_THAT(key_list, testing::UnorderedElementsAreArray(key_list_input)); @@ -319,9 +330,9 @@ TEST_F(ShardedLookupTest, GetKeyValues_ReturnsKeysFromCachePadding) { request.mutable_keys()->Assign(key_list_remote.begin(), key_list_remote.end()); const std::string serialized_request = request.SerializeAsString(); - EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(testing::_, testing::_)) + EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, _, _)) .WillOnce([total_length, key_list_remote]( + const RequestContext& request_context, const std::string_view serialized_message, const int32_t padding_length) { EXPECT_EQ(total_length, @@ -356,8 +367,9 @@ TEST_F(ShardedLookupTest, GetKeyValues_ReturnsKeysFromCachePadding) { key_list_remote.end()); const std::string serialized_request = request.SerializeAsString(); EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(serialized_request, testing::_)) - .WillOnce([&](const std::string_view serialized_message, + GetValues(_, serialized_request, _)) + .WillOnce([&](const RequestContext& request_context, + const std::string_view serialized_message, const int32_t padding_length) { InternalLookupResponse resp; return resp; @@ -374,8 +386,9 @@ TEST_F(ShardedLookupTest, GetKeyValues_ReturnsKeysFromCachePadding) { request.mutable_keys()->Assign(key_list_remote.begin(), key_list_remote.end()); const std::string serialized_request = request.SerializeAsString(); - EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, testing::_)) - .WillOnce([=](const std::string_view serialized_message, + EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, _, _)) + .WillOnce([=](const RequestContext& request_context, + const std::string_view serialized_message, const int32_t padding_length) { InternalLookupRequest request; EXPECT_TRUE(request.ParseFromString(serialized_message)); @@ -402,10 +415,10 @@ TEST_F(ShardedLookupTest, GetKeyValues_ReturnsKeysFromCachePadding) { return std::make_unique(); }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValues(keys); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->GetKeyValues(GetRequestContext(), keys); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -455,7 +468,7 @@ TEST_F(ShardedLookupTest, GetKeyValueSets_KeysFound_Success) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -478,7 +491,7 @@ TEST_F(ShardedLookupTest, GetKeyValueSets_KeysFound_Success) { key_list_remote.end()); request.set_lookup_sets(true); const std::string serialized_request = request.SerializeAsString(); - EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, 0)) + EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, _, 0)) .WillOnce([&]() { InternalLookupResponse resp; TextFormat::ParseFromString( @@ -494,10 +507,11 @@ TEST_F(ShardedLookupTest, GetKeyValueSets_KeysFound_Success) { return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValueSet({"key1", "key4"}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = + sharded_lookup->GetKeyValueSet(GetRequestContext(), {"key1", "key4"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -524,7 +538,7 @@ TEST_F(ShardedLookupTest, GetKeyValueSets_KeysMissing_ReturnsStatus) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -547,8 +561,9 @@ TEST_F(ShardedLookupTest, GetKeyValueSets_KeysMissing_ReturnsStatus) { key_list_remote.end()); request.set_lookup_sets(true); const std::string serialized_request = request.SerializeAsString(); - EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, 0)) - .WillOnce([=](const std::string_view serialized_message, + EXPECT_CALL(*mock_remote_lookup_client_1, GetValues(_, _, 0)) + .WillOnce([=](const RequestContext& request_context, + const std::string_view serialized_message, const int32_t padding_length) { InternalLookupRequest request; EXPECT_TRUE(request.ParseFromString(serialized_message)); @@ -569,10 +584,11 @@ TEST_F(ShardedLookupTest, GetKeyValueSets_KeysMissing_ReturnsStatus) { return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValueSet({"key1", "key4", "key5"}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->GetKeyValueSet(GetRequestContext(), + {"key1", "key4", "key5"}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -604,10 +620,10 @@ TEST_F(ShardedLookupTest, GetKeyValueSet_EmptyRequest_ReturnsEmptyResponse) { std::make_unique(), [](const std::string& ip) { return std::make_unique(); }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValueSet({}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->GetKeyValueSet(GetRequestContext(), {}); EXPECT_TRUE(response.ok()); InternalLookupResponse expected; @@ -623,7 +639,7 @@ TEST_F(ShardedLookupTest, GetKeyValueSet_FailedDownstreamRequest) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -645,16 +661,17 @@ TEST_F(ShardedLookupTest, GetKeyValueSet_FailedDownstreamRequest) { request.set_lookup_sets(true); const std::string serialized_request = request.SerializeAsString(); EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(serialized_request, 0)) + GetValues(_, serialized_request, 0)) .WillOnce([]() { return absl::DeadlineExceededError("too long"); }); return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->GetKeyValueSet({"key1", "key4"}); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = + sharded_lookup->GetKeyValueSet(GetRequestContext(), {"key1", "key4"}); EXPECT_FALSE(response.ok()); EXPECT_EQ(response.status().code(), absl::StatusCode::kDeadlineExceeded); } @@ -668,7 +685,7 @@ TEST_F(ShardedLookupTest, RunQuery_Success) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -691,7 +708,7 @@ TEST_F(ShardedLookupTest, RunQuery_Success) { request.set_lookup_sets(true); const std::string serialized_request = request.SerializeAsString(); EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(serialized_request, 0)) + GetValues(_, serialized_request, 0)) .WillOnce([&]() { InternalLookupResponse resp; TextFormat::ParseFromString( @@ -707,10 +724,10 @@ TEST_F(ShardedLookupTest, RunQuery_Success) { return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->RunQuery("key1|key4"); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->RunQuery(GetRequestContext(), "key1|key4"); EXPECT_TRUE(response.ok()); EXPECT_THAT(response.value().elements(), @@ -726,7 +743,7 @@ TEST_F(ShardedLookupTest, RunQuery_MissingKeySet_IgnoresMissingSet_Success) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -749,7 +766,7 @@ TEST_F(ShardedLookupTest, RunQuery_MissingKeySet_IgnoresMissingSet_Success) { request.set_lookup_sets(true); const std::string serialized_request = request.SerializeAsString(); EXPECT_CALL(*mock_remote_lookup_client_1, - GetValues(serialized_request, 0)) + GetValues(_, serialized_request, 0)) .WillOnce([&]() { InternalLookupResponse resp; TextFormat::ParseFromString( @@ -765,10 +782,10 @@ TEST_F(ShardedLookupTest, RunQuery_MissingKeySet_IgnoresMissingSet_Success) { return mock_remote_lookup_client_1; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->RunQuery("key1|key4"); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->RunQuery(GetRequestContext(), "key1|key4"); EXPECT_TRUE(response.ok()); EXPECT_THAT(response.value().elements(), @@ -784,7 +801,7 @@ TEST_F(ShardedLookupTest, RunQuery_ShardedLookupFails_Error) { } )pb", &local_lookup_response); - EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_)) + EXPECT_CALL(mock_local_lookup_, GetKeyValueSet(_, _)) .WillOnce(Return(local_lookup_response)); std::vector> cluster_mappings; @@ -796,10 +813,10 @@ TEST_F(ShardedLookupTest, RunQuery_ShardedLookupFails_Error) { std::make_unique(), [](const std::string& ip) { return nullptr; }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->RunQuery("key1|key4"); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->RunQuery(GetRequestContext(), "key1|key4"); EXPECT_FALSE(response.ok()); EXPECT_THAT(response.status().code(), absl::StatusCode::kInternal); @@ -816,10 +833,10 @@ TEST_F(ShardedLookupTest, RunQuery_ParseError_ReturnStatus) { return std::make_unique(); }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->RunQuery("key1|"); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->RunQuery(GetRequestContext(), "key1|"); EXPECT_FALSE(response.ok()); EXPECT_EQ(response.status().code(), absl::StatusCode::kInvalidArgument); @@ -836,10 +853,10 @@ TEST_F(ShardedLookupTest, RunQuery_EmptyRequest_EmptyResponse) { return std::make_unique(); }); - auto sharded_lookup = CreateShardedLookup( - mock_local_lookup_, num_shards_, shard_num_, *(*shard_manager), - mock_metrics_recorder_, key_sharder_); - auto response = sharded_lookup->RunQuery(""); + auto sharded_lookup = + CreateShardedLookup(mock_local_lookup_, num_shards_, shard_num_, + *(*shard_manager), key_sharder_); + auto response = sharded_lookup->RunQuery(GetRequestContext(), ""); EXPECT_TRUE(response.ok()); EXPECT_TRUE(response.value().elements().empty()); } diff --git a/components/internal_server/string_padder.cc b/components/internal_server/string_padder.cc index 663b2a4d..88ddcf56 100644 --- a/components/internal_server/string_padder.cc +++ b/components/internal_server/string_padder.cc @@ -15,7 +15,7 @@ #include -#include "glog/logging.h" +#include "absl/log/log.h" #include "quiche/common/quiche_data_reader.h" #include "quiche/common/quiche_data_writer.h" diff --git a/components/sharding/BUILD.bazel b/components/sharding/BUILD.bazel index 901c2024..940030e0 100644 --- a/components/sharding/BUILD.bazel +++ b/components/sharding/BUILD.bazel @@ -33,8 +33,6 @@ cc_library( "@com_google_absl//absl/base", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", ], ) @@ -50,8 +48,7 @@ cc_test( ":shard_manager", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", ], ) @@ -99,7 +96,6 @@ cc_test( "//components/data_server/server:mocks", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/encryption/key_fetcher/src:fake_key_fetcher_manager", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/encryption/key_fetcher:fake_key_fetcher_manager", ], ) diff --git a/components/sharding/cluster_mappings_manager.cc b/components/sharding/cluster_mappings_manager.cc index 101732c8..826422fc 100644 --- a/components/sharding/cluster_mappings_manager.cc +++ b/components/sharding/cluster_mappings_manager.cc @@ -26,7 +26,7 @@ ClusterMappingsManager::ClusterMappingsManager( : environment_{std::move(environment)}, num_shards_{num_shards}, instance_client_{instance_client}, - thread_manager_(TheadManager::Create("Cluster mappings updater")), + thread_manager_(ThreadManager::Create("Cluster mappings updater")), sleep_for_(std::move(sleep_for)), update_interval_millis_(update_interval_millis) { CHECK_GT(num_shards, 1) << "num_shards for ShardedLookup must be > 1"; diff --git a/components/sharding/cluster_mappings_manager.h b/components/sharding/cluster_mappings_manager.h index cb98e0ee..0c6b8ebc 100644 --- a/components/sharding/cluster_mappings_manager.h +++ b/components/sharding/cluster_mappings_manager.h @@ -68,7 +68,7 @@ class ClusterMappingsManager { std::string environment_; int32_t num_shards_; InstanceClient& instance_client_; - std::unique_ptr thread_manager_; + std::unique_ptr thread_manager_; std::unique_ptr sleep_for_; int32_t update_interval_millis_; }; diff --git a/components/sharding/cluster_mappings_manager_aws_test.cc b/components/sharding/cluster_mappings_manager_aws_test.cc index 7fe5cd2d..738e609f 100644 --- a/components/sharding/cluster_mappings_manager_aws_test.cc +++ b/components/sharding/cluster_mappings_manager_aws_test.cc @@ -23,8 +23,7 @@ #include "components/sharding/mocks.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { @@ -171,7 +170,6 @@ TEST_F(ClusterMappingsAwsTest, RetrieveMappingsWithRetrySuccessfully) { TEST_F(ClusterMappingsAwsTest, UpdateMappings) { std::string environment = "testenv"; int32_t num_shards = 2; - privacy_sandbox::server_common::MockMetricsRecorder mock_metrics_recorder; privacy_sandbox::server_common::FakeKeyFetcherManager fake_key_fetcher_manager; auto instance_client = std::make_unique(); @@ -179,9 +177,8 @@ TEST_F(ClusterMappingsAwsTest, UpdateMappings) { for (int i = 0; i < num_shards; i++) { cluster_mappings.push_back({"some_ip"}); } - auto shard_manager_status = - ShardManager::Create(num_shards, fake_key_fetcher_manager, - cluster_mappings, mock_metrics_recorder); + auto shard_manager_status = ShardManager::Create( + num_shards, fake_key_fetcher_manager, cluster_mappings); ASSERT_TRUE(shard_manager_status.ok()); auto shard_manager = std::move(*shard_manager_status); absl::Notification finished; @@ -205,7 +202,7 @@ TEST_F(ClusterMappingsAwsTest, UpdateMappings) { std::vector instances{ii1}; return instances; }) - .WillOnce([&](DescribeInstanceGroupInput& input) { + .WillRepeatedly([&](DescribeInstanceGroupInput& input) { auto aws_describe_instance_group_input = std::get_if(&input); absl::flat_hash_set instance_group_names_expected = { @@ -240,7 +237,7 @@ TEST_F(ClusterMappingsAwsTest, UpdateMappings) { std::vector instances{ii1}; return instances; }) - .WillOnce( + .WillRepeatedly( [&](const absl::flat_hash_set& instance_group_names) { absl::flat_hash_set instance_group_names_expected = { "id20"}; diff --git a/components/sharding/cluster_mappings_manager_gcp_test.cc b/components/sharding/cluster_mappings_manager_gcp_test.cc index 5700b5cd..c081af1a 100644 --- a/components/sharding/cluster_mappings_manager_gcp_test.cc +++ b/components/sharding/cluster_mappings_manager_gcp_test.cc @@ -23,8 +23,7 @@ #include "components/sharding/mocks.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { @@ -133,7 +132,6 @@ TEST_F(ClusterMappingsGcpTest, UpdateMappings) { std::string environment = "testenv"; std::string project_id = "some-project-id"; int32_t num_shards = 2; - privacy_sandbox::server_common::MockMetricsRecorder mock_metrics_recorder; privacy_sandbox::server_common::FakeKeyFetcherManager fake_key_fetcher_manager; auto instance_client = std::make_unique(); @@ -141,9 +139,8 @@ TEST_F(ClusterMappingsGcpTest, UpdateMappings) { for (int i = 0; i < num_shards; i++) { cluster_mappings.push_back({"some_ip"}); } - auto shard_manager_status = - ShardManager::Create(num_shards, fake_key_fetcher_manager, - cluster_mappings, mock_metrics_recorder); + auto shard_manager_status = ShardManager::Create( + num_shards, fake_key_fetcher_manager, cluster_mappings); ASSERT_TRUE(shard_manager_status.ok()); auto shard_manager = std::move(*shard_manager_status); absl::Notification finished; @@ -160,7 +157,7 @@ TEST_F(ClusterMappingsGcpTest, UpdateMappings) { std::vector instances{ii1}; return instances; }) - .WillOnce([&](DescribeInstanceGroupInput& input) { + .WillRepeatedly([&](DescribeInstanceGroupInput& input) { auto gcp_describe_instance_group_input = std::get_if(&input); EXPECT_EQ(gcp_describe_instance_group_input->project_id, project_id); diff --git a/components/sharding/shard_manager.cc b/components/sharding/shard_manager.cc index d76470ff..3a7e567f 100644 --- a/components/sharding/shard_manager.cc +++ b/components/sharding/shard_manager.cc @@ -140,17 +140,15 @@ absl::StatusOr> ShardManager::Create( int32_t num_shards, privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager, - const std::vector>& cluster_mappings, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) { + const std::vector>& cluster_mappings) { auto validationStatus = ValidateMapping(num_shards, cluster_mappings); if (!validationStatus.ok()) { return validationStatus; } auto shard_manager = std::make_unique( cluster_mappings.size(), - [&key_fetcher_manager, &metrics_recorder](const std::string& ip) { - return RemoteLookupClient::Create(ip, key_fetcher_manager, - metrics_recorder); + [&key_fetcher_manager](const std::string& ip) { + return RemoteLookupClient::Create(ip, key_fetcher_manager); }, std::make_unique()); shard_manager->InsertBatch(std::move(cluster_mappings)); diff --git a/components/sharding/shard_manager.h b/components/sharding/shard_manager.h index cdf76608..ea4ff8ca 100644 --- a/components/sharding/shard_manager.h +++ b/components/sharding/shard_manager.h @@ -25,8 +25,6 @@ #include "absl/container/flat_hash_set.h" #include "components/internal_server/remote_lookup_client.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry.h" namespace kv_server { // This class is useful for testing ShardManager @@ -56,8 +54,7 @@ class ShardManager { int32_t num_shards, privacy_sandbox::server_common::KeyFetcherManagerInterface& key_fetcher_manager, - const std::vector>& cluster_mappings, - privacy_sandbox::server_common::MetricsRecorder& metrics_recorder); + const std::vector>& cluster_mappings); static absl::StatusOr> Create( int32_t num_shards, const std::vector>& cluster_mappings, diff --git a/components/sharding/shard_manager_test.cc b/components/sharding/shard_manager_test.cc index 971c5133..86cd0c8c 100644 --- a/components/sharding/shard_manager_test.cc +++ b/components/sharding/shard_manager_test.cc @@ -22,26 +22,22 @@ #include "components/sharding/mocks.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/encryption/key_fetcher/src/fake_key_fetcher_manager.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/encryption/key_fetcher/fake_key_fetcher_manager.h" namespace kv_server { namespace { using privacy_sandbox::server_common::FakeKeyFetcherManager; -using privacy_sandbox::server_common::MockMetricsRecorder; class ShardManagerTest : public ::testing::Test { protected: FakeKeyFetcherManager fake_key_fetcher_manager_; - MockMetricsRecorder mock_metrics_recorder_; }; TEST_F(ShardManagerTest, CreationNotInitialized) { std::vector> cluster_mappings; - auto shard_manager = - ShardManager::Create(4, fake_key_fetcher_manager_, - std::move(cluster_mappings), mock_metrics_recorder_); + auto shard_manager = ShardManager::Create(4, fake_key_fetcher_manager_, + std::move(cluster_mappings)); ASSERT_FALSE(shard_manager.ok()); } @@ -51,9 +47,8 @@ TEST_F(ShardManagerTest, CreationInitialized) { for (int i = 0; i < num_shards; i++) { cluster_mappings.push_back({"some_ip"}); } - auto shard_manager = - ShardManager::Create(num_shards, fake_key_fetcher_manager_, - std::move(cluster_mappings), mock_metrics_recorder_); + auto shard_manager = ShardManager::Create( + num_shards, fake_key_fetcher_manager_, std::move(cluster_mappings)); ASSERT_TRUE(shard_manager.ok()); } @@ -63,9 +58,8 @@ TEST_F(ShardManagerTest, CreationNotInitializedMissingClusters) { for (int i = 0; i < 2; i++) { cluster_mappings.push_back({"some_ip"}); } - auto shard_manager = - ShardManager::Create(num_shards, fake_key_fetcher_manager_, - std::move(cluster_mappings), mock_metrics_recorder_); + auto shard_manager = ShardManager::Create( + num_shards, fake_key_fetcher_manager_, std::move(cluster_mappings)); ASSERT_FALSE(shard_manager.ok()); } @@ -76,9 +70,8 @@ TEST_F(ShardManagerTest, CreationNotInitializedMissingReplicas) { cluster_mappings.push_back({"some_ip"}); } cluster_mappings.push_back({}); - auto shard_manager = - ShardManager::Create(num_shards, fake_key_fetcher_manager_, - std::move(cluster_mappings), mock_metrics_recorder_); + auto shard_manager = ShardManager::Create( + num_shards, fake_key_fetcher_manager_, std::move(cluster_mappings)); ASSERT_FALSE(shard_manager.ok()); } @@ -88,9 +81,8 @@ TEST_F(ShardManagerTest, InsertRetrieveSuccess) { for (int i = 0; i < num_shards; i++) { cluster_mappings.push_back({"some_ip"}); } - auto shard_manager = - ShardManager::Create(num_shards, fake_key_fetcher_manager_, - std::move(cluster_mappings), mock_metrics_recorder_); + auto shard_manager = ShardManager::Create( + num_shards, fake_key_fetcher_manager_, std::move(cluster_mappings)); ASSERT_TRUE(shard_manager.ok()); EXPECT_EQ(absl::StrCat("some_ip:", kRemoteLookupServerPort), (*shard_manager)->Get(0)->GetIpAddress()); @@ -102,9 +94,8 @@ TEST_F(ShardManagerTest, InsertMissingReplicasRetrieveSuccess) { for (int i = 0; i < num_shards; i++) { cluster_mappings.push_back({"some_ip"}); } - auto shard_manager = - ShardManager::Create(num_shards, fake_key_fetcher_manager_, - std::move(cluster_mappings), mock_metrics_recorder_); + auto shard_manager = ShardManager::Create( + num_shards, fake_key_fetcher_manager_, std::move(cluster_mappings)); std::vector> cluster_mappings_2; for (int i = 0; i < 3; i++) { cluster_mappings_2.push_back({"some_ip"}); @@ -117,7 +108,6 @@ TEST_F(ShardManagerTest, InsertMissingReplicasRetrieveSuccess) { TEST_F(ShardManagerTest, InsertRetrieveTwoVersions) { auto random_generator = std::make_unique(); - MockMetricsRecorder mock_metrics_recorder_; EXPECT_CALL(*random_generator, Get(testing::_)) .WillOnce([]() { return 0; }) .WillOnce([]() { return 1; }); @@ -129,11 +119,8 @@ TEST_F(ShardManagerTest, InsertRetrieveTwoVersions) { cluster_mappings.push_back({"some_ip_3"}); } auto& fake_key_fetcher_manager = fake_key_fetcher_manager_; - auto& mock_metrics_recorder = mock_metrics_recorder_; - auto client_factory = [&fake_key_fetcher_manager, - &mock_metrics_recorder](const std::string& ip) { - return RemoteLookupClient::Create(ip, fake_key_fetcher_manager, - mock_metrics_recorder); + auto client_factory = [&fake_key_fetcher_manager](const std::string& ip) { + return RemoteLookupClient::Create(ip, fake_key_fetcher_manager); }; auto shard_manager = ShardManager::Create(4, std::move(cluster_mappings), diff --git a/components/telemetry/BUILD.bazel b/components/telemetry/BUILD.bazel index cfece9c8..67289032 100644 --- a/components/telemetry/BUILD.bazel +++ b/components/telemetry/BUILD.bazel @@ -51,7 +51,23 @@ cc_library( ], deps = [ ":error_code", - "@google_privacysandbox_servers_common//src/cpp/metric:context_map", - "@google_privacysandbox_servers_common//src/cpp/util:read_system", + "@google_privacysandbox_servers_common//src/core/common/uuid", + "@google_privacysandbox_servers_common//src/metric:context_map", + "@google_privacysandbox_servers_common//src/util:duration", + "@google_privacysandbox_servers_common//src/util:read_system", + ], +) + +cc_library( + name = "open_telemetry_sink", + srcs = [ + "open_telemetry_sink.cc", + ], + hdrs = [ + "open_telemetry_sink.h", + ], + deps = [ + "@com_google_absl//absl/log", + "@google_privacysandbox_servers_common//src/telemetry", ], ) diff --git a/components/telemetry/error_code.h b/components/telemetry/error_code.h index 55f1070c..4bf282ac 100644 --- a/components/telemetry/error_code.h +++ b/components/telemetry/error_code.h @@ -56,11 +56,92 @@ inline constexpr absl::string_view kRealtimeSleepFailure = inline constexpr absl::string_view kRealtimeDecodeMessageFailure = "RealtimeDecodeMessageFailure"; -// TODO(b/304311724): Revisit the error codes and provide utililities to make -// it easier to log error metrics +// Failure in decrypting request in the internal secure lookup +inline constexpr absl::string_view kRequestDecryptionFailure = + "RequestDecryptionFailure"; +// Error in unpadding request in the internal secure lookup +inline constexpr absl::string_view kRequestUnpaddingError = + "RequestUnpaddingError"; +// Failure in encrypting response in internal secure lookup +inline constexpr absl::string_view kResponseEncryptionFailure = + "ResponseEncryptionFailure"; +// Internal run query request failure +inline constexpr std::string_view kInternalRunQueryRequestFailure = + "InternalRunQueryRequestFailure"; +// Failure in executing run query in local lookup +inline constexpr std::string_view kLocalRunQueryFailure = + "LocalRunQueryFailure"; +// Missing keyset in the run query in local lookup +inline constexpr std::string_view kLocalRunQueryMissingKeySet = + "LocalRunQueryMissingKeySet"; +// Query parsing failure in the run query in local lookup +inline constexpr std::string_view kLocalRunQueryParsingFailure = + "LocalRunQueryParsingFailure"; + +// Lookup client missing in the sharded lookup +inline constexpr std::string_view kLookupClientMissing = "LookupClientMissing"; +// Failure in creating lookup futures +inline constexpr std::string_view kLookupFuturesCreationFailure = + "LookupFuturesCreationFailure"; +inline constexpr std::string_view kRemoteRequestEncryptionFailure = + "RemoteRequestEncryptionFailure"; +inline constexpr std::string_view kRemoteResponseDecryptionFailure = + "RemoteResponseDecryptionFailure"; +inline constexpr std::string_view kRemoteSecureLookupFailure = + "RemoteSecureLookupFailure"; +// Sharded GetKeyValues request failure +inline constexpr std::string_view kShardedKeyValueRequestFailure = + "ShardedKeyValueRequestFailure"; +// Sharded GetKeyValueSet request failure +inline constexpr std::string_view kShardedKeyValueSetRequestFailure = + "ShardedKeyValueSetRequestFailure"; +// Key collisions in collecting results from sharded GetKeyValueSet requests +inline constexpr std::string_view kShardedKeyCollisionOnKeySetCollection = + "ShardedKeyCollisionOnKeySetCollection"; +// Empty query encountered in the sharded lookup +inline constexpr std::string_view kShardedRunQueryEmptyQuery = + "ShardedRunQueryEmptyQuery"; +// Failure in running query in sharded lookup +inline constexpr std::string_view kShardedRunQueryFailure = + "ShardedRunQueryFailure"; +// Key set not found error in the GetValueKeySet in sharded lookup +inline constexpr std::string_view kShardedGetKeyValueSetKeySetNotFound = + "ShardedGetKeyValueSetKeySetNotFound"; +// Key set retrieval failure in the GetValueKeySet in sharded lookup +inline constexpr std::string_view kShardedGetKeyValueSetKeySetRetrievalFailure = + "ShardedGetKeyValueSetKeySetRetrievalFailure"; +// Failure in key set retrieval in the run query in sharded lookup +inline constexpr std::string_view kShardedRunQueryKeySetRetrievalFailure = + "ShardedRunQueryKeySetRetrievalFailure"; +// Missing keyset in the run query in sharded lookup +inline constexpr std::string_view kShardedRunQueryMissingKeySet = + "ShardedRunQueryMissingKeySet"; +// Query parsing failure in the run query in sharded lookup +inline constexpr std::string_view kShardedRunQueryParsingFailure = + "ShardedRunQueryParsingFailure"; // Strings must be sorted, this is required by the API of partitioned metrics -inline constexpr absl::string_view kChangeNotifierErrorCode[] = { +inline constexpr absl::string_view kKVUdfRequestErrorCode[] = { + kLookupClientMissing, + kLookupFuturesCreationFailure, + kRemoteRequestEncryptionFailure, + kRemoteResponseDecryptionFailure, + kRemoteSecureLookupFailure, + kShardedGetKeyValueSetKeySetNotFound, + kShardedGetKeyValueSetKeySetRetrievalFailure, + kShardedKeyCollisionOnKeySetCollection, + kShardedKeyValueRequestFailure, + kShardedKeyValueSetRequestFailure, + kShardedRunQueryEmptyQuery, + kShardedRunQueryFailure, + kShardedRunQueryKeySetRetrievalFailure, + kShardedRunQueryMissingKeySet, + kShardedRunQueryParsingFailure, +}; + +// Non request related server error +// Strings must be sorted, this is required by the API of partitioned metrics +inline constexpr std::string_view kKVServerErrorCode[] = { kAwsChangeNotifierMessagesDataLoss, kAwsChangeNotifierMessagesDeletionFailure, kAwsChangeNotifierMessagesReceivingFailure, @@ -68,16 +149,20 @@ inline constexpr absl::string_view kChangeNotifierErrorCode[] = { kAwsChangeNotifierTagFailure, kAwsJsonParseError, kDeltaFileRecordChangeNotifierParsingFailure, -}; - -// Strings must be sorted, this is required by the API of partitioned metrics -inline constexpr absl::string_view kRealtimeErrorCode[] = { kRealtimeDecodeMessageFailure, kRealtimeGetNotificationsFailure, kRealtimeMessageApplicationFailure, kRealtimeSleepFailure, }; +// Strings must be sorted, this is required by the API of partitioned metrics +inline constexpr absl::string_view kInternalLookupRequestErrorCode[] = { + kInternalRunQueryRequestFailure, kLocalRunQueryFailure, + kLocalRunQueryMissingKeySet, kLocalRunQueryParsingFailure, + kRequestDecryptionFailure, kRequestUnpaddingError, + kResponseEncryptionFailure, +}; + } // namespace kv_server #endif // COMPONENTS_TELEMETRY_ERROR_CODE_H_ diff --git a/components/telemetry/init_aws.cc b/components/telemetry/init_aws.cc index 050406eb..35020066 100644 --- a/components/telemetry/init_aws.cc +++ b/components/telemetry/init_aws.cc @@ -16,8 +16,8 @@ #include "opentelemetry/exporters/otlp/otlp_grpc_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_grpc_metric_exporter_factory.h" #include "opentelemetry/sdk/trace/random_id_generator_factory.h" -#include "src/cpp/telemetry/init.h" -#include "src/cpp/telemetry/trace_generator_aws.h" +#include "src/telemetry/init.h" +#include "src/telemetry/trace_generator_aws.h" namespace kv_server { std::unique_ptr CreateSpanExporter() { diff --git a/components/telemetry/init_local_ostream.cc b/components/telemetry/init_local_ostream.cc index 8c5ec034..4a0b8a4f 100644 --- a/components/telemetry/init_local_ostream.cc +++ b/components/telemetry/init_local_ostream.cc @@ -16,7 +16,7 @@ #include "opentelemetry/exporters/ostream/span_exporter_factory.h" #include "opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader.h" #include "opentelemetry/sdk/trace/random_id_generator_factory.h" -#include "src/cpp/telemetry/init.h" +#include "src/telemetry/init.h" namespace kv_server { diff --git a/components/telemetry/init_local_otlp.cc b/components/telemetry/init_local_otlp.cc index 8a870ff5..d0754012 100644 --- a/components/telemetry/init_local_otlp.cc +++ b/components/telemetry/init_local_otlp.cc @@ -15,14 +15,14 @@ #include "opentelemetry/exporters/otlp/otlp_grpc_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_grpc_metric_exporter_factory.h" #include "opentelemetry/sdk/trace/random_id_generator_factory.h" -#include "src/cpp/telemetry/init.h" +#include "src/telemetry/init.h" // To use Jaeger first run a local instance of the collector // https://www.jaegertracing.io/docs/1.42/getting-started/ // Then build run server with flags for local and otlp. Ex: // `bazel run //components/data_server/server:server --//:instance=local // --//:platform=aws -// --@google_privacysandbox_servers_common//src/cpp/telemetry:local_otel_export=otlp +// --@google_privacysandbox_servers_common//src/telemetry:local_otel_export=otlp // -- // --environment="test"` namespace kv_server { diff --git a/components/telemetry/local_otlp_config/README.md b/components/telemetry/local_otlp_config/README.md index e51ce8d4..6a1cfbd0 100644 --- a/components/telemetry/local_otlp_config/README.md +++ b/components/telemetry/local_otlp_config/README.md @@ -2,8 +2,8 @@ The server can be run locally with 2 differt compile time flags. -- default: `--@google_privacysandbox_servers_common//src/cpp/telemetry:local_otel_export=ostream` -- alternative: `--@google_privacysandbox_servers_common//src/cpp/telemetry:local_otel_export=otlp` +- default: `--@google_privacysandbox_servers_common//src/telemetry:local_otel_export=ostream` +- alternative: `--@google_privacysandbox_servers_common//src/telemetry:local_otel_export=otlp` ## OTLP diff --git a/components/telemetry/open_telemetry_sink.cc b/components/telemetry/open_telemetry_sink.cc new file mode 100644 index 00000000..a7a8b1dc --- /dev/null +++ b/components/telemetry/open_telemetry_sink.cc @@ -0,0 +1,28 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/telemetry/open_telemetry_sink.h" + +#include + +namespace kv_server { +OpenTelemetrySink::OpenTelemetrySink( + opentelemetry::nostd::shared_ptr logger) + : logger_(std::move(logger)) {} +void OpenTelemetrySink::Send(const absl::LogEntry& entry) { + logger_->EmitLogRecord(entry.text_message_with_prefix_and_newline_c_str()); +} +// opentelemetry::logs::Logger doesn't have flush +void OpenTelemetrySink::Flush() {} +} // namespace kv_server diff --git a/components/telemetry/open_telemetry_sink.h b/components/telemetry/open_telemetry_sink.h new file mode 100644 index 00000000..0334f950 --- /dev/null +++ b/components/telemetry/open_telemetry_sink.h @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_TELEMETRY_OPEN_TELEMETRY_SINK_H_ +#define COMPONENTS_TELEMETRY_OPEN_TELEMETRY_SINK_H_ + +#include + +#include "absl/log/log.h" +#include "src/telemetry/telemetry.h" + +namespace kv_server { + +class OpenTelemetrySink : public absl::LogSink { + public: + explicit OpenTelemetrySink( + opentelemetry::nostd::shared_ptr logger); + void Send(const absl::LogEntry& entry) override; + void Flush() override; + opentelemetry::nostd::shared_ptr logger_; +}; + +} // namespace kv_server + +#endif // COMPONENTS_TELEMETRY_OPEN_TELEMETRY_SINK_H_ diff --git a/components/telemetry/server_definition.h b/components/telemetry/server_definition.h index ba8a4e8a..339dd851 100644 --- a/components/telemetry/server_definition.h +++ b/components/telemetry/server_definition.h @@ -21,12 +21,18 @@ #include #include +#include "absl/time/time.h" #include "components/telemetry/error_code.h" -#include "src/cpp/metric/context_map.h" -#include "src/cpp/util/read_system.h" +#include "src/core/common/uuid/uuid.h" +#include "src/metric/context_map.h" +#include "src/util/duration.h" +#include "src/util/read_system.h" namespace kv_server { +constexpr std::string_view kKVServerServiceName = "KVServer"; +constexpr std::string_view kInternalLookupServiceName = "InternalLookupServer"; + // When server is running in debug mode, all unsafe metrics will be logged // safely without DP noise applied. Therefore for now it is okay to set DP // upper and lower bounds for all unsafe metrics to a default value. But @@ -37,6 +43,11 @@ namespace kv_server { constexpr int kCounterDPLowerBound = 1; constexpr int kCounterDPUpperBound = 10; +constexpr int kErrorCounterDPLowerBound = 0; +constexpr int kErrorCounterDPUpperBound = 1; +constexpr int kErrorMaxPartitionsContributed = 1; +constexpr double kErrorMinNoiseToOutput = 0.99; + constexpr int kMicroSecondsLowerBound = 1; constexpr int kMicroSecondsUpperBound = 2'000'000'000; @@ -69,6 +80,15 @@ inline constexpr absl::string_view kAbslStatusStrings[] = { "UNIMPLEMENTED", "UNKNOWN"}; +inline constexpr std::string_view kKeyValueCacheHit = "KeyValueCacheHit"; +inline constexpr std::string_view kKeyValueCacheMiss = "KeyValueCacheMiss"; +inline constexpr std::string_view kKeyValueSetCacheHit = "KeyValueSetCacheHit"; +inline constexpr std::string_view kKeyValueSetCacheMiss = + "KeyValueSetCacheMiss"; +inline constexpr std::string_view kCacheAccessEvents[] = { + kKeyValueCacheHit, kKeyValueCacheMiss, kKeyValueSetCacheHit, + kKeyValueSetCacheMiss}; + inline constexpr privacy_sandbox::server_common::metrics::PrivacyBudget privacy_total_budget{/*epsilon*/ 5}; @@ -76,156 +96,100 @@ inline constexpr privacy_sandbox::server_common::metrics::PrivacyBudget // and should be logged unsafe with DP(differential privacy) noises. inline constexpr privacy_sandbox::server_common::metrics::Definition< int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kInternalRunQueryKeySetRetrievalFailure( - "InternalRunQueryKeySetRetrievalFailure", - "Number of key set internal retrieval failures during internal" - "run query processing", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kKeysNotFoundInKeySetsInShardedLookup( - "KeysNotFoundInKeySetsInShardedLookup", - "Number of keys not found in the result key set in the sharded lookup", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kKeysNotFoundInKeySetsInLocalLookup( - "KeysNotFoundInKeySetsInLocalLookup", - "Number of keys not found in the result key set in the local lookup", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kInternalRunQueryEmtpyQuery("InternalRunQueryEmtpyQuery", - "Number of empty queries encountered during " - "internal run query processing", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kInternalRunQueryMissingKeySet( - "InternalRunQueryMissingKeySet", - "Number of missing keys not found in the key set " - "during internal run query processing", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kInternalRunQueryParsingFailure( - "InternalRunQueryParsingFailure", - "Number of failures in parsing query during " - "internal run query processing", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kLookupClientMissing( - "LookupClientMissing", - "Number of missing internal lookup clients encountered during " - "sharded lookup", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kShardedLookupServerRequestFailed( - "ShardedLookupServerRequestFailed", - "Number of failed server requests in the sharded lookup", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kShardedLookupServerKeyCollisionOnCollection( - "ShardedLookupServerKeyCollisionOnCollection", - "Number of key collisions when collecting results from shards", - kCounterDPUpperBound, kCounterDPLowerBound); + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kKVUdfRequestError("KVUdfRequestError", + "Errors in processing KV server V2 request", + "error_code", kErrorMaxPartitionsContributed, + kKVUdfRequestErrorCode, kErrorCounterDPUpperBound, + kErrorCounterDPLowerBound, kErrorMinNoiseToOutput); +// Metric definitions for request level metrics that are privacy impacting +// and should be logged unsafe with DP(differential privacy) noises. inline constexpr privacy_sandbox::server_common::metrics::Definition< int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kLookupFuturesCreationFailure( - "LookupFuturesCreationFailure", - "Number of failures in creating lookup futures in the sharded lookup", + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kShardedLookupKeyCountByShard( + "ShardedLookupKeyCountByShard", "Keys count by shard number", + "key_shard_num", 1 /*max_partitions_contributed*/, + privacy_sandbox::server_common::metrics::kEmptyPublicPartition, kCounterDPUpperBound, kCounterDPLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kShardedLookupFailure("ShardedLookupFailure", - "Number of lookup failures in the sharded lookup", - kCounterDPUpperBound, kCounterDPLowerBound); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kRemoteClientEncryptionFailure( - "RemoteClientEncryptionFailure", - "Number of request encryption failures in the remote lookup client", - kCounterDPUpperBound, kCounterDPLowerBound); + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kShardedLookupGetKeyValuesLatencyInMicros( + "ShardedLookupGetKeyValuesLatencyInMicros", + "Latency in executing GetKeyValues in the sharded lookup", + kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, + kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kRemoteClientSecureLookupFailure( - "RemoteClientSecureLookupFailure", - "Number of secure lookup failures in the remote lookup client", - kCounterDPUpperBound, kCounterDPLowerBound); + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kShardedLookupGetKeyValueSetLatencyInMicros( + "ShardedLookupGetKeyValueSetLatencyInMicros", + "Latency in executing GetKeyValueSet in the sharded lookup", + kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, + kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kRemoteClientDecryptionFailure( - "RemoteClientDecryptionFailure", - "Number of response decryption failures in the remote lookup client", - kCounterDPUpperBound, kCounterDPLowerBound); + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kShardedLookupRunQueryLatencyInMicros( + "ShardedLookupRunQueryLatencyInMicros", + "Latency in executing RunQuery in the sharded lookup", + kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, + kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kInternalClientDecryptionFailure( - "InternalClientEncryptionFailure", - "Number of request decryption failures in the internal lookup client", - kCounterDPUpperBound, kCounterDPLowerBound); + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kRemoteLookupGetValuesLatencyInMicros( + "RemoteLookupGetValuesLatencyInMicros", + "Latency in get values in the remote lookup", + kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, + kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kInternalClientUnpaddingRequestError( - "InternalClientUnpaddingRequestError", - "Number of unpadding errors in the request deserialization in the " - "internal lookup client", - kCounterDPUpperBound, kCounterDPLowerBound); + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kInternalRunQueryLatencyInMicros("InternalRunQueryLatencyInMicros", + "Latency in internal run query call", + kLatencyInMicroSecondsBoundaries, + kMicroSecondsUpperBound, + kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, privacy_sandbox::server_common::metrics::Instrument::kHistogram> - kShardedLookupRunQueryLatencyInMicros( - "ShardedLookupRunQueryLatencyInMicros", - "Latency in executing run query in the sharded lookup", + kInternalGetKeyValuesLatencyInMicros( + "InternalGetKeyValuesLatencyInMicros", + "Latency in internal get key values call", kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, privacy_sandbox::server_common::metrics::Instrument::kHistogram> - kRemoteLookupGetValuesLatencyInMicros( - "RemoteLookupGetValuesLatencyInMicros", - "Latency in get values in the remote lookup", + kInternalGetKeyValueSetLatencyInMicros( + "InternalGetKeyValueSetLatencyInMicros", + "Latency in internal get key value set call", kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kInternalLookupRequestError("InternalLookupRequestError", + "Errors in processing internal lookup request", + "error_code", kErrorMaxPartitionsContributed, + kInternalLookupRequestErrorCode, + kErrorCounterDPUpperBound, + kErrorCounterDPLowerBound, + kErrorMinNoiseToOutput); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, privacy_sandbox::server_common::metrics::Instrument::kHistogram> kInternalSecureLookupLatencyInMicros("InternalSecureLookupLatencyInMicros", "Latency in internal secure lookup", @@ -234,7 +198,7 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, privacy_sandbox::server_common::metrics::Instrument::kHistogram> kGetValuePairsLatencyInMicros("GetValuePairsLatencyInMicros", "Latency in executing GetValuePairs in cache", @@ -243,7 +207,7 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< kMicroSecondsLowerBound); inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + double, privacy_sandbox::server_common::metrics::Privacy::kImpacting, privacy_sandbox::server_common::metrics::Instrument::kHistogram> kGetKeyValueSetLatencyInMicros( "GetKeyValueSetLatencyInMicros", @@ -251,7 +215,24 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< kLatencyInMicroSecondsBoundaries, kMicroSecondsUpperBound, kMicroSecondsLowerBound); +inline constexpr privacy_sandbox::server_common::metrics::Definition< + int, privacy_sandbox::server_common::metrics::Privacy::kImpacting, + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kCacheAccessEventCount("CacheAccessEventCount", + "Count of cache hit or miss events by request", + "cache_access", 4 /*max_partitions_contributed*/, + kCacheAccessEvents, kCounterDPUpperBound, + kCounterDPLowerBound); + // Metric definitions for safe metrics that are not privacy impacting +inline constexpr privacy_sandbox::server_common::metrics::Definition< + int, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kRequestFailedCountByStatus( + "request.failed_count_by_status", + "Total number of requests that resulted in failure partitioned by " + "Error Code", + "error_code", kAbslStatusStrings); inline constexpr privacy_sandbox::server_common::metrics::Definition< int, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> @@ -306,12 +287,6 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< "Describe instances status", "status", kAbslStatusStrings); -inline constexpr privacy_sandbox::server_common::metrics::Definition< - double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kRealtimeTotalRowsUpdated("RealtimeTotalRowsUpdated", - "Realtime total rows updated count"); - inline constexpr privacy_sandbox::server_common::metrics::Definition< double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, privacy_sandbox::server_common::metrics::Instrument::kHistogram> @@ -349,15 +324,8 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< inline constexpr privacy_sandbox::server_common::metrics::Definition< int, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> - kChangeNotifierErrors("ChangeNotifierErrors", - "Errors in the change notifier", "error_code", - kChangeNotifierErrorCode); - -inline constexpr privacy_sandbox::server_common::metrics::Definition< - int, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, - privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> - kRealtimeErrors("RealtimeErrors", "Errors in realtime data loading", - "error_code", kRealtimeErrorCode); + kKVServerError("KVServerError", "Non request related server errors", + "error_code", kKVServerErrorCode); inline constexpr privacy_sandbox::server_common::metrics::Definition< double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, @@ -384,21 +352,33 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< inline constexpr privacy_sandbox::server_common::metrics::Definition< double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kTotalRowsDroppedInDataLoading("TotalRowsDroppedInDataLoading", - "Total rows dropped during data loading"); + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kTotalRowsDroppedInDataLoading( + "TotalRowsDroppedInDataLoading", + "Total rows dropped during data loading from data source," + "data source can be a data file or realtime", + "data_source", + privacy_sandbox::server_common::metrics::kEmptyPublicPartition); inline constexpr privacy_sandbox::server_common::metrics::Definition< double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kTotalRowsUpdatedInDataLoading("TotalRowsUpdatedInDataLoading", - "Total rows updated during data loading"); + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kTotalRowsUpdatedInDataLoading( + "TotalRowsUpdatedInDataLoading", + "Total rows updated during data loading from data source," + "data source can be a data file or realtime ", + "data_source", + privacy_sandbox::server_common::metrics::kEmptyPublicPartition); inline constexpr privacy_sandbox::server_common::metrics::Definition< double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, - privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> - kTotalRowsDeletedInDataLoading("TotalRowsDeletedInDataLoading", - "Total rows deleted during data loading"); + privacy_sandbox::server_common::metrics::Instrument::kPartitionedCounter> + kTotalRowsDeletedInDataLoading( + "TotalRowsDeletedInDataLoading", + "Total rows deleted during data loading from data source," + "data source can be a data file or realtime", + "data_source", + privacy_sandbox::server_common::metrics::kEmptyPublicPartition); inline constexpr privacy_sandbox::server_common::metrics::Definition< double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, @@ -424,58 +404,148 @@ inline constexpr privacy_sandbox::server_common::metrics::Definition< "Latency in ConcurrentStreamRecordReader reading byte range", kLatencyInMicroSecondsBoundaries); +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kUpdateKeyValueLatency("UpdateKeyValueLatency", + "Latency in key value update", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kUpdateKeyValueSetLatency("UpdateKeyValueSetLatency", + "Latency in key value set update", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kDeleteKeyLatency("DeleteKeyLatency", "Latency in deleting key", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kDeleteValuesInSetLatency("DeleteValuesInSetLatency", + "Latency in deleting values in set", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kRemoveDeletedKeyLatency( + "RemoveDeletedKeyLatency", + "Latency in removing deleted keys in the clean up process", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kCleanUpKeyValueMapLatency("CleanUpKeyValueMapLatency", + "Latency in cleaning up key value map", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + double, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kHistogram> + kCleanUpKeyValueSetMapLatency("CleanUpKeyValueSetMapLatency", + "Latency in cleaning up key value set map", + kLatencyInMicroSecondsBoundaries); + +inline constexpr privacy_sandbox::server_common::metrics::Definition< + int, privacy_sandbox::server_common::metrics::Privacy::kNonImpacting, + privacy_sandbox::server_common::metrics::Instrument::kUpDownCounter> + kSecureLookupRequestCount( + "SecureLookupRequestCount", + "Number of secure lookup requests received from remote server"); + +// KV server metrics list contains contains non request related safe metrics +// and request metrics collected before stage of internal lookups inline constexpr const privacy_sandbox::server_common::metrics::DefinitionName* kKVServerMetricList[] = { // Unsafe metrics - &kInternalRunQueryKeySetRetrievalFailure, - &kKeysNotFoundInKeySetsInShardedLookup, - &kKeysNotFoundInKeySetsInLocalLookup, &kInternalRunQueryEmtpyQuery, - &kInternalRunQueryMissingKeySet, &kInternalRunQueryParsingFailure, - &kLookupClientMissing, &kShardedLookupServerRequestFailed, - &kShardedLookupServerKeyCollisionOnCollection, - &kLookupFuturesCreationFailure, &kShardedLookupFailure, - &kRemoteClientEncryptionFailure, &kRemoteClientSecureLookupFailure, - &kRemoteClientDecryptionFailure, &kInternalClientDecryptionFailure, - &kInternalClientUnpaddingRequestError, + &kKVUdfRequestError, &kShardedLookupKeyCountByShard, + &kShardedLookupGetKeyValuesLatencyInMicros, + &kShardedLookupGetKeyValueSetLatencyInMicros, &kShardedLookupRunQueryLatencyInMicros, &kRemoteLookupGetValuesLatencyInMicros, - &kInternalSecureLookupLatencyInMicros, &kGetValuePairsLatencyInMicros, - &kGetKeyValueSetLatencyInMicros, // Safe metrics + &kKVServerError, + &privacy_sandbox::server_common::metrics::kTotalRequestCount, &privacy_sandbox::server_common::metrics::kServerTotalTimeMs, - &kGetParameterStatus, &kCompleteLifecycleStatus, - &kCreateDataOrchestratorStatus, &kStartDataOrchestratorStatus, - &kLoadNewFilesStatus, &kGetShardManagerStatus, - &kDescribeInstanceGroupInstancesStatus, &kDescribeInstancesStatus, - &kRealtimeTotalRowsUpdated, + &privacy_sandbox::server_common::metrics::kRequestByte, + &privacy_sandbox::server_common::metrics::kResponseByte, + &kRequestFailedCountByStatus, &kGetParameterStatus, + &kCompleteLifecycleStatus, &kCreateDataOrchestratorStatus, + &kStartDataOrchestratorStatus, &kLoadNewFilesStatus, + &kGetShardManagerStatus, &kDescribeInstanceGroupInstancesStatus, + &kDescribeInstancesStatus, &kReceivedLowLatencyNotificationsE2ECloudProvided, &kReceivedLowLatencyNotificationsE2E, &kReceivedLowLatencyNotifications, - &kChangeNotifierErrors, &kRealtimeErrors, &kAwsSqsReceiveMessageLatency, - &kSeekingInputStreambufSeekoffLatency, + &kAwsSqsReceiveMessageLatency, &kSeekingInputStreambufSeekoffLatency, &kSeekingInputStreambufSizeLatency, &kSeekingInputStreambufUnderflowLatency, &kTotalRowsDroppedInDataLoading, &kTotalRowsUpdatedInDataLoading, &kTotalRowsDeletedInDataLoading, &kConcurrentStreamRecordReaderReadShardRecordsLatency, &kConcurrentStreamRecordReaderReadStreamRecordsLatency, - &kConcurrentStreamRecordReaderReadByteRangeLatency}; + &kConcurrentStreamRecordReaderReadByteRangeLatency, + &kUpdateKeyValueLatency, &kUpdateKeyValueSetLatency, &kDeleteKeyLatency, + &kDeleteValuesInSetLatency, &kRemoveDeletedKeyLatency, + &kCleanUpKeyValueMapLatency, &kCleanUpKeyValueSetMapLatency}; + +// Internal lookup service metrics list contains metrics collected in the +// internal lookup server. This separation from KV metrics list allows all +// lookup requests (local and request from remote KV server) to contribute to +// the same set of metrics, so that the noise of unsafe metric won't be skewed +// for particular batch of requests, e.g server request that requires only +// remote lookups +inline constexpr const privacy_sandbox::server_common::metrics::DefinitionName* + kInternalLookupServiceMetricsList[] = { + // Safe metrics + &kSecureLookupRequestCount, + // Unsafe metrics + &kInternalLookupRequestError, &kInternalRunQueryLatencyInMicros, + &kInternalGetKeyValuesLatencyInMicros, + &kInternalGetKeyValueSetLatencyInMicros, + &kInternalSecureLookupLatencyInMicros, &kGetValuePairsLatencyInMicros, + &kGetKeyValueSetLatencyInMicros, &kCacheAccessEventCount}; inline constexpr absl::Span< const privacy_sandbox::server_common::metrics::DefinitionName* const> kKVServerMetricSpan = kKVServerMetricList; +inline constexpr absl::Span< + const privacy_sandbox::server_common::metrics::DefinitionName* const> + kInternalLookupServiceMetricsSpan = kInternalLookupServiceMetricsList; + inline auto* KVServerContextMap( std::optional< privacy_sandbox::server_common::telemetry::BuildDependentConfig> config = std::nullopt, std::unique_ptr provider = nullptr, - absl::string_view service = "", absl::string_view version = "") { + absl::string_view service = kKVServerServiceName, + absl::string_view version = "") { return privacy_sandbox::server_common::metrics::GetContextMap< const std::string, kKVServerMetricSpan>(std::move(config), std::move(provider), service, version, privacy_total_budget); } +inline auto* InternalLookupServerContextMap( + std::optional< + privacy_sandbox::server_common::telemetry::BuildDependentConfig> + config = std::nullopt, + std::unique_ptr provider = nullptr, + absl::string_view service = kInternalLookupServiceName, + absl::string_view version = "") { + return privacy_sandbox::server_common::metrics::GetContextMap< + const std::string, kInternalLookupServiceMetricsSpan>( + std::move(config), std::move(provider), service, version, + privacy_total_budget); +} + template inline void AddSystemMetric(T* context_map) { context_map->AddObserverable( @@ -519,8 +589,154 @@ inline void InitMetricsContextMap() { kv_server::KVServerContextMap( privacy_sandbox::server_common::telemetry::BuildDependentConfig( config_proto)); + kv_server::InternalLookupServerContextMap( + privacy_sandbox::server_common::telemetry::BuildDependentConfig( + config_proto)); +} + +using UdfRequestMetricsContext = + privacy_sandbox::server_common::metrics::ServerContext; +using InternalLookupMetricsContext = + privacy_sandbox::server_common::metrics::ServerContext< + kInternalLookupServiceMetricsSpan>; +using ServerSafeMetricsContext = + privacy_sandbox::server_common::metrics::ServerSafeContext< + kKVServerMetricSpan>; + +inline void LogUdfRequestErrorMetric(UdfRequestMetricsContext& metrics_context, + std::string_view error_code) { + LogIfError( + metrics_context.AccumulateMetric(1, error_code)); +} + +inline void LogInternalLookupRequestErrorMetric( + InternalLookupMetricsContext& metrics_context, + std::string_view error_code) { + LogIfError(metrics_context.AccumulateMetric( + 1, error_code)); } +// Logs non-request related error metrics +inline void LogServerErrorMetric(std::string_view error_code) { + LogIfError( + KVServerContextMap()->SafeMetric().LogUpDownCounter( + {{std::string(error_code), 1}})); +} + +// Logs common safe request metrics +template +inline void LogRequestCommonSafeMetrics( + const RequestT* request, const ResponseT* response, + const grpc::Status& grpc_request_status, + const absl::Time& request_received_time) { + LogIfError( + KVServerContextMap() + ->SafeMetric() + .LogUpDownCounter< + privacy_sandbox::server_common::metrics::kTotalRequestCount>(1)); + if (auto request_status = + privacy_sandbox::server_common::ToAbslStatus(grpc_request_status); + !request_status.ok()) { + LogIfError(KVServerContextMap() + ->SafeMetric() + .LogUpDownCounter( + {{absl::StatusCodeToString(request_status.code()), 1}})); + } + LogIfError(KVServerContextMap() + ->SafeMetric() + .template LogHistogram< + privacy_sandbox::server_common::metrics::kRequestByte>( + (int)request->ByteSizeLong())); + LogIfError(KVServerContextMap() + ->SafeMetric() + .template LogHistogram< + privacy_sandbox::server_common::metrics::kResponseByte>( + (int)response->ByteSizeLong())); + int duration_ms = + (absl::Now() - request_received_time) / absl::Milliseconds(1); + LogIfError( + KVServerContextMap() + ->SafeMetric() + .LogHistogram< + privacy_sandbox::server_common::metrics::kServerTotalTimeMs>( + duration_ms)); +} + +// ScopeMetricsContext provides metrics context ties to the request and +// should have the same lifetime of the request. +// The purpose of this class is to avoid explicit creating and deleting metrics +// context from context map. The metrics context associated with the request +// will be destroyed after ScopeMetricsContext goes out of scope. +class ScopeMetricsContext { + public: + explicit ScopeMetricsContext( + std::string request_id = google::scp::core::common::ToString( + google::scp::core::common::Uuid::GenerateUuid())) + : request_id_(std::move(request_id)) { + // Create a metrics context in the context map and + // associated it with request id + KVServerContextMap()->Get(&request_id_); + CHECK_OK([this]() { + // Remove the metrics context for request_id to transfer the ownership + // of metrics context to the ScopeMetricsContext. This is to ensure that + // metrics context has the same lifetime with RequestContext and be + // destroyed when ScopeMetricsContext goes out of scope. + PS_ASSIGN_OR_RETURN(udf_request_metrics_context_, + KVServerContextMap()->Remove(&request_id_)); + return absl::OkStatus(); + }()) << "Udf request metrics context is not initialized"; + InternalLookupServerContextMap()->Get(&request_id_); + CHECK_OK([this]() { + // Remove the metrics context for request_id to transfer the ownership + // of metrics context to the ScopeMetricsContext. This is to ensure that + // metrics context has the same lifetime with RequestContext and be + // destroyed when ScopeMetricsContext goes out of scope. + PS_ASSIGN_OR_RETURN( + internal_lookup_metrics_context_, + InternalLookupServerContextMap()->Remove(&request_id_)); + return absl::OkStatus(); + }()) << "Internal lookup metrics context is not initialized"; + } + UdfRequestMetricsContext& GetUdfRequestMetricsContext() const { + return *udf_request_metrics_context_; + } + InternalLookupMetricsContext& GetInternalLookupMetricsContext() const { + return *internal_lookup_metrics_context_; + } + + private: + const std::string request_id_; + // Metrics context has the same lifetime of server request context + std::unique_ptr udf_request_metrics_context_; + std::unique_ptr + internal_lookup_metrics_context_; +}; + +// Measures the latency of a block of code. The latency is recorded in +// microseconds as histogram metrics when the object of this class goes +// out of scope. The metric can be either safe or unsafe metric. +template +class ScopeLatencyMetricsRecorder { + public: + explicit ScopeLatencyMetricsRecorder( + ContextT& metrics_context, + std::unique_ptr stopwatch = + std::make_unique()) + : metrics_context_(metrics_context) { + stopwatch_ = std::move(stopwatch); + } + ~ScopeLatencyMetricsRecorder() { + LogIfError(metrics_context_.template LogHistogram( + absl::ToDoubleMicroseconds(stopwatch_->GetElapsedTime()))); + } + // Returns the latency so far + absl::Duration GetLatency() { return stopwatch_->GetElapsedTime(); } + + private: + ContextT& metrics_context_; + std::unique_ptr stopwatch_; +}; + } // namespace kv_server #endif // COMPONENTS_TELEMETRY_SERVER_DEFINITION_H_ diff --git a/components/tools/BUILD.bazel b/components/tools/BUILD.bazel index f18e4c3c..91a7483f 100644 --- a/components/tools/BUILD.bazel +++ b/components/tools/BUILD.bazel @@ -24,7 +24,7 @@ pkg_tar( name = "data_loading_analyzer_binaries", srcs = [ ":data_loading_analyzer", - "@google_privacysandbox_servers_common//scp/cc/aws/proxy/src:proxify_layer", + "@google_privacysandbox_servers_common//src/aws/proxy:libnsm_and_proxify_tar", ], package_dir = "/opt/privacysandbox/bin", ) @@ -74,12 +74,10 @@ cc_binary( "//public/data_loading/readers:riegeli_stream_record_reader_factory", "//public/sharding:key_sharder", "@com_github_google_flatbuffers//:flatbuffers", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", ], ) @@ -111,7 +109,7 @@ cc_library( cc_binary( name = "blob_storage_util", srcs = [ - "blob_storage_util_aws.cc", + "blob_storage_util.cc", ], deps = [ ":blob_storage_commands", @@ -167,10 +165,10 @@ cc_binary( "//components/errors:aws_error_util", "@aws_sdk_cpp//:core", "@aws_sdk_cpp//:ec2", - "@com_github_google_glog//:glog", "@com_google_absl//absl/cleanup", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", ], @@ -189,9 +187,11 @@ cc_binary( deps = [ ":concurrent_publishing_engine", "//components/util:platform_initializer", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/strings", ], ) @@ -202,6 +202,9 @@ cc_library( "//:gcp_platform": [ "publisher_service_gcp.cc", ], + "//:local_platform": [ + "publisher_service_local.cc", + ], "//conditions:default": [ "publisher_service_aws.cc", ], @@ -220,15 +223,17 @@ cc_library( "//components/util:platform_initializer", "@com_github_googleapis_google_cloud_cpp//:pubsub", ], + "//:local_platform": [ + ], "//conditions:default": [ "//components/errors:aws_error_util", "@aws_sdk_cpp//:sns", ], }) + [ "//components/data/common:message_service", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -275,6 +280,8 @@ cc_binary( "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", "@com_google_absl//absl/flags:usage", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/strings", ], ) @@ -288,8 +295,8 @@ cc_library( visibility = ["//tools:__subpackages__"], deps = [ ":publisher_service", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/util:duration", + "@google_privacysandbox_servers_common//src/util:duration", ], ) diff --git a/components/tools/benchmarks/BUILD.bazel b/components/tools/benchmarks/BUILD.bazel index a0c5b33a..ccc611e4 100644 --- a/components/tools/benchmarks/BUILD.bazel +++ b/components/tools/benchmarks/BUILD.bazel @@ -25,7 +25,7 @@ cc_library( deps = [ "//public/data_loading:records_utils", "//public/data_loading/writers:delta_record_stream_writer", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -47,6 +47,7 @@ cc_test( cc_binary( name = "data_loading_benchmark", srcs = ["data_loading_benchmark.cc"], + malloc = "@com_google_tcmalloc//tcmalloc", deps = [ ":benchmark_util", "//components/data/blob_storage:blob_storage_client", @@ -57,36 +58,38 @@ cc_binary( "//public/data_loading:data_loading_fbs", "//public/data_loading:records_utils", "//public/data_loading/readers:riegeli_stream_io", - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", "@com_google_benchmark//:benchmark", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", ], ) cc_binary( name = "cache_benchmark", srcs = ["cache_benchmark.cc"], + malloc = "@com_google_tcmalloc//tcmalloc", deps = [ ":benchmark_util", "//components/data_server/cache", "//components/data_server/cache:key_value_cache", "//components/data_server/cache:noop_key_value_cache", - "@com_github_google_glog//:glog", + "//components/util:request_context", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", "@com_google_benchmark//:benchmark", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", ], ) diff --git a/components/tools/benchmarks/cache_benchmark.cc b/components/tools/benchmarks/cache_benchmark.cc index 662ddc73..3aa3133f 100644 --- a/components/tools/benchmarks/cache_benchmark.cc +++ b/components/tools/benchmarks/cache_benchmark.cc @@ -23,6 +23,9 @@ #include "absl/container/flat_hash_map.h" #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" @@ -32,9 +35,6 @@ #include "components/data_server/cache/key_value_cache.h" #include "components/data_server/cache/noop_key_value_cache.h" #include "components/tools/benchmarks/benchmark_util.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry_provider.h" ABSL_FLAG(std::vector, record_size, std::vector({"1"}), @@ -69,8 +69,6 @@ namespace { using kv_server::benchmark::AsyncTask; using kv_server::benchmark::GenerateRandomString; using kv_server::benchmark::ParseInt64List; -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::TelemetryProvider; // Format variables used to generate benchmark names. // @@ -104,8 +102,8 @@ Cache* GetNoOpCache() { return cache; } -Cache* GetLockBasedCache(MetricsRecorder& metrics_recorder) { - static auto* const cache = KeyValueCache::Create(metrics_recorder).release(); +Cache* GetLockBasedCache() { + static auto* const cache = KeyValueCache::Create().release(); return cache; } @@ -173,8 +171,11 @@ void BM_GetKeyValuePairs(::benchmark::State& state, BenchmarkArgs args) { } auto keys = GetKeys(args.query_size); auto keys_view = ToContainerView>(keys); + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); for (auto _ : state) { - ::benchmark::DoNotOptimize(args.cache->GetKeyValuePairs(keys_view)); + ::benchmark::DoNotOptimize( + args.cache->GetKeyValuePairs(request_context, keys_view)); } state.counters[std::string(kReadsPerSec)] = ::benchmark::Counter(state.iterations(), ::benchmark::Counter::kIsRate); @@ -199,8 +200,11 @@ void BM_GetKeyValueSet(::benchmark::State& state, BenchmarkArgs args) { } auto keys = GetKeys(args.query_size); auto keys_view = ToContainerView>(keys); + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); for (auto _ : state) { - ::benchmark::DoNotOptimize(args.cache->GetKeyValueSet(keys_view)); + ::benchmark::DoNotOptimize( + args.cache->GetKeyValueSet(request_context, keys_view)); } state.counters[std::string(kReadsPerSec)] = ::benchmark::Counter(state.iterations(), ::benchmark::Counter::kIsRate); @@ -209,14 +213,16 @@ void BM_GetKeyValueSet(::benchmark::State& state, BenchmarkArgs args) { void BM_UpdateKeyValue(::benchmark::State& state, BenchmarkArgs args) { uint seed = args.concurrent_tasks; std::vector reader_tasks; + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); if (state.thread_index() == 0 && args.concurrent_tasks) { auto num_readers = args.concurrent_tasks; reader_tasks.reserve(num_readers); while (num_readers-- > 0) { - reader_tasks.emplace_back([args, &seed]() { + reader_tasks.emplace_back([args, &seed, &request_context]() { auto key = std::to_string(rand_r(&seed) % args.keyspace_size); args.cache->GetKeyValuePairs( - absl::flat_hash_set({key})); + request_context, absl::flat_hash_set({key})); }); } } @@ -232,13 +238,15 @@ void BM_UpdateKeyValue(::benchmark::State& state, BenchmarkArgs args) { void BM_UpdateKeyValueSet(::benchmark::State& state, BenchmarkArgs args) { uint seed = args.concurrent_tasks; std::vector reader_tasks; + auto scope_metrics_context = std::make_unique(); + RequestContext request_context(*scope_metrics_context); if (state.thread_index() == 0 && args.concurrent_tasks) { auto num_readers = args.concurrent_tasks; reader_tasks.reserve(num_readers); while (num_readers-- > 0) { - reader_tasks.emplace_back([args, &seed]() { + reader_tasks.emplace_back([args, &seed, &request_context]() { auto key = std::to_string(rand_r(&seed) % args.keyspace_size); - args.cache->GetKeyValueSet({key}); + args.cache->GetKeyValueSet(request_context, {key}); }); } } @@ -267,7 +275,7 @@ void RegisterBenchmark( } } -void RegisterReadBenchmarks(MetricsRecorder& metrics_recorder) { +void RegisterReadBenchmarks() { auto query_sizes = ParseInt64List(absl::GetFlag(FLAGS_query_size)); auto set_query_sizes = ParseInt64List(absl::GetFlag(FLAGS_set_query_size)); auto record_sizes = ParseInt64List(absl::GetFlag(FLAGS_record_size)); @@ -286,7 +294,7 @@ void RegisterReadBenchmarks(MetricsRecorder& metrics_recorder) { absl::StrFormat(kNoOpCacheGetKeyValuePairsFmt, query_size, record_size, num_writers), args, BM_GetKeyValuePairs); - args.cache = GetLockBasedCache(metrics_recorder); + args.cache = GetLockBasedCache(); ::kv_server::RegisterBenchmark( absl::StrFormat(kLockBasedCacheGetKeyValuePairsFmt, query_size, record_size, num_writers), @@ -298,7 +306,7 @@ void RegisterReadBenchmarks(MetricsRecorder& metrics_recorder) { absl::StrFormat(kNoOpCacheGetKeyValueSetFmt, query_size, set_query_size, record_size, num_writers), args, BM_GetKeyValueSet); - args.cache = GetLockBasedCache(metrics_recorder); + args.cache = GetLockBasedCache(); ::kv_server::RegisterBenchmark( absl::StrFormat(kLockBasedCacheGetKeyValueSetFmt, query_size, set_query_size, record_size, num_writers), @@ -309,7 +317,7 @@ void RegisterReadBenchmarks(MetricsRecorder& metrics_recorder) { } } -void RegisterWriteBenchmarks(MetricsRecorder& metrics_recorder) { +void RegisterWriteBenchmarks() { auto keyspace_sizes = ParseInt64List(absl::GetFlag(FLAGS_keyspace_size)); auto record_sizes = ParseInt64List(absl::GetFlag(FLAGS_record_size)); auto set_query_sizes = ParseInt64List(absl::GetFlag(FLAGS_set_query_size)); @@ -328,7 +336,7 @@ void RegisterWriteBenchmarks(MetricsRecorder& metrics_recorder) { absl::StrFormat(kNoOpCacheUpdateKeyValueFmt, keyspace_size, record_size, num_readers), args, BM_UpdateKeyValue); - args.cache = GetLockBasedCache(metrics_recorder); + args.cache = GetLockBasedCache(); ::kv_server::RegisterBenchmark( absl::StrFormat(kLockBasedCacheUpdateKeyValueFmt, keyspace_size, record_size, num_readers), @@ -340,7 +348,7 @@ void RegisterWriteBenchmarks(MetricsRecorder& metrics_recorder) { absl::StrFormat(kNoOpCacheUpdateKeyValueSetFmt, keyspace_size, set_query_size, record_size, num_readers), args, BM_UpdateKeyValueSet); - args.cache = GetLockBasedCache(metrics_recorder); + args.cache = GetLockBasedCache(); ::kv_server::RegisterBenchmark( absl::StrFormat(kLockBasedCacheUpdateKeyValueSetFmt, keyspace_size, set_query_size, record_size, @@ -357,19 +365,18 @@ void RegisterWriteBenchmarks(MetricsRecorder& metrics_recorder) { // Microbenchmarks for Cache impelementations. Sample run: // -// GLOG_logtostderr=1 bazel run -c opt \ +// bazel run -c opt \ // //components/tools/benchmarks:cache_benchmark \ // --//:instance=local \ // --//:platform=local -- \ -// --benchmark_counters_tabular=true +// --benchmark_counters_tabular=true --stderrthreshold=0 int main(int argc, char** argv) { - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); ::benchmark::Initialize(&argc, argv); absl::ParseCommandLine(argc, argv); - auto noop_metrics_recorder = - ::kv_server::TelemetryProvider::GetInstance().CreateMetricsRecorder(); - ::kv_server::RegisterReadBenchmarks(*noop_metrics_recorder); - ::kv_server::RegisterWriteBenchmarks(*noop_metrics_recorder); + kv_server::InitMetricsContextMap(); + ::kv_server::RegisterReadBenchmarks(); + ::kv_server::RegisterWriteBenchmarks(); ::benchmark::RunSpecifiedBenchmarks(); ::benchmark::Shutdown(); return 0; diff --git a/components/tools/benchmarks/data_loading_benchmark.cc b/components/tools/benchmarks/data_loading_benchmark.cc index 1f71ebca..cf95b0ea 100644 --- a/components/tools/benchmarks/data_loading_benchmark.cc +++ b/components/tools/benchmarks/data_loading_benchmark.cc @@ -23,6 +23,9 @@ #include "absl/container/flat_hash_map.h" #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" #include "absl/time/time.h" @@ -33,11 +36,9 @@ #include "components/data_server/cache/noop_key_value_cache.h" #include "components/tools/benchmarks/benchmark_util.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/readers/riegeli_stream_io.h" #include "public/data_loading/records_utils.h" -#include "src/cpp/telemetry/telemetry_provider.h" ABSL_FLAG(std::string, data_directory, "", "Data directory or bucket to store benchmark input data files in."); @@ -83,8 +84,6 @@ using kv_server::RecordStream; using kv_server::Value; using kv_server::benchmark::ParseInt64List; using kv_server::benchmark::WriteRecords; -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::TelemetryProvider; constexpr std::string_view kNoOpCacheNameFormat = "BM_DataLoading_NoOpCache/tds:%d/conns:%d/buf:%d"; @@ -169,11 +168,7 @@ void RegisterBenchmarks() { RegisterBenchmark(absl::StrFormat(kNoOpCacheNameFormat, num_threads, num_connections, byte_range_mb), args); - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - args.create_cache_fn = [&noop_metrics_recorder]() { - return KeyValueCache::Create(*noop_metrics_recorder); - }; + args.create_cache_fn = []() { return KeyValueCache::Create(); }; RegisterBenchmark(absl::StrFormat(kMutexCacheNameFormat, num_threads, num_connections, byte_range_mb), args); @@ -283,7 +278,7 @@ void BM_LoadDataIntoCache(benchmark::State& state, BenchmarkArgs args) { // Sample usage: // -// GLOG_logtostderr=1 bazel run \ +// bazel run \ // components/tools/benchmarks:data_loading_benchmark \ // --//:instance=local --//:platform=local -- \ // --benchmark_time_unit=ms \ @@ -295,10 +290,10 @@ void BM_LoadDataIntoCache(benchmark::State& state, BenchmarkArgs args) { // --record_size=1000 \ // --args_client_max_range_mb=8 \ // --args_client_max_connections=64 \ -// --args_reader_worker_threads=16,32,64 +// --args_reader_worker_threads=16,32,64 --stderrthreshold=0 int main(int argc, char** argv) { ::kv_server::PlatformInitializer platform_initializer; - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); ::benchmark::Initialize(&argc, argv); absl::ParseCommandLine(argc, argv); if (absl::GetFlag(FLAGS_data_directory).empty()) { diff --git a/components/tools/blob_storage_change_watcher_aws.cc b/components/tools/blob_storage_change_watcher_aws.cc index 559f88a4..48477cce 100644 --- a/components/tools/blob_storage_change_watcher_aws.cc +++ b/components/tools/blob_storage_change_watcher_aws.cc @@ -20,7 +20,7 @@ #include "components/data/blob_storage/blob_storage_change_notifier.h" #include "components/telemetry/server_definition.h" #include "components/util/platform_initializer.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" ABSL_FLAG(std::string, sns_arn, "", "sns_arn"); diff --git a/components/tools/blob_storage_commands.cc b/components/tools/blob_storage_commands.cc index b660a507..c555eb0c 100644 --- a/components/tools/blob_storage_commands.cc +++ b/components/tools/blob_storage_commands.cc @@ -24,7 +24,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "components/data/blob_storage/blob_storage_client.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { namespace blob_storage_commands { @@ -55,12 +55,16 @@ class StdinBlobReader : public BlobReader { using kv_server::BlobStorageClient; -bool CatObjects(std::string bucket_or_directory, absl::Span keys) { +bool CatObjects(std::string bucket_or_directory, std::string prefix, + absl::Span keys) { std::unique_ptr blob_storage_client_factory = BlobStorageClientFactory::Create(); std::unique_ptr client = blob_storage_client_factory->CreateBlobStorageClient(); - BlobStorageClient::DataLocation location = {std::move(bucket_or_directory)}; + BlobStorageClient::DataLocation location = { + .bucket = std::move(bucket_or_directory), + .prefix = std::move(prefix), + }; for (const auto& key : keys) { location.key = key; auto reader = client->GetBlobReader(location); @@ -69,12 +73,16 @@ bool CatObjects(std::string bucket_or_directory, absl::Span keys) { return true; } -bool DeleteObjects(std::string bucket_or_directory, absl::Span keys) { +bool DeleteObjects(std::string bucket_or_directory, std::string prefix, + absl::Span keys) { std::unique_ptr blob_storage_client_factory = BlobStorageClientFactory::Create(); std::unique_ptr client = blob_storage_client_factory->CreateBlobStorageClient(); - BlobStorageClient::DataLocation location = {std::move(bucket_or_directory)}; + BlobStorageClient::DataLocation location = { + .bucket = std::move(bucket_or_directory), + .prefix = std::move(prefix), + }; for (const auto& key : keys) { location.key = key; const absl::Status status = client->DeleteBlob(location); @@ -86,13 +94,15 @@ bool DeleteObjects(std::string bucket_or_directory, absl::Span keys) { return true; } -bool ListObjects(std::string bucket_or_directory) { +bool ListObjects(std::string bucket_or_directory, std::string prefix) { std::unique_ptr blob_storage_client_factory = BlobStorageClientFactory::Create(); std::unique_ptr client = blob_storage_client_factory->CreateBlobStorageClient(); const BlobStorageClient::DataLocation location = { - std::move(bucket_or_directory)}; + .bucket = std::move(bucket_or_directory), + .prefix = std::move(prefix), + }; const absl::StatusOr> keys = client->ListBlobs(location, {}); if (!keys.ok()) { diff --git a/components/tools/blob_storage_commands.h b/components/tools/blob_storage_commands.h index d3339cff..e61e48dc 100644 --- a/components/tools/blob_storage_commands.h +++ b/components/tools/blob_storage_commands.h @@ -28,13 +28,15 @@ namespace kv_server { namespace blob_storage_commands { // Print the contents of blobs to stdout; returns success. -bool CatObjects(std::string bucket_or_directory, absl::Span keys); +bool CatObjects(std::string bucket_or_directory, std::string prefix, + absl::Span keys); // Delete the blobs; returns success. -bool DeleteObjects(std::string bucket_or_directory, absl::Span keys); +bool DeleteObjects(std::string bucket_or_directory, std::string prefix, + absl::Span keys); // Print to stdout a list of blobs that are in this location; returns success. -bool ListObjects(std::string bucket_or_directory); +bool ListObjects(std::string bucket_or_directory, std::string prefix); // Create a new BlobReader that will read from either a file or stdin if the // source is "-". diff --git a/components/tools/blob_storage_util_aws.cc b/components/tools/blob_storage_util.cc similarity index 91% rename from components/tools/blob_storage_util_aws.cc rename to components/tools/blob_storage_util.cc index 1db257e1..4ee70581 100644 --- a/components/tools/blob_storage_util_aws.cc +++ b/components/tools/blob_storage_util.cc @@ -21,9 +21,10 @@ #include "components/data/blob_storage/blob_storage_client.h" #include "components/tools/blob_storage_commands.h" #include "components/util/platform_initializer.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" ABSL_FLAG(std::string, bucket, "", "cloud storage bucket name"); +ABSL_FLAG(std::string, prefix, "", "object prefix name"); using kv_server::BlobReader; using kv_server::BlobStorageClient; @@ -102,13 +103,15 @@ int main(int argc, char** argv) { std::cerr << "Must specify bucket" << std::endl; return -1; } + std::string prefix = absl::GetFlag(FLAGS_prefix); absl::string_view operation = commands[1]; if (operation == "ls") { if (commands.size() != 2) { std::cerr << "ls does not take any extra arguments." << std::endl; return -1; } - return kv_server::blob_storage_commands::ListObjects(std::move(bucket)) + return kv_server::blob_storage_commands::ListObjects(std::move(bucket), + std::move(prefix)) ? 0 : -1; } @@ -118,7 +121,8 @@ int main(int argc, char** argv) { return -1; } return kv_server::blob_storage_commands::DeleteObjects( - std::move(bucket), absl::MakeSpan(commands).subspan(2)) + std::move(bucket), std::move(prefix), + absl::MakeSpan(commands).subspan(2)) ? 0 : -1; } @@ -128,7 +132,8 @@ int main(int argc, char** argv) { return -1; } return kv_server::blob_storage_commands::CatObjects( - std::move(bucket), absl::MakeSpan(commands).subspan(2)) + std::move(bucket), std::move(prefix), + absl::MakeSpan(commands).subspan(2)) ? 0 : -1; } diff --git a/components/tools/concurrent_publishing_engine.cc b/components/tools/concurrent_publishing_engine.cc index 07591b29..3994e8f1 100644 --- a/components/tools/concurrent_publishing_engine.cc +++ b/components/tools/concurrent_publishing_engine.cc @@ -37,14 +37,24 @@ void ConcurrentPublishingEngine::Start() { } void ConcurrentPublishingEngine::Stop() { + { + absl::MutexLock l(&mutex_); + stop_ = true; + } for (auto& publisher_thread : publishers_) { publisher_thread->join(); } } -std::optional ConcurrentPublishingEngine::Pop() { - absl::MutexLock lock(&mutex_); - if (updates_queue_.empty()) { +bool ConcurrentPublishingEngine::HasNewMessageToProcess() const { + return !updates_queue_.empty() || stop_; +} + +std::optional ConcurrentPublishingEngine::Pop( + absl::Condition& has_new_event) { + absl::MutexLock lock(&mutex_, has_new_event); + if (stop_) { + LOG(INFO) << "Thread for new file processing stopped"; return std::nullopt; } auto file_encoded = updates_queue_.front(); @@ -52,20 +62,31 @@ std::optional ConcurrentPublishingEngine::Pop() { return file_encoded; } +bool ConcurrentPublishingEngine::ShouldStop() { + absl::MutexLock l(&mutex_); + return stop_; +} + void ConcurrentPublishingEngine::ConsumeAndPublish(int thread_idx) { const auto start = clock_.Now(); int delta_file_index = 1; auto batch_end = clock_.Now() + absl::Seconds(1); - auto message = Pop(); auto maybe_msg_service = PublisherService::Create(notifier_metadata_); if (!maybe_msg_service.ok()) { LOG(ERROR) << "Failed creating a publisher service"; return; } auto msg_service = std::move(*maybe_msg_service); - while (message.has_value()) { - LOG(INFO) << ": Inserting to the SNS: " << delta_file_index - << " Thread idx " << thread_idx; + absl::Condition has_new_event( + this, &ConcurrentPublishingEngine::HasNewMessageToProcess); + + while (!ShouldStop()) { + auto message = Pop(has_new_event); + if (!message.has_value()) { + return; + } + VLOG(9) << ": Inserting to the SNS: " << delta_file_index << " Thread idx " + << thread_idx; auto status = msg_service->Publish(message->message, message->shard_num); if (!status.ok()) { LOG(ERROR) << status; @@ -79,7 +100,6 @@ void ConcurrentPublishingEngine::ConsumeAndPublish(int thread_idx) { } batch_end += absl::Seconds(1); } - message = Pop(); } int64_t elapsed_seconds = absl::ToInt64Seconds(clock_.Now() - start); diff --git a/components/tools/concurrent_publishing_engine.h b/components/tools/concurrent_publishing_engine.h index 317971ba..0785f7c5 100644 --- a/components/tools/concurrent_publishing_engine.h +++ b/components/tools/concurrent_publishing_engine.h @@ -21,9 +21,9 @@ #include #include +#include "absl/log/log.h" #include "components/tools/publisher_service.h" -#include "glog/logging.h" -#include "src/cpp/util/duration.h" +#include "src/util/duration.h" namespace kv_server { @@ -56,13 +56,16 @@ class ConcurrentPublishingEngine { delete; private: - std::optional Pop(); + std::optional Pop(absl::Condition& has_new_event); + bool ShouldStop(); void ConsumeAndPublish(int thread_idx); const int insertion_num_threads_; const NotifierMetadata notifier_metadata_; const int files_insertion_rate_; absl::Mutex& mutex_; + bool stop_ ABSL_GUARDED_BY(mutex_) = false; + bool HasNewMessageToProcess() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); std::queue& updates_queue_ ABSL_GUARDED_BY(mutex_); std::vector> publishers_; privacy_sandbox::server_common::SteadyClock& clock_ = diff --git a/components/tools/data_loading_analyzer.cc b/components/tools/data_loading_analyzer.cc index 334f8942..53ff75a0 100644 --- a/components/tools/data_loading_analyzer.cc +++ b/components/tools/data_loading_analyzer.cc @@ -19,6 +19,7 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" #include "components/data/blob_storage/blob_storage_client.h" #include "components/data/blob_storage/delta_file_notifier.h" @@ -27,13 +28,11 @@ #include "components/data_server/data_loading/data_orchestrator.h" #include "components/udf/noop_udf_client.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" #include "public/base_types.pb.h" #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/readers/riegeli_stream_io.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" #include "public/sharding/key_sharder.h" -#include "src/cpp/telemetry/telemetry_provider.h" ABSL_FLAG(std::vector, operations, std::vector({"PASS_THROUGH", "READ_ONLY", "CACHE"}), @@ -44,10 +43,6 @@ ABSL_FLAG(std::string, bucket, "performance-test-data-bucket", namespace kv_server { namespace { -using privacy_sandbox::server_common::GetTracer; -using privacy_sandbox::server_common::MetricsRecorder; -using privacy_sandbox::server_common::TelemetryProvider; - class NoopBlobStorageChangeNotifier : public BlobStorageChangeNotifier { public: absl::StatusOr> GetNotifications( @@ -146,9 +141,7 @@ std::vector OperationsFromFlag() { absl::Status InitOnce(Operation operation) { std::unique_ptr noop_udf_client = NewNoopUdfClient(); InitMetricsContextMap(); - auto noop_metrics_recorder = - TelemetryProvider::GetInstance().CreateMetricsRecorder(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); + std::unique_ptr cache = KeyValueCache::Create(); std::unique_ptr blob_storage_client_factory = BlobStorageClientFactory::Create(); diff --git a/components/tools/delta_file_record_change_watcher.cc b/components/tools/delta_file_record_change_watcher.cc index 0e209af9..de01a605 100644 --- a/components/tools/delta_file_record_change_watcher.cc +++ b/components/tools/delta_file_record_change_watcher.cc @@ -26,7 +26,7 @@ #include "public/data_loading/filename_utils.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" #include "public/data_loading/records_utils.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" ABSL_FLAG(std::string, sns_arn, "", "sns_arn"); diff --git a/components/tools/delta_file_watcher_aws.cc b/components/tools/delta_file_watcher_aws.cc index bf4cea44..9a951a00 100644 --- a/components/tools/delta_file_watcher_aws.cc +++ b/components/tools/delta_file_watcher_aws.cc @@ -21,7 +21,7 @@ #include "components/data/blob_storage/delta_file_notifier.h" #include "components/data/common/thread_manager.h" #include "components/util/platform_initializer.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" ABSL_FLAG(std::string, bucket, "", "cloud storage bucket name"); ABSL_FLAG(std::string, sns_arn, "", "sns_arn"); @@ -74,7 +74,8 @@ int main(int argc, char** argv) { } const absl::Status status = notifier->Start( - **status_or_change_notifier, {.bucket = std::move(bucket)}, "", + **status_or_change_notifier, {.bucket = std::move(bucket)}, + /*prefix_start_after_map=*/{std::make_pair("", "")}, [](const std::string& key) { std::cout << key << std::endl; }); if (!status.ok()) { std::cerr << "Failed to start notifier: " << status << std::endl; diff --git a/components/tools/get_region_aws.cc b/components/tools/get_region_aws.cc index bee8805b..561bdd15 100644 --- a/components/tools/get_region_aws.cc +++ b/components/tools/get_region_aws.cc @@ -20,6 +20,7 @@ #include "absl/cleanup/cleanup.h" #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" @@ -30,7 +31,6 @@ #include "aws/core/internal/AWSHttpResourceClient.h" #include "aws/core/utils/Outcome.h" #include "components/errors/error_util_aws.h" -#include "glog/logging.h" ABSL_FLAG(std::string, output_file, "", "output_file"); diff --git a/components/tools/get_region_local.cc b/components/tools/get_region_local.cc index 7c86ffc8..176a4bd3 100644 --- a/components/tools/get_region_local.cc +++ b/components/tools/get_region_local.cc @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "glog/logging.h" +#include "absl/log/log.h" int main(int argc, char** argv) { LOG(FATAL) << "The get_region tool is not available for --platform==local"; diff --git a/components/tools/publisher_service_local.cc b/components/tools/publisher_service_local.cc new file mode 100644 index 00000000..1783fde1 --- /dev/null +++ b/components/tools/publisher_service_local.cc @@ -0,0 +1,48 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "components/tools/publisher_service.h" + +namespace kv_server { +namespace { + +class LocalPublisherService : public PublisherService { + public: + LocalPublisherService() {} + + absl::Status Publish(const std::string& body, std::optional shard_num) { + return absl::UnimplementedError( + "PublisherService is not implemented for local"); + } + + absl::StatusOr BuildNotifierMetadataAndSetQueue() { + return absl::UnimplementedError( + "PublisherService is not implemented for local"); + } +}; + +} // namespace + +absl::StatusOr> PublisherService::Create( + NotifierMetadata notifier_metadata) { + return std::make_unique(); +} + +absl::StatusOr PublisherService::GetNotifierMetadata() { + return absl::UnimplementedError( + "PublisherService is not implemented for local"); +} +} // namespace kv_server diff --git a/components/tools/realtime_notifier.cc b/components/tools/realtime_notifier.cc index a46e76ee..65667625 100644 --- a/components/tools/realtime_notifier.cc +++ b/components/tools/realtime_notifier.cc @@ -19,6 +19,8 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" #include "absl/strings/str_join.h" #include "components/data/common/msg_svc.h" #include "components/tools/publisher_service.h" @@ -27,7 +29,7 @@ #include "public/data_loading/filename_utils.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" #include "public/data_loading/records_utils.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" ABSL_FLAG(std::string, local_directory, "", "Local directory"); @@ -122,7 +124,7 @@ absl::Status Run() { int main(int argc, char* argv[]) { const std::vector commands = absl::ParseCommandLine(argc, argv); - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); const absl::Status status = kv_server::Run(); if (!status.ok()) { LOG(FATAL) << "Failed to run: " << status; diff --git a/components/tools/realtime_updates_publisher.cc b/components/tools/realtime_updates_publisher.cc index 45f8c2d3..65854d17 100644 --- a/components/tools/realtime_updates_publisher.cc +++ b/components/tools/realtime_updates_publisher.cc @@ -19,12 +19,14 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/strings/substitute.h" #include "components/data/common/msg_svc.h" #include "components/tools/concurrent_publishing_engine.h" #include "components/tools/publisher_service.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" ABSL_FLAG(std::string, deltas_folder_path, "", "Path to the folder with delta files"); @@ -93,7 +95,7 @@ absl::Status Run() { // `tools/serving_data_generator/generate_load_test_data`. int main(int argc, char** argv) { const std::vector commands = absl::ParseCommandLine(argc, argv); - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); const absl::Status status = kv_server::Run(); if (!status.ok()) { LOG(FATAL) << "Failed to run: " << status; diff --git a/components/tools/sharding_correctness_validator/BUILD.bazel b/components/tools/sharding_correctness_validator/BUILD.bazel index 6a3d0091..160abb3d 100644 --- a/components/tools/sharding_correctness_validator/BUILD.bazel +++ b/components/tools/sharding_correctness_validator/BUILD.bazel @@ -20,9 +20,11 @@ cc_binary( deps = [ "//public/applications/pa:response_utils", "//public/query/cpp:grpc_client", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/random", "@com_google_absl//absl/strings", ], diff --git a/components/tools/sharding_correctness_validator/validator.cc b/components/tools/sharding_correctness_validator/validator.cc index e0755045..5299bf79 100644 --- a/components/tools/sharding_correctness_validator/validator.cc +++ b/components/tools/sharding_correctness_validator/validator.cc @@ -16,10 +16,12 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/random/random.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" -#include "glog/logging.h" #include "public/applications/pa/response_utils.h" #include "public/query/cpp/grpc_client.h" @@ -30,6 +32,7 @@ ABSL_FLAG(int, number_of_requests_to_make, 1, "Number of requests to make"); ABSL_FLAG(int, value_size, 10000, "Specify the size of value for the key"); ABSL_FLAG(int, batch_size, 10, "Batch size"); ABSL_FLAG(std::string, key_prefix, "foo", "Key prefix"); +ABSL_FLAG(bool, use_tls, false, "Whether to use TLS for grpc calls."); namespace kv_server { namespace { @@ -121,11 +124,18 @@ void Validate() { const int qps = absl::GetFlag(FLAGS_qps); const int number_of_requests_to_make = absl::GetFlag(FLAGS_number_of_requests_to_make); + const bool use_tls = absl::GetFlag(FLAGS_use_tls); int requests_made_this_second = 0; int total_requests_made = 0; auto batch_end = absl::Now() + absl::Seconds(1); - std::unique_ptr stub = - GrpcClient::CreateStub(kv_endpoint, grpc::InsecureChannelCredentials()); + std::unique_ptr stub; + if (use_tls) { + stub = GrpcClient::CreateStub( + kv_endpoint, grpc::SslCredentials(grpc::SslCredentialsOptions())); + } else { + stub = + GrpcClient::CreateStub(kv_endpoint, grpc::InsecureChannelCredentials()); + } GrpcClient client(*stub); while (total_requests_made < number_of_requests_to_make) { auto random_index = Get(inclusive_upper_bound / batch_size); @@ -168,7 +178,7 @@ void Validate() { int main(int argc, char** argv) { const std::vector commands = absl::ParseCommandLine(argc, argv); - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); kv_server::Validate(); if (kv_server::total_failures > 0 || kv_server::total_mismatches > 0) { diff --git a/components/udf/BUILD.bazel b/components/udf/BUILD.bazel index 630b048a..5f7e7525 100644 --- a/components/udf/BUILD.bazel +++ b/components/udf/BUILD.bazel @@ -48,8 +48,8 @@ cc_library( "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_interface_lib", - "@google_privacysandbox_servers_common//scp/cc/roma/roma_service:roma_service_lib", + "@google_privacysandbox_servers_common//src/roma/interface", + "@google_privacysandbox_servers_common//src/roma/roma_service", ], ) @@ -83,8 +83,8 @@ cc_library( "//components/udf/hooks:get_values_hook", "//components/udf/hooks:logging_hook", "//components/udf/hooks:run_query_hook", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_interface_lib", - "@google_privacysandbox_servers_common//scp/cc/roma/roma_service:roma_service_lib", + "@google_privacysandbox_servers_common//src/roma/interface", + "@google_privacysandbox_servers_common//src/roma/roma_service", ], ) @@ -105,12 +105,15 @@ cc_test( "//components/internal_server:mocks", "//components/udf/hooks:get_values_hook", "//components/udf/hooks:run_query_hook", + "//public/query/v2:get_values_v2_cc_proto", "//public/test_util:proto_matcher", + "//public/udf:constants", + "@com_google_absl//absl/log:scoped_mock_log", "@com_google_absl//absl/status", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_interface_lib", - "@google_privacysandbox_servers_common//scp/cc/roma/roma_service:roma_service_lib", + "@google_privacysandbox_servers_common//src/roma/interface", + "@google_privacysandbox_servers_common//src/roma/roma_service", ], ) @@ -123,6 +126,6 @@ cc_library( ":udf_client", "@com_google_absl//absl/status", "@com_google_googletest//:gtest", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_interface_lib", + "@google_privacysandbox_servers_common//src/roma/interface", ], ) diff --git a/components/udf/hooks/BUILD.bazel b/components/udf/hooks/BUILD.bazel index b18d74b8..c3a37094 100644 --- a/components/udf/hooks/BUILD.bazel +++ b/components/udf/hooks/BUILD.bazel @@ -31,15 +31,14 @@ cc_library( "//components/internal_server:internal_lookup_cc_proto", "//components/internal_server:local_lookup", "//components/internal_server:lookup", + "//components/util:request_context", "//public/udf:binary_get_values_cc_proto", - "@com_github_google_glog//:glog", "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_interface_lib", - "@google_privacysandbox_servers_common//src/cpp/telemetry", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", + "@google_privacysandbox_servers_common//src/roma/interface", "@nlohmann_json//:lib", ], ) @@ -55,12 +54,13 @@ cc_library( deps = [ "//components/internal_server:internal_lookup_cc_proto", "//components/internal_server:lookup", - "@com_github_google_glog//:glog", + "//components/util:request_context", "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_function_binding_io_cc_proto", - "@google_privacysandbox_servers_common//scp/cc/roma/interface:roma_interface_lib", + "@google_privacysandbox_servers_common//src/roma/interface", + "@google_privacysandbox_servers_common//src/roma/interface:function_binding_io_cc_proto", "@nlohmann_json//:lib", ], ) @@ -71,7 +71,8 @@ cc_library( "logging_hook.h", ], deps = [ - "@com_github_google_glog//:glog", + "//components/util:request_context", + "@com_google_absl//absl/log", ], ) @@ -88,8 +89,6 @@ cc_test( "@com_google_absl//absl/status", "@com_google_googletest//:gtest", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", ], ) diff --git a/components/udf/hooks/get_values_hook.cc b/components/udf/hooks/get_values_hook.cc index 8d356071..e35fa682 100644 --- a/components/udf/hooks/get_values_hook.cc +++ b/components/udf/hooks/get_values_hook.cc @@ -21,16 +21,16 @@ #include #include "absl/functional/any_invocable.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/strings/str_join.h" #include "components/data_server/cache/cache.h" #include "components/internal_server/local_lookup.h" #include "components/internal_server/lookup.pb.h" -#include "glog/logging.h" +#include "components/telemetry/server_definition.h" #include "google/protobuf/util/json_util.h" #include "nlohmann/json.hpp" #include "public/udf/binary_get_values.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" namespace kv_server { namespace { @@ -38,7 +38,6 @@ namespace { using google::protobuf::json::MessageToJsonString; using google::scp::roma::FunctionBindingPayload; using google::scp::roma::proto::FunctionBindingIoProto; -using privacy_sandbox::server_common::MetricsRecorder; constexpr char kOkStatusMessage[] = "ok"; @@ -129,7 +128,7 @@ class GetValuesHookImpl : public GetValuesHook { } } - void operator()(FunctionBindingPayload<>& payload) { + void operator()(FunctionBindingPayload& payload) { VLOG(9) << "Called getValues hook"; if (lookup_ == nullptr) { SetStatus(absl::StatusCode::kInternal, @@ -154,7 +153,7 @@ class GetValuesHookImpl : public GetValuesHook { VLOG(9) << "Calling internal lookup client"; absl::StatusOr response_or_status = - lookup_->GetKeyValues(keys); + lookup_->GetKeyValues(payload.metadata, keys); if (!response_or_status.ok()) { SetStatus(response_or_status.status().code(), response_or_status.status().message(), payload.io_proto); diff --git a/components/udf/hooks/get_values_hook.h b/components/udf/hooks/get_values_hook.h index dba7a7e7..24d576ac 100644 --- a/components/udf/hooks/get_values_hook.h +++ b/components/udf/hooks/get_values_hook.h @@ -25,13 +25,11 @@ #include "absl/functional/any_invocable.h" #include "components/data_server/cache/cache.h" #include "components/internal_server/lookup.h" -#include "roma/config/src/function_binding_object_v2.h" -#include "src/cpp/telemetry/metrics_recorder.h" +#include "components/util/request_context.h" +#include "src/roma/config/function_binding_object_v2.h" namespace kv_server { -using privacy_sandbox::server_common::MetricsRecorder; - // Functor that acts as a wrapper for the internal lookup client call. class GetValuesHook { public: @@ -48,7 +46,7 @@ class GetValuesHook { // This is registered with v8 and is exposed to the UDF. Internally, it calls // the internal lookup client. virtual void operator()( - google::scp::roma::FunctionBindingPayload<>& payload) = 0; + google::scp::roma::FunctionBindingPayload& payload) = 0; static std::unique_ptr Create(OutputType output_type); }; diff --git a/components/udf/hooks/get_values_hook_test.cc b/components/udf/hooks/get_values_hook_test.cc index b74f8d12..3d6f2b03 100644 --- a/components/udf/hooks/get_values_hook_test.cc +++ b/components/udf/hooks/get_values_hook_test.cc @@ -38,7 +38,12 @@ using google::scp::roma::proto::FunctionBindingIoProto; using testing::_; using testing::Return; -TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesValue) { +class GetValuesHookTest : public ::testing::Test { + protected: + void SetUp() override { InitMetricsContextMap(); } +}; + +TEST_F(GetValuesHookTest, StringOutput_SuccessfullyProcessesValue) { absl::flat_hash_set keys = {"key1", "key2"}; InternalLookupResponse lookup_response; TextFormat::ParseFromString(R"pb(kv_pairs { @@ -51,7 +56,7 @@ TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesValue) { })pb", &lookup_response); auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, GetKeyValues(keys)) + EXPECT_CALL(*mock_lookup, GetKeyValues(_, keys)) .WillOnce(Return(lookup_response)); FunctionBindingIoProto io; @@ -60,7 +65,9 @@ TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesValue) { auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); get_values_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + FunctionBindingPayload payload{ + io, RequestContext(metrics_context)}; (*get_values_hook)(payload); nlohmann::json result_json = @@ -77,7 +84,7 @@ TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesValue) { EXPECT_EQ(result_json["status"]["message"], "ok"); } -TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesResultsWithStatus) { +TEST_F(GetValuesHookTest, StringOutput_SuccessfullyProcessesResultsWithStatus) { absl::flat_hash_set keys = {"key1"}; InternalLookupResponse lookup_response; TextFormat::ParseFromString( @@ -88,7 +95,7 @@ TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesResultsWithStatus) { &lookup_response); auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, GetKeyValues(keys)) + EXPECT_CALL(*mock_lookup, GetKeyValues(_, keys)) .WillOnce(Return(lookup_response)); FunctionBindingIoProto io; @@ -97,7 +104,9 @@ TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesResultsWithStatus) { auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); get_values_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + FunctionBindingPayload payload{ + io, RequestContext(metrics_context)}; (*get_values_hook)(payload); nlohmann::json expected = @@ -105,10 +114,10 @@ TEST(GetValuesHookTest, StringOutput_SuccessfullyProcessesResultsWithStatus) { EXPECT_EQ(io.output_string(), expected.dump()); } -TEST(GetValuesHookTest, StringOutput_LookupReturnsError) { +TEST_F(GetValuesHookTest, StringOutput_LookupReturnsError) { absl::flat_hash_set keys = {"key1"}; auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, GetKeyValues(keys)) + EXPECT_CALL(*mock_lookup, GetKeyValues(_, keys)) .WillOnce(Return(absl::UnknownError("Some error"))); FunctionBindingIoProto io; @@ -117,14 +126,16 @@ TEST(GetValuesHookTest, StringOutput_LookupReturnsError) { auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); get_values_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + FunctionBindingPayload payload{ + io, RequestContext(metrics_context)}; (*get_values_hook)(payload); nlohmann::json expected = R"({"code":2,"message":"Some error"})"_json; EXPECT_EQ(io.output_string(), expected.dump()); } -TEST(GetValuesHookTest, StringOutput_InputIsNotListOfStrings) { +TEST_F(GetValuesHookTest, StringOutput_InputIsNotListOfStrings) { absl::flat_hash_set keys = {"key1"}; auto mock_lookup = std::make_unique(); @@ -133,7 +144,9 @@ TEST(GetValuesHookTest, StringOutput_InputIsNotListOfStrings) { auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); get_values_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + FunctionBindingPayload payload{ + io, RequestContext(metrics_context)}; (*get_values_hook)(payload); nlohmann::json expected = @@ -141,7 +154,7 @@ TEST(GetValuesHookTest, StringOutput_InputIsNotListOfStrings) { EXPECT_EQ(io.output_string(), expected.dump()); } -TEST(GetValuesHookTest, BinaryOutput_SuccessfullyProcessesValue) { +TEST_F(GetValuesHookTest, BinaryOutput_SuccessfullyProcessesValue) { absl::flat_hash_set keys = {"key1", "key2"}; InternalLookupResponse lookup_response; TextFormat::ParseFromString( @@ -155,7 +168,7 @@ TEST(GetValuesHookTest, BinaryOutput_SuccessfullyProcessesValue) { })pb", &lookup_response); auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, GetKeyValues(keys)) + EXPECT_CALL(*mock_lookup, GetKeyValues(_, keys)) .WillOnce(Return(lookup_response)); FunctionBindingIoProto io; @@ -164,7 +177,9 @@ TEST(GetValuesHookTest, BinaryOutput_SuccessfullyProcessesValue) { auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kBinary); get_values_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + FunctionBindingPayload payload{ + io, RequestContext(metrics_context)}; (*get_values_hook)(payload); EXPECT_TRUE(io.has_output_bytes()); @@ -187,10 +202,10 @@ TEST(GetValuesHookTest, BinaryOutput_SuccessfullyProcessesValue) { EXPECT_THAT(response.kv_pairs().at("key2"), EqualsProto(value_with_status)); } -TEST(GetValuesHookTest, BinaryOutput_LookupReturnsError) { +TEST_F(GetValuesHookTest, BinaryOutput_LookupReturnsError) { absl::flat_hash_set keys = {"key1"}; auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, GetKeyValues(keys)) + EXPECT_CALL(*mock_lookup, GetKeyValues(_, keys)) .WillOnce(Return(absl::UnknownError("Some error"))); FunctionBindingIoProto io; @@ -199,7 +214,9 @@ TEST(GetValuesHookTest, BinaryOutput_LookupReturnsError) { auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kBinary); get_values_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + FunctionBindingPayload payload{ + io, RequestContext(metrics_context)}; (*get_values_hook)(payload); EXPECT_TRUE(io.has_output_bytes()); diff --git a/components/udf/hooks/logging_hook.h b/components/udf/hooks/logging_hook.h index 296c358e..3e541600 100644 --- a/components/udf/hooks/logging_hook.h +++ b/components/udf/hooks/logging_hook.h @@ -20,17 +20,16 @@ #include #include -#include "glog/logging.h" +#include "absl/log/log.h" +#include "components/util/request_context.h" namespace kv_server { -// UDF hook for logging a string. -// TODO(b/285331079): Disable for production builds. -inline void LogMessage(google::scp::roma::FunctionBindingPayload<>& payload) { - if (payload.io_proto.has_input_string()) { - LOG(INFO) << payload.io_proto.input_string(); - } - payload.io_proto.set_output_string(""); +// Logging function to register with Roma. +inline void LoggingFunction(absl::LogSeverity severity, + const RequestContext& context, + std::string_view msg) { + LOG(LEVEL(severity)) << msg; } } // namespace kv_server diff --git a/components/udf/hooks/run_query_hook.cc b/components/udf/hooks/run_query_hook.cc index f878ced7..c2aa113f 100644 --- a/components/udf/hooks/run_query_hook.cc +++ b/components/udf/hooks/run_query_hook.cc @@ -21,9 +21,9 @@ #include #include "absl/functional/any_invocable.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "components/internal_server/lookup.h" -#include "glog/logging.h" #include "nlohmann/json.hpp" namespace kv_server { @@ -39,7 +39,7 @@ class RunQueryHookImpl : public RunQueryHook { } } - void operator()(FunctionBindingPayload<>& payload) { + void operator()(FunctionBindingPayload& payload) { if (lookup_ == nullptr) { nlohmann::json status; status["code"] = absl::StatusCode::kInternal; @@ -62,7 +62,7 @@ class RunQueryHookImpl : public RunQueryHook { VLOG(9) << "Calling internal run query client"; absl::StatusOr response_or_status = - lookup_->RunQuery(payload.io_proto.input_string()); + lookup_->RunQuery(payload.metadata, payload.io_proto.input_string()); if (!response_or_status.ok()) { LOG(ERROR) << "Internal run query returned error: " diff --git a/components/udf/hooks/run_query_hook.h b/components/udf/hooks/run_query_hook.h index 1ab871c8..35560c3e 100644 --- a/components/udf/hooks/run_query_hook.h +++ b/components/udf/hooks/run_query_hook.h @@ -24,7 +24,8 @@ #include "absl/functional/any_invocable.h" #include "components/internal_server/lookup.h" -#include "roma/config/src/function_binding_object_v2.h" +#include "components/util/request_context.h" +#include "src/roma/config/function_binding_object_v2.h" namespace kv_server { @@ -42,7 +43,7 @@ class RunQueryHook { // This is registered with v8 and is exposed to the UDF. Internally, it calls // the internal query client. virtual void operator()( - google::scp::roma::FunctionBindingPayload<>& payload) = 0; + google::scp::roma::FunctionBindingPayload& payload) = 0; static std::unique_ptr Create(); }; diff --git a/components/udf/hooks/run_query_hook_test.cc b/components/udf/hooks/run_query_hook_test.cc index d1a1d54d..985513de 100644 --- a/components/udf/hooks/run_query_hook_test.cc +++ b/components/udf/hooks/run_query_hook_test.cc @@ -36,41 +36,50 @@ using testing::_; using testing::Return; using testing::UnorderedElementsAreArray; -TEST(RunQueryHookTest, SuccessfullyProcessesValue) { +class RunQueryHookTest : public ::testing::Test { + protected: + void SetUp() override { InitMetricsContextMap(); } +}; + +TEST_F(RunQueryHookTest, SuccessfullyProcessesValue) { std::string query = "Q"; InternalRunQueryResponse run_query_response; TextFormat::ParseFromString(R"pb(elements: "a" elements: "b")pb", &run_query_response); auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, RunQuery(query)) + EXPECT_CALL(*mock_lookup, RunQuery(_, query)) .WillOnce(Return(run_query_response)); FunctionBindingIoProto io; TextFormat::ParseFromString(R"pb(input_string: "Q")pb", &io); auto run_query_hook = RunQueryHook::Create(); run_query_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + RequestContext request_context(metrics_context); + FunctionBindingPayload payload{io, request_context}; (*run_query_hook)(payload); EXPECT_THAT(io.output_list_of_string().data(), UnorderedElementsAreArray({"a", "b"})); } -TEST(GetValuesHookTest, RunQueryClientReturnsError) { +TEST_F(RunQueryHookTest, RunQueryClientReturnsError) { std::string query = "Q"; auto mock_lookup = std::make_unique(); - EXPECT_CALL(*mock_lookup, RunQuery(query)) + EXPECT_CALL(*mock_lookup, RunQuery(_, query)) .WillOnce(Return(absl::UnknownError("Some error"))); FunctionBindingIoProto io; TextFormat::ParseFromString(R"pb(input_string: "Q")pb", &io); auto run_query_hook = RunQueryHook::Create(); run_query_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + RequestContext request_context(metrics_context); + FunctionBindingPayload payload{io, request_context}; (*run_query_hook)(payload); EXPECT_TRUE(io.output_list_of_string().data().empty()); } -TEST(GetValuesHookTest, InputIsNotString) { +TEST_F(RunQueryHookTest, InputIsNotString) { auto mock_lookup = std::make_unique(); FunctionBindingIoProto io; @@ -78,7 +87,9 @@ TEST(GetValuesHookTest, InputIsNotString) { &io); auto run_query_hook = RunQueryHook::Create(); run_query_hook->FinishInit(std::move(mock_lookup)); - FunctionBindingPayload<> payload{io, {}}; + ScopeMetricsContext metrics_context; + RequestContext request_context(metrics_context); + FunctionBindingPayload payload{io, request_context}; (*run_query_hook)(payload); EXPECT_THAT( diff --git a/components/udf/mocks.h b/components/udf/mocks.h index cba13db7..8e8bbbc5 100644 --- a/components/udf/mocks.h +++ b/components/udf/mocks.h @@ -25,16 +25,16 @@ #include "components/udf/code_config.h" #include "components/udf/udf_client.h" #include "gmock/gmock.h" -#include "roma/interface/roma.h" +#include "src/roma/interface/roma.h" namespace kv_server { class MockUdfClient : public UdfClient { public: MOCK_METHOD((absl::StatusOr), ExecuteCode, - (std::vector), (const, override)); + (RequestContext, std::vector), (const, override)); MOCK_METHOD((absl::StatusOr), ExecuteCode, - (UDFExecutionMetadata&&, + (RequestContext, UDFExecutionMetadata&&, const google::protobuf::RepeatedPtrField&), (const, override)); MOCK_METHOD((absl::Status), Stop, (), (override)); diff --git a/components/udf/noop_udf_client.cc b/components/udf/noop_udf_client.cc index ceacba4f..e257a7e7 100644 --- a/components/udf/noop_udf_client.cc +++ b/components/udf/noop_udf_client.cc @@ -24,18 +24,19 @@ #include "absl/status/statusor.h" #include "components/udf/code_config.h" #include "components/udf/udf_client.h" -#include "roma/config/src/config.h" +#include "src/roma/config/config.h" namespace kv_server { namespace { class NoopUdfClientImpl : public UdfClient { public: - absl::StatusOr ExecuteCode(std::vector keys) const { + absl::StatusOr ExecuteCode(RequestContext request_context, + std::vector keys) const { return ""; } absl::StatusOr ExecuteCode( - UDFExecutionMetadata&&, + RequestContext request_context, UDFExecutionMetadata&&, const google::protobuf::RepeatedPtrField& arguments) const { return ""; } diff --git a/components/udf/udf_client.cc b/components/udf/udf_client.cc index d8ecb289..8569c7f5 100644 --- a/components/udf/udf_client.cc +++ b/components/udf/udf_client.cc @@ -22,15 +22,15 @@ #include #include "absl/flags/flag.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" #include "absl/time/time.h" -#include "glog/logging.h" #include "google/protobuf/util/json_util.h" -#include "roma/config/src/config.h" -#include "roma/interface/roma.h" -#include "roma/roma_service/roma_service.h" +#include "src/roma/config/config.h" +#include "src/roma/interface/roma.h" +#include "src/roma/roma_service/roma_service.h" namespace kv_server { @@ -54,13 +54,16 @@ constexpr int kUdfInterfaceVersion = 1; class UdfClientImpl : public UdfClient { public: - explicit UdfClientImpl(Config<>&& config = Config(), - absl::Duration udf_timeout = absl::Seconds(5)) - : udf_timeout_(udf_timeout), roma_service_(std::move(config)) {} + explicit UdfClientImpl( + Config&& config = Config(), + absl::Duration udf_timeout = absl::Seconds(5), int udf_min_log_level = 0) + : udf_timeout_(udf_timeout), + roma_service_(std::move(config)), + udf_min_log_level_(udf_min_log_level) {} // Converts the arguments into plain JSON strings to pass to Roma. absl::StatusOr ExecuteCode( - UDFExecutionMetadata&& execution_metadata, + RequestContext request_context, UDFExecutionMetadata&& execution_metadata, const google::protobuf::RepeatedPtrField& arguments) const { execution_metadata.set_udf_interface_version(kUdfInterfaceVersion); std::vector string_args; @@ -88,27 +91,29 @@ class UdfClientImpl : public UdfClient { } string_args.push_back(json_arg); } - return ExecuteCode(std::move(string_args)); + return ExecuteCode(std::move(request_context), std::move(string_args)); } - absl::StatusOr ExecuteCode(std::vector keys) const { + absl::StatusOr ExecuteCode( + RequestContext request_context, std::vector input) const { std::shared_ptr response_status = std::make_shared(); std::shared_ptr result = std::make_shared(); std::shared_ptr notification = std::make_shared(); - InvocationStrRequest<> invocation_request = - BuildInvocationRequest(std::move(keys)); - VLOG(9) << "Executing UDF"; + auto invocation_request = + BuildInvocationRequest(std::move(request_context), std::move(input)); + VLOG(9) << "Executing UDF with input arg(s): " + << absl::StrJoin(invocation_request.input, ","); const auto status = roma_service_.Execute( - std::make_unique>(invocation_request), + std::make_unique>( + std::move(invocation_request)), [notification, response_status, - result](std::unique_ptr> response) { - if (response->ok()) { - auto& code_response = **response; - *result = std::move(code_response.resp); + result](absl::StatusOr response) { + if (response.ok()) { + *result = std::move(response->resp); } else { - response_status->Update(std::move(response->status())); + response_status->Update(std::move(response.status())); } notification->Notify(); }); @@ -149,10 +154,9 @@ class UdfClientImpl : public UdfClient { code_config.version); absl::Status load_status = roma_service_.LoadCodeObj( std::make_unique(code_object), - [notification, response_status]( - std::unique_ptr> resp) { - if (!resp->ok()) { - response_status->Update(std::move(resp->status())); + [notification, response_status](absl::StatusOr resp) { + if (!resp.ok()) { + response_status->Update(std::move(resp.status())); } notification->Notify(); }); @@ -166,12 +170,14 @@ class UdfClientImpl : public UdfClient { return absl::InternalError("Timed out setting UDF code object."); } if (!response_status->ok()) { - LOG(ERROR) << "Error setting UDF Code object: " << *response_status; + LOG(ERROR) << "Error compiling UDF code object. " << *response_status; return *response_status; } handler_name_ = std::move(code_config.udf_handler_name); logical_commit_time_ = code_config.logical_commit_time; version_ = code_config.version; + VLOG(5) << "Successfully set UDF code object with handler_name " + << handler_name_; return absl::OkStatus(); } @@ -184,13 +190,16 @@ class UdfClientImpl : public UdfClient { } private: - InvocationStrRequest<> BuildInvocationRequest( - std::vector keys) const { + InvocationStrRequest BuildInvocationRequest( + RequestContext request_context, std::vector input) const { return {.id = kInvocationRequestId, .version_string = absl::StrCat("v", version_), .handler_name = handler_name_, - .tags = {{kTimeoutDurationTag, FormatDuration(udf_timeout_)}}, - .input = std::move(keys)}; + .tags = {{std::string(kTimeoutDurationTag), + FormatDuration(udf_timeout_)}}, + .input = std::move(input), + .metadata = std::move(request_context), + .min_log_level = absl::LogSeverity(udf_min_log_level_)}; } CodeObject BuildCodeObject(std::string js, std::string wasm, @@ -205,6 +214,7 @@ class UdfClientImpl : public UdfClient { int64_t logical_commit_time_ = -1; int64_t version_ = 1; const absl::Duration udf_timeout_; + int udf_min_log_level_; // Per b/299667930, RomaService has been extended to support metadata storage // as a side effect of RomaService::Execute(), making it no longer const. // However, UDFClient::ExecuteCode() remains logically const, so RomaService @@ -212,15 +222,16 @@ class UdfClientImpl : public UdfClient { // concerns about mutable or go/totw/174, RomaService is thread-safe, so // losing the thread-safety of usage within a const function is a lesser // concern. - mutable RomaService<> roma_service_; + mutable RomaService roma_service_; }; } // namespace absl::StatusOr> UdfClient::Create( - Config<>&& config, absl::Duration udf_timeout) { - auto udf_client = - std::make_unique(std::move(config), udf_timeout); + Config&& config, absl::Duration udf_timeout, + int udf_min_log_level) { + auto udf_client = std::make_unique( + std::move(config), udf_timeout, udf_min_log_level); const auto init_status = udf_client->Init(); if (!init_status.ok()) { return init_status; diff --git a/components/udf/udf_client.h b/components/udf/udf_client.h index 99d274fb..abb9e8bd 100644 --- a/components/udf/udf_client.h +++ b/components/udf/udf_client.h @@ -23,11 +23,13 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "components/telemetry/server_definition.h" #include "components/udf/code_config.h" +#include "components/util/request_context.h" #include "google/protobuf/message.h" #include "public/api_schema.pb.h" -#include "roma/config/src/config.h" -#include "roma/interface/roma.h" +#include "src/roma/config/config.h" +#include "src/roma/interface/roma.h" namespace kv_server { @@ -41,12 +43,12 @@ class UdfClient { // UDF signature. ABSL_DEPRECATED("Use ExecuteCode(metadata, arguments) instead") virtual absl::StatusOr ExecuteCode( - std::vector keys) const = 0; + RequestContext request_context, std::vector keys) const = 0; // Executes the UDF. Code object must be set before making // this call. virtual absl::StatusOr ExecuteCode( - UDFExecutionMetadata&& execution_metadata, + RequestContext request_context, UDFExecutionMetadata&& execution_metadata, const google::protobuf::RepeatedPtrField& arguments) const = 0; @@ -60,8 +62,9 @@ class UdfClient { // Creates a UDF executor. This calls Roma::Init, which forks. static absl::StatusOr> Create( - google::scp::roma::Config<>&& config = google::scp::roma::Config(), - absl::Duration udf_timeout = absl::Seconds(5)); + google::scp::roma::Config&& config = + google::scp::roma::Config(), + absl::Duration udf_timeout = absl::Seconds(5), int udf_min_log_level = 0); }; } // namespace kv_server diff --git a/components/udf/udf_client_test.cc b/components/udf/udf_client_test.cc index 1b193ea0..755d5a00 100644 --- a/components/udf/udf_client_test.cc +++ b/components/udf/udf_client_test.cc @@ -20,6 +20,7 @@ #include #include +#include "absl/log/scoped_mock_log.h" #include "absl/status/statusor.h" #include "components/internal_server/mocks.h" #include "components/udf/code_config.h" @@ -30,14 +31,15 @@ #include "gmock/gmock.h" #include "google/protobuf/text_format.h" #include "gtest/gtest.h" -#include "roma/config/src/config.h" -#include "roma/interface/roma.h" +#include "public/query/v2/get_values_v2.pb.h" +#include "public/udf/constants.h" +#include "src/roma/config/config.h" +#include "src/roma/interface/roma.h" using google::protobuf::TextFormat; using google::scp::roma::Config; using google::scp::roma::FunctionBindingObjectV2; using google::scp::roma::FunctionBindingPayload; -using google::scp::roma::WasmDataType; using testing::_; using testing::Return; @@ -45,19 +47,24 @@ namespace kv_server { namespace { absl::StatusOr> CreateUdfClient() { - Config config; + Config config; config.number_of_workers = 1; return UdfClient::Create(std::move(config)); } -TEST(UdfClientTest, UdfClient_Create_Success) { +class UdfClientTest : public ::testing::Test { + protected: + void SetUp() override { InitMetricsContextMap(); } +}; + +TEST_F(UdfClientTest, UdfClient_Create_Success) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); absl::Status stop = udf_client.value()->Stop(); EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsCallSucceeds) { +TEST_F(UdfClientTest, JsCallSucceeds) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -68,8 +75,9 @@ TEST(UdfClientTest, JsCallSucceeds) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result = udf_client.value()->ExecuteCode({}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Hello world!")"); @@ -77,7 +85,7 @@ TEST(UdfClientTest, JsCallSucceeds) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, RepeatedJsCallsSucceed) { +TEST_F(UdfClientTest, RepeatedJsCallsSucceed) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -88,12 +96,14 @@ TEST(UdfClientTest, RepeatedJsCallsSucceed) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result1 = udf_client.value()->ExecuteCode({}); + ScopeMetricsContext metrics_context; + absl::StatusOr result1 = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_TRUE(result1.ok()); EXPECT_EQ(*result1, R"("Hello world!")"); - absl::StatusOr result2 = udf_client.value()->ExecuteCode({}); + absl::StatusOr result2 = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_TRUE(result2.ok()); EXPECT_EQ(*result2, R"("Hello world!")"); @@ -101,7 +111,7 @@ TEST(UdfClientTest, RepeatedJsCallsSucceed) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsEchoCallSucceeds) { +TEST_F(UdfClientTest, JsEchoCallSucceeds) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -112,9 +122,9 @@ TEST(UdfClientTest, JsEchoCallSucceeds) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result = - udf_client.value()->ExecuteCode({R"("ECHO")"}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {R"("ECHO")"}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Hello world! \"ECHO\"")"); @@ -122,7 +132,7 @@ TEST(UdfClientTest, JsEchoCallSucceeds) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string) { +TEST_F(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -141,8 +151,9 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string) { arg.mutable_data()->set_string_value("ECHO"); return arg; }()); - absl::StatusOr result = - udf_client.value()->ExecuteCode({}, args); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {}, args); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Hello world! \"ECHO\"")"); @@ -150,7 +161,7 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged) { +TEST_F(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -170,8 +181,9 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged) { arg.mutable_data()->set_string_value("ECHO"); return arg; }()); - absl::StatusOr result = - udf_client.value()->ExecuteCode({}, args); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {}, args); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Hello world! {\"tags\":[\"tag1\"],\"data\":\"ECHO\"}")"); @@ -179,7 +191,7 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged) { absl::Status stop = udf_client.value()->Stop(); EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged_list) { +TEST_F(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged_list) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -201,8 +213,9 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged_list) { list_value->add_values()->set_string_value("key2"); return arg; }()); - absl::StatusOr result = - udf_client.value()->ExecuteCode({}, args); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {}, args); EXPECT_TRUE(result.ok()); EXPECT_EQ( *result, @@ -212,7 +225,7 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_string_tagged_list) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_struct) { +TEST_F(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_struct) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -232,8 +245,9 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_struct) { .set_string_value("value"); return arg; }()); - absl::StatusOr result = - udf_client.value()->ExecuteCode({}, args); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {}, args); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Hello world! {\"key\":\"value\"}")"); @@ -241,20 +255,20 @@ TEST(UdfClientTest, JsEchoCallSucceeds_SimpleUDFArg_struct) { EXPECT_TRUE(stop.ok()); } -static void udfCbEcho(FunctionBindingPayload<>& payload) { +static void udfCbEcho(FunctionBindingPayload& payload) { payload.io_proto.set_output_string("Echo: " + payload.io_proto.input_string()); } -TEST(UdfClientTest, JsEchoHookCallSucceeds) { - auto function_object = std::make_unique>(); +TEST_F(UdfClientTest, JsEchoHookCallSucceeds) { + auto function_object = + std::make_unique>(); function_object->function_name = "echo"; function_object->function = udfCbEcho; - Config config; + Config config; config.number_of_workers = 1; config.RegisterFunctionBinding(std::move(function_object)); - absl::StatusOr> udf_client = UdfClient::Create(std::move(config)); EXPECT_TRUE(udf_client.ok()); @@ -266,9 +280,9 @@ TEST(UdfClientTest, JsEchoHookCallSucceeds) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result = - udf_client.value()->ExecuteCode({R"("I'm a key")"}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {R"("I'm a key")"}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Hello world! Echo: I'm a key")"); @@ -276,7 +290,7 @@ TEST(UdfClientTest, JsEchoHookCallSucceeds) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsStringInWithGetValuesHookSucceeds) { +TEST_F(UdfClientTest, JsStringInWithGetValuesHookSucceeds) { auto mock_lookup = std::make_unique(); InternalLookupResponse response; @@ -285,7 +299,7 @@ TEST(UdfClientTest, JsStringInWithGetValuesHookSucceeds) { value { value: "value1" } })pb", &response); - ON_CALL(*mock_lookup, GetKeyValues(_)).WillByDefault(Return(response)); + ON_CALL(*mock_lookup, GetKeyValues(_, _)).WillByDefault(Return(response)); auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); @@ -315,9 +329,9 @@ TEST(UdfClientTest, JsStringInWithGetValuesHookSucceeds) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result = - udf_client.value()->ExecuteCode({R"("key1")"}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {R"("key1")"}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Key: key1, Value: value1")"); @@ -325,7 +339,7 @@ TEST(UdfClientTest, JsStringInWithGetValuesHookSucceeds) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsJSONObjectInWithGetValuesHookSucceeds) { +TEST_F(UdfClientTest, JsJSONObjectInWithGetValuesHookSucceeds) { auto mock_lookup = std::make_unique(); InternalLookupResponse response; @@ -334,7 +348,7 @@ TEST(UdfClientTest, JsJSONObjectInWithGetValuesHookSucceeds) { value { value: "value1" } })pb", &response); - ON_CALL(*mock_lookup, GetKeyValues(_)).WillByDefault(Return(response)); + ON_CALL(*mock_lookup, GetKeyValues(_, _)).WillByDefault(Return(response)); auto get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); @@ -366,9 +380,9 @@ TEST(UdfClientTest, JsJSONObjectInWithGetValuesHookSucceeds) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result = - udf_client.value()->ExecuteCode({R"({"keys":["key1"]})"}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {R"({"keys":["key1"]})"}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("Key: key1, Value: value1")"); @@ -376,12 +390,12 @@ TEST(UdfClientTest, JsJSONObjectInWithGetValuesHookSucceeds) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsJSONObjectInWithRunQueryHookSucceeds) { +TEST_F(UdfClientTest, JsJSONObjectInWithRunQueryHookSucceeds) { auto mock_lookup = std::make_unique(); InternalRunQueryResponse response; TextFormat::ParseFromString(R"pb(elements: "a")pb", &response); - ON_CALL(*mock_lookup, RunQuery(_)).WillByDefault(Return(response)); + ON_CALL(*mock_lookup, RunQuery(_, _)).WillByDefault(Return(response)); auto run_query_hook = RunQueryHook::Create(); run_query_hook->FinishInit(std::move(mock_lookup)); @@ -405,9 +419,9 @@ TEST(UdfClientTest, JsJSONObjectInWithRunQueryHookSucceeds) { .version = 1, }); EXPECT_TRUE(code_obj_status.ok()); - - absl::StatusOr result = - udf_client.value()->ExecuteCode({R"({"keys":["key1"]})"}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {R"({"keys":["key1"]})"}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"(["a"])"); @@ -415,19 +429,21 @@ TEST(UdfClientTest, JsJSONObjectInWithRunQueryHookSucceeds) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, JsCallsLogMessageTwiceSucceeds) { +TEST_F(UdfClientTest, JsCallsLoggingFunctionSucceeds) { UdfConfigBuilder config_builder; absl::StatusOr> udf_client = - UdfClient::Create(std::move( - config_builder.RegisterLoggingHook().SetNumberOfWorkers(1).Config())); + UdfClient::Create(std::move(config_builder.RegisterLoggingFunction() + .SetNumberOfWorkers(1) + .Config())); EXPECT_TRUE(udf_client.ok()); absl::Status code_obj_status = udf_client.value()->SetCodeObject(CodeConfig{ .js = R"( function hello(input) { - const a = logMessage("first message"); - const b = logMessage("second message"); - return a + b; + const a = console.error("Error message"); + const b = console.warn("Warning message"); + const c = console.log("Info message"); + return ""; } )", .udf_handler_name = "hello", @@ -436,16 +452,26 @@ TEST(UdfClientTest, JsCallsLogMessageTwiceSucceeds) { }); EXPECT_TRUE(code_obj_status.ok()); - absl::StatusOr result = - udf_client.value()->ExecuteCode({R"({"keys":["key1"]})"}); + absl::ScopedMockLog log; + EXPECT_CALL(log, Log(absl::LogSeverity::kError, testing::_, "Error message")); + EXPECT_CALL(log, + Log(absl::LogSeverity::kWarning, testing::_, "Warning message")); + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, testing::_, "Info message")); + log.StartCapturingLogs(); + + ScopeMetricsContext metrics_context; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), {R"({"keys":["key1"]})"}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("")"); + log.StopCapturingLogs(); + absl::Status stop = udf_client.value()->Stop(); EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, UpdatesCodeObjectTwice) { +TEST_F(UdfClientTest, UpdatesCodeObjectTwice) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -464,8 +490,9 @@ TEST(UdfClientTest, UpdatesCodeObjectTwice) { .version = 2, }); EXPECT_TRUE(status.ok()); - - absl::StatusOr result = udf_client.value()->ExecuteCode({}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("2")"); @@ -473,7 +500,7 @@ TEST(UdfClientTest, UpdatesCodeObjectTwice) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, IgnoresCodeObjectWithSameCommitTime) { +TEST_F(UdfClientTest, IgnoresCodeObjectWithSameCommitTime) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -492,8 +519,9 @@ TEST(UdfClientTest, IgnoresCodeObjectWithSameCommitTime) { .version = 1, }); EXPECT_TRUE(status.ok()); - - absl::StatusOr result = udf_client.value()->ExecuteCode({}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("1")"); @@ -501,7 +529,7 @@ TEST(UdfClientTest, IgnoresCodeObjectWithSameCommitTime) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, IgnoresCodeObjectWithSmallerCommitTime) { +TEST_F(UdfClientTest, IgnoresCodeObjectWithSmallerCommitTime) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); @@ -520,8 +548,9 @@ TEST(UdfClientTest, IgnoresCodeObjectWithSmallerCommitTime) { .version = 1, }); EXPECT_TRUE(status.ok()); - - absl::StatusOr result = udf_client.value()->ExecuteCode({}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_TRUE(result.ok()); EXPECT_EQ(*result, R"("1")"); @@ -529,11 +558,12 @@ TEST(UdfClientTest, IgnoresCodeObjectWithSmallerCommitTime) { EXPECT_TRUE(stop.ok()); } -TEST(UdfClientTest, CodeObjectNotSetError) { +TEST_F(UdfClientTest, CodeObjectNotSetError) { auto udf_client = CreateUdfClient(); EXPECT_TRUE(udf_client.ok()); - - absl::StatusOr result = udf_client.value()->ExecuteCode({}); + ScopeMetricsContext metrics_context; + absl::StatusOr result = + udf_client.value()->ExecuteCode(RequestContext(metrics_context), {}); EXPECT_FALSE(result.ok()); EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument); @@ -541,6 +571,200 @@ TEST(UdfClientTest, CodeObjectNotSetError) { EXPECT_TRUE(stop.ok()); } +TEST_F(UdfClientTest, MetadataPassedSuccesfully) { + UdfConfigBuilder config_builder; + absl::StatusOr> udf_client = UdfClient::Create( + std::move(config_builder.SetNumberOfWorkers(1).Config())); + EXPECT_TRUE(udf_client.ok()); + absl::Status code_obj_status = udf_client.value()->SetCodeObject(CodeConfig{ + .js = R"( + function hello(metadata) { + if(metadata.requestMetadata && + metadata.requestMetadata.is_pas) + { + return "true"; + } + return "false"; + } + )", + .udf_handler_name = "hello", + .logical_commit_time = 1, + .version = 1, + }); + EXPECT_TRUE(code_obj_status.ok()); + v2::GetValuesRequest req; + (*(req.mutable_metadata()->mutable_fields()))["is_pas"].set_string_value( + "true"); + UDFExecutionMetadata udf_metadata; + *udf_metadata.mutable_request_metadata() = *req.mutable_metadata(); + ScopeMetricsContext metrics_context; + google::protobuf::RepeatedPtrField args; + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), std::move(udf_metadata), args); + EXPECT_TRUE(result.ok()); + EXPECT_EQ(*result, R"("true")"); + + UDFExecutionMetadata udf_metadata_non_pas; + result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), std::move(udf_metadata_non_pas), args); + EXPECT_TRUE(result.ok()); + EXPECT_EQ(*result, R"("false")"); + absl::Status stop = udf_client.value()->Stop(); + EXPECT_TRUE(stop.ok()); +} + +TEST_F(UdfClientTest, DefaultUdfPASucceeds) { + auto mock_lookup = std::make_unique(); + InternalLookupResponse response; + TextFormat::ParseFromString(R"pb(kv_pairs { + key: "key1" + value { value: "value1" } + })pb", + &response); + ON_CALL(*mock_lookup, GetKeyValues(_, _)).WillByDefault(Return(response)); + + auto get_values_hook = + GetValuesHook::Create(GetValuesHook::OutputType::kString); + get_values_hook->FinishInit(std::move(mock_lookup)); + UdfConfigBuilder config_builder; + absl::StatusOr> udf_client = UdfClient::Create( + std::move(config_builder.RegisterStringGetValuesHook(*get_values_hook) + .SetNumberOfWorkers(1) + .Config())); + EXPECT_TRUE(udf_client.ok()); + absl::Status code_obj_status = udf_client.value()->SetCodeObject(CodeConfig{ + .js = kDefaultUdfCodeSnippet, + .udf_handler_name = kDefaultUdfHandlerName, + .logical_commit_time = kDefaultLogicalCommitTime, + .version = kDefaultVersion, + }); + EXPECT_TRUE(code_obj_status.ok()); + ScopeMetricsContext metrics_context; + UDFExecutionMetadata udf_metadata; + google::protobuf::RepeatedPtrField args; + args.Add([] { + UDFArgument arg; + TextFormat::ParseFromString(R"( + data { + list_value { + values { + string_value: "key1" + } + } + })", + &arg); + return arg; + }()); + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), std::move(udf_metadata), args); + EXPECT_TRUE(result.ok()); + EXPECT_EQ( + *result, + R"({"keyGroupOutputs":[{"keyValues":{"key1":{"value":"value1"}}}],"udfOutputApiVersion":1})"); + absl::Status stop = udf_client.value()->Stop(); + EXPECT_TRUE(stop.ok()); +} + +TEST_F(UdfClientTest, DefaultUdfPasKeyLookupFails) { + auto mock_lookup = std::make_unique(); + absl::Status status = absl::InvalidArgumentError("Error!"); + ON_CALL(*mock_lookup, GetKeyValues(_, _)).WillByDefault(Return(status)); + auto get_values_hook = + GetValuesHook::Create(GetValuesHook::OutputType::kString); + get_values_hook->FinishInit(std::move(mock_lookup)); + UdfConfigBuilder config_builder; + absl::StatusOr> udf_client = UdfClient::Create( + std::move(config_builder.RegisterStringGetValuesHook(*get_values_hook) + .SetNumberOfWorkers(1) + .Config())); + EXPECT_TRUE(udf_client.ok()); + absl::Status code_obj_status = udf_client.value()->SetCodeObject(CodeConfig{ + .js = kDefaultUdfCodeSnippet, + .udf_handler_name = kDefaultUdfHandlerName, + .logical_commit_time = kDefaultLogicalCommitTime, + .version = kDefaultVersion, + }); + EXPECT_TRUE(code_obj_status.ok()); + ScopeMetricsContext metrics_context; + v2::GetValuesRequest req; + (*(req.mutable_metadata()->mutable_fields()))["is_pas"].set_string_value( + "true"); + UDFExecutionMetadata udf_metadata; + *udf_metadata.mutable_request_metadata() = *req.mutable_metadata(); + google::protobuf::RepeatedPtrField args; + args.Add([] { + UDFArgument arg; + TextFormat::ParseFromString(R"( + data { + list_value { + values { + string_value: "key1" + } + } + })", + &arg); + return arg; + }()); + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), std::move(udf_metadata), args); + EXPECT_FALSE(result.ok()); + absl::Status stop = udf_client.value()->Stop(); + EXPECT_TRUE(stop.ok()); +} + +TEST_F(UdfClientTest, DefaultUdfPasSucceeds) { + auto mock_lookup = std::make_unique(); + InternalLookupResponse response; + TextFormat::ParseFromString(R"pb(kv_pairs { + key: "key1" + value { value: "value1" } + })pb", + &response); + ON_CALL(*mock_lookup, GetKeyValues(_, _)).WillByDefault(Return(response)); + auto get_values_hook = + GetValuesHook::Create(GetValuesHook::OutputType::kString); + get_values_hook->FinishInit(std::move(mock_lookup)); + UdfConfigBuilder config_builder; + absl::StatusOr> udf_client = UdfClient::Create( + std::move(config_builder.RegisterStringGetValuesHook(*get_values_hook) + .SetNumberOfWorkers(1) + .Config())); + EXPECT_TRUE(udf_client.ok()); + absl::Status code_obj_status = udf_client.value()->SetCodeObject(CodeConfig{ + .js = kDefaultUdfCodeSnippet, + .udf_handler_name = kDefaultUdfHandlerName, + .logical_commit_time = kDefaultLogicalCommitTime, + .version = kDefaultVersion, + }); + EXPECT_TRUE(code_obj_status.ok()); + ScopeMetricsContext metrics_context; + v2::GetValuesRequest req; + (*(req.mutable_metadata()->mutable_fields()))["is_pas"].set_string_value( + "true"); + UDFExecutionMetadata udf_metadata; + *udf_metadata.mutable_request_metadata() = *req.mutable_metadata(); + google::protobuf::RepeatedPtrField args; + args.Add([] { + UDFArgument arg; + TextFormat::ParseFromString(R"( + data { + list_value { + values { + string_value: "key1" + } + } + })", + &arg); + return arg; + }()); + absl::StatusOr result = udf_client.value()->ExecuteCode( + RequestContext(metrics_context), std::move(udf_metadata), args); + EXPECT_TRUE(result.ok()); + EXPECT_EQ(*result, R"({"key1":{"value":"value1"}})"); + absl::Status stop = udf_client.value()->Stop(); + EXPECT_TRUE(stop.ok()); +} + } // namespace } // namespace kv_server diff --git a/components/udf/udf_config_builder.cc b/components/udf/udf_config_builder.cc index 42178634..e6bc3fbe 100644 --- a/components/udf/udf_config_builder.cc +++ b/components/udf/udf_config_builder.cc @@ -24,9 +24,9 @@ #include "components/udf/hooks/get_values_hook.h" #include "components/udf/hooks/logging_hook.h" #include "components/udf/hooks/run_query_hook.h" -#include "roma/config/src/config.h" -#include "roma/config/src/function_binding_object_v2.h" -#include "roma/interface/roma.h" +#include "src/roma/config/config.h" +#include "src/roma/config/function_binding_object_v2.h" +#include "src/roma/interface/roma.h" namespace kv_server { namespace { @@ -38,15 +38,17 @@ using google::scp::roma::FunctionBindingPayload; constexpr char kStringGetValuesHookJsName[] = "getValues"; constexpr char kBinaryGetValuesHookJsName[] = "getValuesBinary"; constexpr char kRunQueryHookJsName[] = "runQuery"; -constexpr char kLoggingHookJsName[] = "logMessage"; -std::unique_ptr> GetValuesFunctionObject( - GetValuesHook& get_values_hook, std::string handler_name) { +std::unique_ptr> +GetValuesFunctionObject(GetValuesHook& get_values_hook, + std::string handler_name) { auto get_values_function_object = - std::make_unique>(); + std::make_unique>(); get_values_function_object->function_name = std::move(handler_name); get_values_function_object->function = - [&get_values_hook](FunctionBindingPayload<>& in) { get_values_hook(in); }; + [&get_values_hook](FunctionBindingPayload& in) { + get_values_hook(in); + }; return get_values_function_object; } @@ -69,19 +71,18 @@ UdfConfigBuilder& UdfConfigBuilder::RegisterBinaryGetValuesHook( UdfConfigBuilder& UdfConfigBuilder::RegisterRunQueryHook( RunQueryHook& run_query_hook) { auto run_query_function_object = - std::make_unique>(); + std::make_unique>(); run_query_function_object->function_name = kRunQueryHookJsName; run_query_function_object->function = - [&run_query_hook](FunctionBindingPayload<>& in) { run_query_hook(in); }; + [&run_query_hook](FunctionBindingPayload& in) { + run_query_hook(in); + }; config_.RegisterFunctionBinding(std::move(run_query_function_object)); return *this; } -UdfConfigBuilder& UdfConfigBuilder::RegisterLoggingHook() { - auto logging_function_object = std::make_unique>(); - logging_function_object->function_name = kLoggingHookJsName; - logging_function_object->function = LogMessage; - config_.RegisterFunctionBinding(std::move(logging_function_object)); +UdfConfigBuilder& UdfConfigBuilder::RegisterLoggingFunction() { + config_.SetLoggingFunction(LoggingFunction); return *this; } @@ -91,6 +92,8 @@ UdfConfigBuilder& UdfConfigBuilder::SetNumberOfWorkers( return *this; } -google::scp::roma::Config<>& UdfConfigBuilder::Config() { return config_; } +google::scp::roma::Config& UdfConfigBuilder::Config() { + return config_; +} } // namespace kv_server diff --git a/components/udf/udf_config_builder.h b/components/udf/udf_config_builder.h index 55f4ed69..9ad033fb 100644 --- a/components/udf/udf_config_builder.h +++ b/components/udf/udf_config_builder.h @@ -17,7 +17,7 @@ #include "components/udf/hooks/get_values_hook.h" #include "components/udf/hooks/run_query_hook.h" -#include "roma/config/src/config.h" +#include "src/roma/config/config.h" namespace kv_server { @@ -29,13 +29,13 @@ class UdfConfigBuilder { UdfConfigBuilder& RegisterRunQueryHook(RunQueryHook& run_query_hook); - UdfConfigBuilder& RegisterLoggingHook(); + UdfConfigBuilder& RegisterLoggingFunction(); UdfConfigBuilder& SetNumberOfWorkers(int number_of_workers); - google::scp::roma::Config<>& Config(); + google::scp::roma::Config& Config(); private: - google::scp::roma::Config<> config_; + google::scp::roma::Config config_; }; } // namespace kv_server diff --git a/components/util/BUILD.bazel b/components/util/BUILD.bazel index d098a0ea..a017a0dd 100644 --- a/components/util/BUILD.bazel +++ b/components/util/BUILD.bazel @@ -47,7 +47,7 @@ selects.config_setting_group( name = "local_otel_otlp", match_all = [ "//:local_instance", - "@google_privacysandbox_servers_common//src/cpp/telemetry:local_otel_export_otlp", + "@google_privacysandbox_servers_common//src/telemetry:local_otel_export_otlp", ], ) @@ -55,7 +55,7 @@ selects.config_setting_group( name = "local_otel_ostream", match_all = [ "//:local_instance", - "@google_privacysandbox_servers_common//src/cpp/telemetry:local_otel_export_ostream", + "@google_privacysandbox_servers_common//src/telemetry:local_otel_export_ostream", ], ) @@ -102,8 +102,8 @@ cc_library( local_defines = local_defines, visibility = ["//visibility:private"], deps = [ - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", ], ) @@ -136,15 +136,15 @@ cc_library( "//:aws_platform": [ "//components/errors:aws_error_util", "@aws_sdk_cpp//:core", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface:cpio", + "@google_privacysandbox_servers_common//src/public/cpio/interface:cpio", ], "//:gcp_platform": [ - "@google_privacysandbox_servers_common//scp/cc/public/core/interface:errors", - "@google_privacysandbox_servers_common//scp/cc/public/cpio/interface:cpio", + "@google_privacysandbox_servers_common//src/public/core/interface:errors", + "@google_privacysandbox_servers_common//src/public/cpio/interface:cpio", ], "//conditions:default": [], }) + [ - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", ], @@ -171,7 +171,7 @@ cc_library( "sleepfor.h", ], deps = [ - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/time", ], @@ -198,3 +198,16 @@ cc_library( "@com_google_googletest//:gtest", ], ) + +cc_library( + name = "request_context", + srcs = [ + "request_context.cc", + ], + hdrs = [ + "request_context.h", + ], + deps = [ + "//components/telemetry:server_definition", + ], +) diff --git a/components/util/build_info.cc b/components/util/build_info.cc index d78a2e1b..202377dc 100644 --- a/components/util/build_info.cc +++ b/components/util/build_info.cc @@ -18,7 +18,7 @@ #include -#include "glog/logging.h" +#include "absl/log/log.h" namespace kv_server { diff --git a/components/util/platform_initializer_aws.cc b/components/util/platform_initializer_aws.cc index a9519f63..1f2fc025 100644 --- a/components/util/platform_initializer_aws.cc +++ b/components/util/platform_initializer_aws.cc @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "absl/log/log.h" #include "aws/core/Aws.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" -#include "public/cpio/interface/cpio.h" +#include "src/public/cpio/interface/cpio.h" namespace kv_server { using google::scp::cpio::Cpio; diff --git a/components/util/platform_initializer_gcp.cc b/components/util/platform_initializer_gcp.cc index 0bb071ae..e15be624 100644 --- a/components/util/platform_initializer_gcp.cc +++ b/components/util/platform_initializer_gcp.cc @@ -14,11 +14,12 @@ #include "absl/flags/declare.h" #include "absl/flags/flag.h" +#include "absl/log/check.h" +#include "absl/log/log.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" -#include "public/cpio/interface/cpio.h" -#include "scp/cc/public/core/interface/errors.h" -#include "scp/cc/public/core/interface/execution_result.h" +#include "src/public/core/interface/errors.h" +#include "src/public/core/interface/execution_result.h" +#include "src/public/cpio/interface/cpio.h" // This flag is added to allow for a local instance to use GCP as the cloud // platform. Ideally, this would be fetched from the parameter_client, but the diff --git a/components/util/request_context.cc b/components/util/request_context.cc new file mode 100644 index 00000000..aec75902 --- /dev/null +++ b/components/util/request_context.cc @@ -0,0 +1,31 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "components/util/request_context.h" + +#include + +#include "components/telemetry/server_definition.h" + +namespace kv_server { + +UdfRequestMetricsContext& RequestContext::GetUdfRequestMetricsContext() const { + return udf_request_metrics_context_; +} +InternalLookupMetricsContext& RequestContext::GetInternalLookupMetricsContext() + const { + return internal_lookup_metrics_context_; +} + +} // namespace kv_server diff --git a/components/util/request_context.h b/components/util/request_context.h new file mode 100644 index 00000000..b58036c2 --- /dev/null +++ b/components/util/request_context.h @@ -0,0 +1,52 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMPONENTS_UTIL_REQUEST_CONTEXT_H_ +#define COMPONENTS_UTIL_REQUEST_CONTEXT_H_ + +#include +#include +#include + +#include "components/telemetry/server_definition.h" + +namespace kv_server { + +// RequestContext holds the reference of udf request metrics context and +// internal lookup request context that ties to a single +// request. The request_id can be either passed from upper stream or assigned +// from uuid generated when RequestContext is constructed. + +class RequestContext { + public: + explicit RequestContext(const ScopeMetricsContext& metrics_context) + : udf_request_metrics_context_( + metrics_context.GetUdfRequestMetricsContext()), + internal_lookup_metrics_context_( + metrics_context.GetInternalLookupMetricsContext()) {} + UdfRequestMetricsContext& GetUdfRequestMetricsContext() const; + InternalLookupMetricsContext& GetInternalLookupMetricsContext() const; + + ~RequestContext() = default; + + private: + UdfRequestMetricsContext& udf_request_metrics_context_; + InternalLookupMetricsContext& internal_lookup_metrics_context_; +}; + +} // namespace kv_server + +#endif // COMPONENTS_UTIL_REQUEST_CONTEXT_H_ diff --git a/components/util/sleepfor.cc b/components/util/sleepfor.cc index bdf84ebf..2687c32c 100644 --- a/components/util/sleepfor.cc +++ b/components/util/sleepfor.cc @@ -14,7 +14,7 @@ #include "components/util/sleepfor.h" -#include "glog/logging.h" +#include "absl/log/log.h" namespace kv_server { diff --git a/docs/AWS_Terraform_vars.md b/docs/AWS_Terraform_vars.md index 144c0e83..cdb80321 100644 --- a/docs/AWS_Terraform_vars.md +++ b/docs/AWS_Terraform_vars.md @@ -1,5 +1,9 @@ # AWS Key Value Server Terraform vars documentation +- **add_missing_keys_v1** + + Add missing keys v1. + - **autoscaling_desired_capacity** Number of Amazon EC2 instances that should be running in the autoscaling group @@ -24,6 +28,10 @@ If you want to import an existing public certificate into ACM, follow these steps to [import the certificate](https://docs.aws.amazon.com/acm/latest/userguide/import-certificate.html). +- **data_loading_blob_prefix_allowlist** + + A comma separated list of prefixes (i.e., directories) where data is loaded from. + - **data_loading_file_format** Data file format for blob storage and realtime updates. See /public/constants.h for possible @@ -107,6 +115,14 @@ Primary coordinator account identity. +- **primary_coordinator_private_key_endpoint** + + Primary coordinator private key endpoint. + +- **primary_coordinator_region** + + Primary coordinator region. + - **prometheus_service_region** Specifies which region to find Prometheus service and use. Not all regions have Prometheus @@ -122,6 +138,10 @@ from that region is created before this terraform file is applied. That can be done by running the Key Value service terraform file in that region. +- **public_key_endpoint** + + Public key endpoint. Can only be overriden in non-prod mode. + - **realtime_updater_num_threads** The number of threads to process real time updates. @@ -149,8 +169,7 @@ - **run_server_outside_tee** - Whether to run the server outside the TEE. Not suitable for production. This runs the server in - a Docker container and can be useful for debugging. + Whether to run the server outside the TEE. - **s3_delta_file_bucket_name** @@ -170,6 +189,14 @@ Secondary coordinator account identity. +- **secondary_coordinator_private_key_endpoint** + + Secondary coordinator private key endpoint. + +- **secondary_coordinator_region** + + Secondary coordinator region. + - **server_port** Set the port of the EC2 parent instance (that hosts the Nitro Enclave instance). @@ -191,6 +218,17 @@ Source ips allowed to send ssh traffic to the ssh instance. +- **telemetry_config** + + Telemetry configuration to control whether metrics are raw or noised. Options are: mode: + PROD(noised metrics), mode: EXPERIMENT(raw metrics), mode: COMPARE(both raw and noised metrics), + mode: OFF(no metrics) + +- **udf_min_log_level** + + Minimum log level for UDFs. Info = 0, Warn = 1, Error = 2. The UDF will only attempt to log for + min_log_level and above. Default is 0 (info). + - **udf_num_workers** Total number of workers for UDF execution diff --git a/docs/GCP_Terraform_vars.md b/docs/GCP_Terraform_vars.md index 84f1b568..3ad47098 100644 --- a/docs/GCP_Terraform_vars.md +++ b/docs/GCP_Terraform_vars.md @@ -1,5 +1,9 @@ # GCP Key Value Server Terraform vars documentation +- **add_missing_keys_v1** + + Add missing keys v1. + - **backup_poll_frequency_secs** Backup poll frequency for delta file notifier in seconds. @@ -32,10 +36,19 @@ Directory to watch for files. +- **data_loading_blob_prefix_allowlist** + + A comma separated list of prefixes (i.e., directories) where data is loaded from. + - **data_loading_num_threads** Number of parallel threads for reading and loading data files. +- **enable_external_traffic** + + Whether to serve external traffic. If disabled, only internal traffic via service mesh will be + served. + - **environment** Assigned environment name to group related resources. Also servers as gcp image tag. @@ -43,7 +56,7 @@ - **envoy_port** External load balancer will send traffic to this port. Envoy will forward traffic to - kv_service_port. Must match envoy.yaml. + kv_service_port. Must match envoy.yaml. Ignored if `enable_external_traffic` is false. - **existing_service_mesh** @@ -102,6 +115,14 @@ Account identity for the primary coordinator. +- **primary_coordinator_private_key_endpoint** + + Primary coordinator private key endpoint. + +- **primary_coordinator_region** + + Primary coordinator region. + - **primary_key_service_cloud_function_url** Primary workload identity pool provider. @@ -114,6 +135,10 @@ GCP project id. +- **public_key_endpoint** + + Public key endpoint. Can only be overriden in non-prod mode. + - **realtime_updater_num_threads** Amount of realtime updates threads locally. @@ -122,6 +147,15 @@ Regions to deploy to. +- **regions_cidr_blocks** + + A set of CIDR ranges for all specified regions. The number of blocks here should correspond to + the number of regions. + +- **regions_use_existing_nat** + + Regions that use existing nat. No new nats will be created for regions specified here. + - **route_v1_to_v2** Whether to route V1 requests through V2. @@ -130,6 +164,14 @@ Account identity for the secondary coordinator. +- **secondary_coordinator_private_key_endpoint** + + Secondary coordinator private key endpoint. + +- **secondary_coordinator_region** + + Secondary coordinator region. + - **secondary_key_service_cloud_function_url** Secondary key service cloud function url. @@ -140,24 +182,35 @@ - **server_dns_zone** - Dns zone for Kv-serer. + Dns zone for Kv-serer. Ignored if `enable_external_traffic` is false. - **server_domain_ssl_certificate_id** - Ssl certificate id of the Kv-server domain. + Ssl certificate id of the Kv-server domain. Ignored if `enable_external_traffic` is false. - **server_url** - Kv-serer URL. Example: kv-server-environment.example.com + Kv-serer URL. Example: kv-server-environment.example.com. Ignored if `enable_external_traffic` + is false. - **service_account_email** Email of the service account that be used by all instances. +- **service_mesh_address** + + Service mesh address of the KV server. + - **tee_impersonate_service_accounts** Tee can impersonate these service accounts. Necessary for coordinators. +- **telemetry_config** + + Telemetry configuration to control whether metrics are raw or noised. Options are: mode: + PROD(noised metrics), mode: EXPERIMENT(raw metrics), mode: COMPARE(both raw and noised metrics), + mode: OFF(no metrics) + - **udf_num_workers** Number of workers for UDF execution. diff --git a/docs/ad_retrieval_overview.md b/docs/ad_retrieval_overview.md new file mode 100644 index 00000000..2cf44adf --- /dev/null +++ b/docs/ad_retrieval_overview.md @@ -0,0 +1,4 @@ +The content of this file has been moved to +[docs/protected_app_signals/ad_retrieval_overview.md](/docs/protected_app_signals/ad_retrieval_overview.md). + +You can also find other latest documentation related to Protected App Signals in that directory. diff --git a/docs/assets/ad_retrieval_filter_funnel.png b/docs/assets/ad_retrieval_filter_funnel.png new file mode 100644 index 0000000000000000000000000000000000000000..bf78273a95ee87d21a6401f8417ba150736088ba GIT binary patch literal 40841 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 literal 0 HcmV?d00001 diff --git a/docs/assets/ad_retrieval_udf.png b/docs/assets/ad_retrieval_udf.png new file mode 100644 index 0000000000000000000000000000000000000000..52618a874c8bada42e4392c25d027206eb6bfca9 GIT binary patch literal 272341 zcmeFZWmH{Dwg!p^2yVeO!QI`1yL*t}?(Xgo++_!McMtCF5ZocSleft^r*HSk?brSH zjdzokv6ob>s+#)MoO2~qPDT_S78@1>1O#4OOjrQ~1gaJU1k4i}3V6o>y5ZH24 zAt5<&At6FJJ8L6T3quePvCxF1H%f}?s6J=z2Sh~tf)HN=)*ucbVo==nrv#J)Q3&Ia zzy&(Xsms5I5fe+RPeLL?&VvnTROt#pP%p3z4agoYfK?l5fcO9|7`Tq(%Rv# zMq)Q@F*V=e^|K;(V9VGJ)JG6W=DVTYG%{~cgsN67`&*D1Ug3%sZb|0BB;@?iSO{b< z_CfJ!Zq7j%#Rw7E#eiTp!cs7h%x@bZaUg^TL~bIiY%u3S7s3mI1E|o*B5S^qZf;PB zdhcoJ1ucZ3Wgm=5pL%7d);&lEJnY!T88g?0Yc`O04-gH`MikqdTC3`G5GW*2$O#|H35e!S=wzb%MWPOPE3rN= zLd*bv%t{Cw0YCHuv*Pk&{A}WSc4mT6518~Z-$t%oA z&WJK&NPlFSFft*lrQ-w}{YWQNA!03}ntKb-oOn0EHVM8jf29hC1&CtkW!GOwHjzRx z;>ZV43@Ylu?brIY(g&xvW>BTZIfrB(ecjuqt5``@nO4v?*mf z)~Jj7CMl4rQ~5i>=GgZK8=*!-7mikpHQ2Mr=-#*IN9Trj;CIt^yiZI|5CXp0vNrD_ zP+bDF1R3-Qax!fE>4lRCDN*1<$G+Lv0oOH6&tp}4X5t(f)T=W5W!NLbAz>n&JgMfJXC#Ev0Bd00~mwP+g1 zCrD*!ti`2YEdaPh=>V-f4+(V%(MkAy3pebxWUm0P;1|=VKHNFnmvm|S)ZMF5EY*~f z&HQdv3MEsOrm_q1gq*{?)Z%98I_2wFY>iQ2%bX>d7xvpQpWqmHS=8Jg_311pb* zmCd1Ug%7aTZHL;22H2_Cyx7E%u#uP8bSwv{R;d)Jd8q{~qP2w@v>IS#3S~?h<5)v* z7$lMMV~w4dvnum9vxz2Wlc#f;6AUw<6Y3SyrI%t!g~Kz_Gc>a&6L_=JQ*UQ9i{=ZR zKV@Z)&&bZL7p)iZS?QV}8CFha&+0D7^~P8x8!wpLO@vr%o7IjK&g~~M%q5$qo3#!% z_Cv5%v52!4F*fPJrY)twN;#+8jP;DzCzGYCa#gHHeC}f$W;JKVp)W{#lU%`O&En1S z$=d7q$?}qG&%3U&PON6Z_`FkLaBAGH&9G$$hYFRdTsdKu#=N?&;Trph^=O_opVcF6 zmDSm@sNT08$CAsUV_|VQ^d$9I!jr5F@@O(+MemM8oaM-RWGtfJUBYaDM~#` z6!)hMqrHS9=Xd7TP8(3~u69WOG0rjEJ)SXzcR`Y^!_l+~s+Ouz=4D5yY}#y@nifsJTxNQ zA_O=3B7!pXi^zDi4!S3e2#pBRG)8!MWJFmwp5}I)Nq=q*`*NdSBUd>5yFKdmYGF(9 zr7wz-6p|d0(R~J+fK9}2-n$U?W*i1E#{q`{^5t)w$!;OXBUOiRHn=V&@5WIwP$f}< z~m$s*skkS2ZTKy!hngsf2XP&8BMQ}kCnQtZh4n8#AEHN7=03!M;9)cqDK6^jF_ zjaX6MSFCx&b}`Bjpzm#~O&Z_CUPBWe#4M_fLNe(5#n&R~xcC@xm?Vi{RDJ|`*I_4h z+iJ)&aWCU`8ukkIG%PA%iE6$KSFKm0&X}c^Q_ryd*f=dq@A048zCWHy&!thadeUs8>(Eq_Q&K&& zrnKB2A|~?1|NLPoxtVyX-?F)?S*01Md1hh1THhgD^}gb>jr;eTLyn9*1&#A2#--F`Buv^HVS_R4NVwu)zt z!j0fsuphXT(~MJ}&NOnfpVYHav%9+=Kj&NL*!2FoMSf{A&G2k6+h5fGRx>+sC2?|Y zTg6mUO7je+0~PN?=A_@7;b-2<*PFp&#i4>QSyb7*tQRNJvylb$$;G6-FJtgd)F&=B zybn)hZ{N~6e7fpzx&LyTcbQQ-#*~ynm({xIvhz)OcXrO)XQ$@j0mkE8-LgP~`ks*0)V zKbu~cjdsSZz6+i&SbDj#~=&Ki2Qf3AYkR-a`K_IwISXTxFAGm z!ed$7X|oFE*tEx zr%<)o;D4QidA>FjP!tju2mUG=*clpH*_&8949pMd06{aasgjz5nzR(Bfwd*A-e+rl zLs}P0o7XNN+%BBJQ%ge!Jwg{t3oCn07arnYEjWSauea%l34b+lFy|pwla?bCvbHlM zWTmC2r6=ZvB_t%|w)vgA20dqJ;H|e26m=44yM*tgs<<_)3@h(Wt?)RE`}EB!lsr$ zssg9R%gV&Y{cD)tp8U^5|LCb~Z)hiEZ3*n?!23UJ`LpvsKm2>gUqh<>&yWmk46Oe= z=0839r>?JS;FPs91rDe8N<>};Zo1#@{n?(I?v=v-MDah^`Rgu_pS-Z#bpMbIURXE2 z`ZEv^eh_hC0VNmEKD=9a?>E{4JD z?6@yDxCqM|4G8W%XDS=e!ECq)!A>W2UlYV)i9afQ6-Iu~j{*|<$h+5W{BU}6d-<8C zqx|&X^!_^AeWIVO%^w zvzb&kaIV>lTPFR#XboJK*Bhx$i2r!Zv4B$5&RuWWZvMxnzy_rg7hL}Zzep>~VbpT! z$_b)TjsF{#1tzZl7nGv+b(HReEpvQ!NoW7hT%ZT8>j~jM)71y2)ErF3r0l4F>s!iy zrF5peH&`(*cLm6)Cvw7kJUrOuv7^Iq zYm7OxFv}@07Sdn@p@LNL${w8<^cCGKycQ^K<0IejyPUm$ihdb^EV?X$N$w2(z~7#k znMv#E=~SazVEe;N&mw|J!j1iX`AyW(iFstI7U$hu0_nse zT&v??@b99Q7R)cpN@~BpT1GUTGD}{jWVAuGoC{~qADd8}i50n>npqxJ{_^6BIXkOO z?F6*Mm9x-V^EtB9H1HmHR?W;-wO0?tf#3mltiX4tg@gFr#xx(*Ge+-a>Tp;nd11CC zpr?<1>~uB&E0HBc@7m7ej3l&WHZ;M=+osbVf5P}cvvV9ayaZ=+ebz&i`kNw_qJmAT z2&dFlvl3cPEs9{xov+q@79Crs;d+ z5i?T;Nl<*5cJ!t$)?hekAg-SJ`ni|zA=aP`U#-C;N@OZBc5x=dzw^+xP@)n(8Gxx0 zO#q;TtS3YIWH>Z0Whk3-gVQgR;~S)-*`L0lAze(jnXZ(7lf3m^&il<2cQzCAZ&eJL zG)vEIZ!4wD)cUt+kd+Mi?J(`|4EU(=Nfwu6>d;6|;`R@ev7fSHv;cj?f*C}4iJ&=0 zqsIbLWuAKB4+-}(pcJvvf?l|keXEUMk~YlG*tHp%bP)q+E#NA=Of`Kjhm&EGJhKJvu}z)Z^?x(Il~pjGH5qPKSEXyQRoGD)Wrywq8mtJ#Lv*v=CGmx5>AsfmfICK0e@{&z{MY=X`<(@1GH z6~{u-)ySZUQylX7@ha51V3Np<<*;Q>^poh^e7?}j3rhr4D|-6l=fng}c~%2Az!pbY z{2|@%wvWbcbHu&8aM-1Do*p`w3ML?UHNdt3=_-{OU#N|jT`4YcAA2u8FaJdEmzUBk za)f-R`FBUqnNPwGj&y%KWL>&CH*#ZA{}F~}2@q+)E+p&hdfN8N*IF2@28uUa1SA2L z5}*X7J5sRQKl(KYU*#Jo3*?%I=JwxQXj(YGn2m$v@UY#xHTwadnH#BfGY6;b zu0{O3)l`xuL6zFdXVVhB)ewc@z>PG0gAts_;iPTz+GU#YKzXVE`)u=x4U2nLe!|L8_(n0cV`7R8oubYcY}p-Nn6Y_KWCOVo}R} zZ!GBcOnrx7*_8 zwHjp^vwWQ)BIt$luJcV@HDkkfW1TJc*naTm=e&k{D+_#-c2ra~Pq1mG8bdR)<_R0! zB`EmzLuLpwe*s%oc$hk5PN))d#`)v(RQ(#V-%!b`e=2!iu)J~!bt*LKLA;L|lUJd! z2xrvv8t_T>lFNjGdJkT$)u=h*WDKSm)#;dFN6#xV`i3HK2^AI9(8HrOA~G@o=7fxt zj0=$nO*8$TWKM4r9CcwIVqm^W1!L%>%h#8Yn^7SsC`kV;QR_;jY+zysD9&>c*YkWN z$WGpcjD@tn&_fwxQFVeH>(J6*=u($3&D#ru(NCP}Rd!*}sbb~J@SMcI&-_S|5GTCp zjL|7I`~pRB<*2cup#lybF$vjHs7FXa3x1%SRova7te!5k=Vas$4=eskW^_R&w{U-T zFlOd6@)}yLM!B&pzE5MBJesuH%}TVN+E!gIy5cB|7fd+`;MwXEc@v8Gck$7kMwaWL%g1Z2#{ry( zx1g$zN8xdCGG?DFHzaj+b;bO=x;AAckY-TW>0`-sWh5B%H!~hFm-P1p!b1yuFan93 z-Z!DqlFPLTa#}#A{-wUHc=>>6%#l}xuI);}^}P4paFs#7;6PBPr5g5w<#Ii`h-D?w z1nEDSco?cpmzExn=artM=j7DM z{CQn5jAPv@zvlh(+3uy=H8LcV+&zyQA0JD_hKJJJ9nfl++MR+0zjL_s6@}_n+r3uk zclN`J`mi2owbZ6JXJ2` z+fO4Czs)ZTx4>LPU?g1XrWQxeDUMu;1!d`XU6Woqeg%G8D0gzOWvwUm*^ygMK7|$HgfR#z_A=eexi3eU7*+{S@Az{j^tXh1UCZlw9f5w`H&WK8RVwd&f}A2} zU(^}cawsao@Ds^>%3n?Q|YgcYPy|2)+`hmQuP*NCVDX zw=2^YiscuX2C}^RtJLvI=~>*aobhUWKYQthxXxNpcW7_6dFB8(xL*V%_u?BYq|T7S zzN3Rm8mgSCZZAgt9n%3>2;m!FvEAMxOwy2p$H`q?CMC(yD@A0WVYNF`G;IdZq3n+) zh|VK3SwB_U+x35gn8?#o|MC0cnBjPk9o0^+Y0|~sse-|)zfB9|kLCrVdc>|fOOKYY z&m@RLieTwB3>bwjRZNRYd#B@)2uTVn)@(K!wQOG^hRQ| z5_HLjY@xfpkXHxtD@we&@e$TYp}?m;k?> z6+r&-j!1Q^3t_+~=9UmV7hZBX*_;S`stKW4Hdz~p7cES{2&@7yy0;)3#zg~ZBFaf> zcelFZ{jANFNYFH-mhhj-Yht}HM_abY*_rw#B#cTZG)A?068&TWe zo}59hgVe7+u+o3TUJu*L^5#9ZYP+6WaHgb+^zI(8mW!Y@ZD-RfFlQyL#DW%vY->VB zQ;&@%H%_}?y#RF;ScbWFaK9{b9xAD5!v^46w_V#b9hJ8Xfg|#g_s*?VYhNZHik;6+ zQ+=q+_ZX;7ylh&Tp{W}&XgZtATK#~s4ls(E(LIIs(W@Bf@ zXtX5&Rcp{cc5yLGm!zuWTF32tr)ub8Qn})EL}R#I`NRxMwbZ$I`e8TzLsft6@`&CB z0Z>AurN1wsX+Tc~k0gro0odiCwRs8G#NeAa@LN6eI?4wcGwfmjndY8 zUzuZ?H8Y`@FDU4nAW;fuuE+c5xI^cSb#xqQ zdNnuSepA<&$z$Ky%SI+zBqx+QIXB!Oj-yzy!)E$6=eo>_5o}m_Y>*3cMYtci?b5_~ z-Rm*qBGTFmpw-H)azjq%dCultvtmEav(txjn{ZxrkX(}PI`T$&3<3TYJz+{dGdglq zxS}zEq?XZqXXLo&?;CAaY~KWmn#0xEo6T>++Ta<;8Ux-z!JM}gnO0EwFSxH!f{x_#DW{bRb*(CEY9J14)aY!8Fqt#iFH~1WFq(3%bwBiKcb}UOq>hG5un{jz3qc8L*%_9gfODMab7-Y2=+;(fMRGuK+xOFbX7(+11#vX7rlb# zU7{G0vT6lCo6J^O9pfufFqe2Wy7s$Vw}Vy%eD~ujIk!um{aLmT=!fUe@2!QtHUyjO2=u)i<6o34g!{Y+HV$ZYM zrF1jFDW^P32~9^En?mk~eS=9E&yzY;pigO+$3&~L9=FoQfxc(@%E`E&|19aw-eAFG z&73P!0bF7oawBPAVmoFAqQv7?*P8%%E-5d$d{Qz4>2$>cnO^7!pk#5)T`V-&8yh#1 zK&;%`3dXWf@L8BTTQ(Sf0#58G5@h{NJN)+jlZxD6SJV?PBclQW9w*vk++p73cARV? zlkr&ma1!0Sb}Tp!S?}}6SUxE)YX{r5)#_;jFe=hcF8pbe3orXpq|G+#wOc2j*2irk zPQUc|rTG5nEb`mcpZraV(+{W19iK?5pz=D4VQJ!3B6PoQ4J;(@5~}iV_kv!3A^BN0 zAgoeyZ~gJ*c;W9dC8g_+EC+5AIND(o1t(@Ht&t{WKdy0TQOoxR>T&qHoHy|O+t$+& zJk}l0^>e4bkUCCYOJ5skdYiuzY{L=d#B{Q=>&AWqM9!W0WHy$tG!SMFYG1|m zm<1W6ymT)f3?s*y35jr~VXFzBUyar2Y73Xu^rz-MfJ$&S!Z@nip5vc8#CAmJ>C2X` z4BcSjG@5{$Y6IAAJdLwAJ-7b&oa8NcI$zgs8c3tL{JifaTcE%3kplTm{wZJ*e+T1? zXNTnMy4yL8+wpw!qa%J3?tO^spmqBWMxfCioPS6*l5>s9v;hZy&g*c_j`JoZRV^GC5_YxzS49HqRV52_ zsZ$~O>fq>r%2OjMXi?b79474K3r<#tO}iGmch1UJ z$NH(-<$)7}@@dk(&mOj$zbv%m_f&L^`&co(CH;VXd0wg4EG|gsR3{1m8WHVuwAZ?s&2%4nT;MaxSV!y=Em{0ZX zYb%+-nEj?&>7{Y=x609#PxXa-60>9^s_%%TS4BipY;P5C`5sFEwTENy!i0ZtkX8BLuPmWNiQ% zQvH!&Av(NrgF_>Iq}wxTol_9i>4xiTF1H-UZ4wuV8KMJZ1_pJdcycM6g-wG1rqBz_ zw?D?lir=L>^pnRq4|r0>Cq;QyQIXfwmX%7^sPWhT9{5+QfS74-O$;Kyql>*pfQK&{ z4P9|oapz=AGO3V}WmuN!;QS=h%gDmzd`jug;>laRA&HGEpva_=5yrRNwn%F=>@#K6 zwrSrCL{chAoh($fcWMs2q-D%{1OzB2$b}n0dU@5<%QN5}E-sA#4$&Wp1cdQw(3Mzq zf!ffEUqQAOUh%g*()VS4`~pTH{AlYyE1GuH;_d9p+-m=g2|Gv2ZZeGg;cqLon>y5p=ts}mrrhJsq63`{E zR2vME_pojC?00f(T#=W_V%O;t>F?gqXCypU$S)1DY$W-GZ_5O}r=n>nBeAko=h7$* z8}O2N04{oZLZ~3jp{wk-33`f1#e zm3SYVma4|j-mJsNtJl4g{S>j1>*v4W9t(Br5p82_0G1X8vO{=QZxwcdikcy`S(q4p zQq3@=!2;2(4M--Ig-1Xr1BmIswWOR17C+Kw~Yvm{Xj7=&z5tk zZPHF59zegv^#x;zj@w%MA_vBT984YTLPk*}e}nrHw%<02Sn?_q~=N)bKGAD*twMGo_NpQ!I@~ zGb^e1Xi6ikh_1n7w%O+cRVVxShy^YGhI7IxBVk#KuLN%o-AR~Gi0ha5nj-;ifC}vR zg+bQoP`{JBlqhA(UU>-Pf zIzX?$RZ&o#HYA0;5`?6K;qj(0^Q(6A_?eKG{RMZ=k2MI^_n>WOty`vY>~-Up?3ZuP ze$HGeZ6_%i3`|GCY@lM2=d2hB*9MOAw+N#JJJ@_N zWonLd9(Go(>Z3c0`8?}V_MrtuldMDW=lYo4huS%}(>Q(e>i3~7N4urD%e_6v^4GwkX853JOv_JRueDNm+4;F^}svL8rJN!+7jHn zF#Ij60=)Fa@HG*gOZ+@s-_m1}G-awdR=@(;ZUi%Unl_0sd%Yy{G4un6Z9$%!;g88R zk^ znyZ6Rd1LmQpZOcGef7s0uZdWiF9`j=fw;frXn>cx($K(^C8-)-;omv=H^#nRcA6mr zlY4byoVdS9`EOIE_hlghW^Pk;9kc%hdH+7&Uw2AA0&|?R#0NJ2OtF7=J{JH?^qTw4 z`u>i^exLRK5&BG>J^Z!i8zgWTlKbVe*4$>S>xqolZeGz;!xu*p7 z)SPM-`lFp{MnRo63Qtgmcchmc;TzD`1S@!WRPzmT(sss{O%q!11K)|XP)*Cz(Zr=5 zc6R(JU6pV^XP4-^CHXJX_Rp>W7-h;HDoVwV=gE28$j;jkf0m=H6I`8{gL6y^f!xrU zU6xpF06YF#CvUXl`LUWwYjw+FPmQeT&Ma{2uQ>jG5YR2G%2oeLi2twkN{vupH8o6z zPDZRE^bBDzJ2KH;OKuraI^pJ{cll!>1qt7%0&H2QZSqnrwq!rILZ%lU&NVt-g`+1+4VJsu}Pq;c_5LBe|sc$A^s2fl()>^|L^>;lx`$+ z_TIt4L0L`%I@f+l;{v%i=VPUQHNUn6Rj3*C@JlX4xMLaPu?k2=VdLDl$6uz zxlQeRJCxyHU)725IpSbCIFk z1Z*_L=VuP=(!BB1cXGfFl@$gqLC=(v3Gmw_uu}+WX*)S zfhzA}j{my;oy5FozBA{a3DW^*rm}-c^Ajw#ZSrV&*UGmMwGwzJkAb%JVEc`dE#{$; z=KhA*d%0#&3p7$f`7~Qsz_`ILpr^tQ#8l{=)&DQcfdNGmjYu~;3KyL30n@nlUTa5n z+7DF82i5;N`|Z6ws7jtfX~8T^r6bb$TxAMMRYOI?)y_b9VoeFY2+~8*4Ce^=|pYz@MWhzG}sVhsW%{MWN*>Ae;nf92^=H@f_ri(DG!C6|u_c4WsB7 zC*hI=!530V<^!MOUskHc(02Qc8UM**2+C;ONoSsfItvG$~wZt=E0k&a_qIsf|(cl|^W_V1*p5?#LTE)dWq6tf>hVH$#`~w#ykgrk&^xmxR%R`wmzPmiC3;b1P|pW_8_m zyXlbolFGGiw>OoJ$+~&i;a2Tr>KC_lUBRRrMLp?n3H&##^WWlI7i+|+@|iW zCo8y>9x!KIH}jU>dET-?do4S?i$U5>VMb3Q=@3{be5K5WM}<&cxy8|2wy&U*9$tao zjS_rd96DV8TBPSIEe2FM={q^kzdT*#F2qcu(qpZZlvI%|ueykch{Kv9X@QYrd0$ig z#>Pfvn*IX{_VLogtjmeK?C27-@|N^TorQ&{f2Aq7GC9ukMH0EGaz2^9t)_%eC>6il|%F zY0!_hz-)hX(&S_nquae*lf%72lgEKw6SrGa#GsYFzToxNpxwq(pxx1b!FOqF&^kNW#b|d?M({8^MiUhMOgawE_h0HamjrT-@uAD6UYkQ>1O2P@LOb zH|JgyIur%nDAr54GI=r{=b3|}D=kG2Nm^ITr$@``JitJq=WJ7-YwK;~F4d|{j_66@ zjVrs(57g@hNGD#FSB)=wb+TnY;yt7P!n#8c`{L|HP|x3Wy|J*dqWT`+lhn+}<-Pd; zFOh1|2mqBZsOA%K5J%3}DaK5VWrj5;iJ4SmRxt$3;YhP%hgP+NM9Q+bncglXcd8H} zx%zYaRhvRWfnxi$y?6!i&ys|O)4NacPQA1@EDV$R9b>SnQRh0kT@V#T2X;fo`1ty4&aD6 z>aPj+T-b*WO48zHRA+>im=hp(Uj#jY`i)b5$w2zq&M1-Mg?UK=l8B_3mTlQJl9|(e zSBk^&u(n=tXC!4BTi#K}Tupn*WcOUoDkuIRWs;mRkgd?p$t2_&P#@fFx;K_#IA5+^ zb+Xz*cAVVhgDm3kej_>3p_kbszN46FukZN-jup87GN+yKvx=YI7e$IWr5-n#(2uKt zwhb>4$27?#mRw6I3E^4UL+FM{yYtQ7?vM)my|KhgeQzDW6767=yfx|;HWpa}`M1r* z3*LQgOnmGp<4X72(`x7SwQBG6mul;uFWpSvQ-B7d&0?Af8-EcajN)Mhrn%dvB5JEm z*R#gNQ+R0$J-!`UFj@pJ{F5ojOkV!iG0Flz5vAidJQn1I@2=Pl73CKNxZVu zF8@gWWwOBiI~G+I`O5w~HNZJAEBtB&7+)iCP*HUMC~u&aA;j5YeyR2qAGVOY+@Izb z%8t^_*dk(KUX+z8R3_`G^=IBSnXM`*rg@e)?8JRYR?ml%$BcM_b-tR?&e7!q1J)f{ z$br@IaW#=JiqAVEE4~?GaiuDFgd>W)K%Ez)&$nPDHN+6)4loCwVvW00!BCtRul{3) z3XEaneL@Dw;d!Z|!S`;=%?xxTf zH6x#X%RoUwyb9UzV}|Gn!KM%e0U{x z>3g+dF*Lk3!*MH$0w+ew;>3uBSSU^(xP2WtBX|=M9&%+l4Tf1KtMr=5u5g=k$@^hf zN`uWzXvKMdaO%`Z_mzPlH!rK4P85S1^7=~AXz_y7K?;%j^O5|o^yzRPCX?fXx!LM% zBNwofh{KITanLm4;_CbnNSgH>(lfb&*g_{|P~FPUI&Q68_f8r(!KhmMBQv zWR~l#skoev;R4?Q*4YWLR6ND3ax=f$*wN^U#2h$=M>$`o6j=fgJh>mGgxdRCX;HV# za3-NPn~vIwL~cuqAg_J4un_mPJL!wPq1SceOLZO)X?(6 z-wNw>1`sDcP4d5QHKFedG(_2vB&|$=-fydD%7vY7ZmXl@j$@Qm+=xL$eu;@j>L%pZ zE8`OUmbuouEq;xD#_IA~UH$5He9NKb#wKk2VxM|Edno;L-3qEo#bJMFWuG(WHKqq zWHPDAq6ngm$H=)vlqQ>u#fGRwW=_^-u}AByKP>_#wH9nG)y}BQlP1dr7DMzn#W}a- z`}H&EMr{h61pv)P53_xGpOuxrZzMWsE4e|<=0qPL6?x~l?BE#>3Fp&UT&WMe<&l5a zgJ{d-mF}aoW6X;y^#I22^tAyzQUYZU8D13b z?E41zo$3^@N~Al?GoA_(40cAr0S|SZ3aa7c9l-R?SL_mi9Xa}z7QjYMih<@k7DX|; zQCPEN5;MoBM4dSJa9QzQ^|$IVB{Rc`SHV!Vh2)VJn!1fgMW% zU!%&y9?Z$vnR6x3mEuv!3sNadZdQmIgkiS`;}u-TGVR`d5Kp~Q%owK-wn@X8w}Vi9 z(6n}+(%`BJEoR>q;k;~Y>&FkoFh3syCg2ldI)gva4l%QDv4^m#Rbk@tcc~|Q>@o8{ zp1RCy6Trr7`)G`}gqw7Y|IOixS=x-ewv@8kyt=_Cb3WU4hU;~L>Smi>8MBCX&i8HY zg>ud*D+-p|{nbUXym{UeXYq566635e+PQPV$v}mK8O29z`BW1eTW|92{8|K@d7J5q zND6vDYQFm>Z$n*vZUoM2y~-=2m0?B_@>Lzp-uy{yAiZNCW?;8I!4Ig@9Bchd-n3BY zho&;?iQ1xIp*=_$*hg}knezE;7HLvC+%AKK>ihA#eWVVWI;=n$4|aP^)%aZ-oA0Q! zPb+GQMPNfHV>rA39*^$<-bnW$^obw{z?{5|U3A~?Seh|=AbQAxDq?i(&rPkaEf}2D z!SoXUNNs*D*$~cQ6!iH~{t_Un2=PV^GqCN@oipEyBQNs849HqqGwlzD(Md|txNY2& zfwP72=&77B_7v12jw7{xjmGg9v;6`Iwhk-0-)5!R@Tl2VY=jm&yt72QkM|?e1+aWK z;dYeqVil*w?Q%0q@C?l|>?+Fv-z*lUu_3jMvE+oqFiU+^KA|_pvt!CEi&Ky+TZKYjLNF^tUGOKY}E?uivZjB4+)=MLT@thnKHw?_=rUnNFPyDY>*6I*W@L13~MEddA;Bi+!Lq;ThF0y|!2 zsTd+RK4`C(Olq(Za4JRNaws5}dIMKloLSd$JdtWn-b#A?5+P$!o3zC1u#2;a7rl+z zUyG1-D;~{t5vY(|2p)j0erB|CfL4Y z#$X=j>X8e4)3k0wzg(<|0-f%xRI~5MCy6lCsU5GUAdwI($`dgRAj%Qa))dfVKq=Me zX8{^(BQiNdqZF>@4$b&3v5#`p)6?%Z@Y-Sz5=P=BTbxcJ9I;Oux5gNA50VjjmnX-s-a?gtIgBQ+#5BAP%18QqH8O}w_EonL2~d?B zh4gX8GT0j4u)C4dh@NKypP~X`)kAE$>m>GzKKop=J}eujJ-uMlhC< zyFGP`v*wqaj!s;P9zSHb(%dTTr#R*msAd<{Ktv6Fb;P_z8MEawPBl2$#JW1-Fw2MT z4noC|7@!C;MwYc`Oz@M;a7wOj!J=O1iS(~T?9A*+ZoxBRU#BTg6toMc)2>Y@lS7l!UiM^vwsJqN)a=yF z>rgeUc-UWk{z{E)#VQhrq@rw6qS3 zC?f{{bmnmY7!5()iDX-s8^1x+mk#kybvh_-IScb}?16vt-lXVgHP)L(LLJnE>rd>M z3F7D8R5&d)FVC!mQFO^5K@bQ*l7k89zj^5Q83(8pRO7sl7Qeo;cr8>0)Z5%nfFs7g zA#f{P@&4Jb9Nj7o8<0azK|!S6fWCtc&=VB@kcYgpUYnoVBNu&KP%chJ>B?W#Czf(B zn8Jydu_B_aXhqJY?*66uIJehYQEnH_8Cl>G6_)8V1xz`gQgN4n3DTKMS50^6kz2=v z0AYRu@m4({PeD(2QV@)V&L(O@lh2Pg!pU*4R7RPY z-u!!Ea<@TWjyz|JbX;EUFH+~$-8KT>ZN4;^lZI(pwUw$`D{w`#(0r_Rnw20RV$Hz1<&_s z@o+6o=W;D?J%7O2$#7ny?PtmRzQ4aCCu})CQ1Zz4a(=Pc&j*lKrn6_eRte6VJA-%? zWjI@v$h8lg^T?H~@mdy`BC0vMZTH`$4IABg7POj&NsVds*H@WoAhk^;*Dxc<1W#cQnb|j0h>YD3I*_qw{ zi0&=8K^)flU9lP#V`B6hon+P+)0u>I#qIRI)1f|VeRh)iX}8{Xn@A^_gWhJslH_p$ z8h%k?Dho!3^Z6JMTclm{Dc7^%I_5E|0m9cw#U$}aZPJqoFr6%7M5K$2l^7%3PS$5U z1Gsy>$e$x{6_?lreh5?1TCjHZ$U-yb1Ow-M{Qod@7F=y^UDpOmTcE`q3ZYQk-GjAI z+})wLyAx<}cP;MjE@{!??(R--C%ox7=NaEO?)-pckKB8&wdOVFo-fE8l^Of??`~*- z0LX+x@lP>Q2*jd$B|fAxsp4NG7nO*eCKuCCz3TZ`v9IHBKiRv#&XFIh&RueOLV^Wd zbG@@)D7p@FJthd{BhM(I;>=*m$WREAtirfaz5j*dr21MFS6GH|PKBJmNl9A$sh5n5 z1Vc@m5ZfZknV#Y1f;a~wX}RzV4S`nTl!CAGW)8HK<+S%?@?BoyZf_!f2q?B%D_Q8G z8!byssyh79V$zoqwQ|l&&hGkPzloH?E90{PX8_UZPvS+=>(t-KMG;V|OdlEgg@Dz= z`uM{#xsY>m&eN+8MpDA7lqCIMMLE@sP@H)UJ*7JSm}rEJ|KMJk_i&3D%9!RzwF6(; zdhA?t52nto(%>^kCpqnnS(bJ=u|AyT7U~JPpBW4%aj?}g&XQ1|M$B%mbaE*}HW!~3 zlmC|bFmO)I+-BLV$$an@vZn4TrKmb5pW+nP;X+92qxPC^52sZKT9jpO6W?U4@vIJh zD&R#2vi*0qz&y&A4&U%AQU4U8>JS$@Mqm?T&!ZszA-?aF0BZi0 zwYn+9h{)EF`$&SqBq~s>*HzB#IH&8D1o7VE-+3!^=X-xYCI5waL*D0*-J}SQvnnnR z_p^D?e?3bz61=!cWMp(-NNH@WVWiGPNIAD2v%Ju?Gfk~rhJY#>DIY;tSsSFaj0N>H zQ&8(&IA?V|=`0*cwn~z$tfkbiLOqSSUZv1JXU9n0DpM5U*oi5ckL0wa%1kP31ClP( zZ)s|I%+&k{Bl~h|3By=)462T7UK~(`BL8;%ISx$;le8xbdCmI)k`HXvrqRU5mc^ee zn4VtC&7D}R>Yo~0cBonM-{$k1+&glQ-Zy$VhzL%`oCn^)z)wIZj2mj1jjvSp#J+ zJFWE(y>9RyH6N)kCi1vjEUQXtTJ^3IxR1{Y5Iy=sA~9G5ye@HkgcZ(}D{Z-qf!0xO zonS&_yjX4_r@Xpm$^XcLzsTIc>JMgb=;cm)G!RR0{c^S#XTl1Z67TYbjrlrW$gB0~ zxTGle%mlh7^vM0f6(n8P82Z2Z9@b&Je&0c6T%RcJIzF1El6SE`VO#_KO@S_?19W(K z-ihKrjqx*b<3Q02Xj#EzBMPw0n)sa89+$^ch?=Ec*gMkvY2#Uq9OK?7kXuI2a9i*@ z!N6FT{VM;TtEpFMT_tjR&T>=RG5Y$`59qfmj}EjMZsOF`v-ltN5JQkGF~0U*;uAnB}vpB;=h_<&lN>pSH?7&gJOGFAIFN6;;OyYuT?nQPFjX7~kHU zFt$x^cxTl9pgQPpJ%vVr~+{8EE8sL zL1(u+LND>4ngs5p{{&d!U^s-+(9lK10*Q~PQiVe)lip`AE1-g1&)guy_xWgNLIM!! zf`-BK{z^i+7F&F(vOIEb_giOp%bRcg@#r!J?=XbrYFL<^Vb*f7^~tsErV?7(yy+>8 zi`deLNtaQ;Wc?JkMe9${brDEQ&*wT(!`SKtl{@Ko4zQw#k~i$4x!&lbaE|iyFJXN{ zSr@)q`ai`&eJZl5ty4A^$NxsSz??sSE<$QUFZ^OT)6SR&i4H-U@9i^kDfmPbPOokf zKUO4r9?wr)4ANI6fFFjy3}*G6wenPeBvO7KI>uVn1HO1;QVZ44iK>zX zWR$0sy^EUEs^mlCf4kv20a)=NI7 zmj`NQdOjRnbU>ana9ZDT@>!L8Z~DI-`s*%g=F6Hx`>ZOXT?8z*VKgaNO#SLoz5a$R zHALT{?(Zw%U-7qHthniM0pr{;=(R`-eukbt3ehA>NEpY?W>FXQUAJ?`2k$w}g zibtr3VlE^hB9hE06A`_U{r&v~%ZD?bZ~2V|2@naNvBD^xGzJ-5OlThqx@tO*t?>|I z8!FFD|0GWs>p~Cyq~90E`Z$YzDd|mfh5a9uRx_CJne@hUca!UPXB8xbzptHD0iNb! zH3R73!#&?IQ+_`D40FZ*)6&R_kPYD#!VTLAnTQUXm+`C2FpD4FvSNYN51BIR3J)J4 z9m<)$4`w?B$@)xgYd-2NUtsN|UQ9zk5JzsHBT_HEauX}^hrxnk{nF*o3p4nR-s95v zz=CCjXdtJ5@j$s?Q0l<~TS=yfpm~nF;kD(>LBVRLO+Jg)YY3EHOFJc2OJ>6Ne6v7< z_*7i>-N^JSs)?sX!Ui&nVyE$hakeGYgjF(PALK0q5d2_PDZAaH< z)zVvMHfAYujn08!>&2UY--$WB_|K$H?UP@%e;>}8HkaqIoSa+&tB+UB8s*1(u8K0w zV#p_Dtj)|Dyc51gj+3qW?w!ON^~K?q+gN04IfS{3S>jnZ98dHuzWr? z6QXAZKZS`icQWwKC=kvrIwjJ>Q2ji;gB(wy$- z_ewI-JXS%kK+2GfaK-tTS!}Q-@Oq%1JmO~T{u18^>9!91pbC=_om=cTz9W{$AKJ+5 z7^n|VJg3To9!edc3*cwapjA}|sR%nN1#Y+WA7FleBXP~G6wX0 z-Q=3){SWJqo(P9vN2EhLZjd1^?yit@yws*Hh?$DrNmutBp0P%ab&AjWqm+r7wu1lA z>r8E+w>d(3?5~E-Yb1CI{1P+?L8b&v!}yPM%mco{2x8*KE7c(VD>PtPiT!998n;*3 z6AeabfFm9ip3)rVk=evOvKX^BQ&D>L)qkVnLJEv8@IrL>3L1)OJ?K(2xCk^21P#PV z2_&@u;&d8`8O*C>@KfMnfY)F*x}XYdu8zGEL~LuGpsMs;aaI9X@{91pKKk#M{KL48 zkz&SNNotC{RqxrMQG^xK8IYLb{II7U65lV8sJAJstH|JQ|9K!vcu zyETfiOm%%9Mx^n)!zli!V#ftK2A4pT!vW=@!8PB#wvAJutm+-%b*YwIPOrePG0WfT z30f)qr#x7=8sh99m%VS)!AxLZ=zWaeIDN_&14S-Ilds)YRbb2`A6av{vJR{`C=~L1 zDzI0+baC1SNdmphthYcpWJjCE<{WjV`)uM2|vSYu3;{X))AvZ-Nj*&EtER@=#`)Jqt-G8^cc;BO?sIa zZUKIZk9rk40EF&8>Jb&Csd(_nE~y<}Zpqk1qs7?c_X=O*XV|hvrG9*|eIJMyid+>y z&HaZ7)70|cJRg`=hg>Y97#JPf*>N}=Lm_6W@U>R~>ofx)*VQ}aXUgn;xzkR7sr0?D zR0Ty7s8HZ$pJFGwP=z?D*3z>SjaFnIu<6mI`dd^}EIq?`Ftr!8AWAy37nV`8RFHMu z*O9e30I7C#?tL0Qg=|xPyqha?`pa@0N zl`k0EBfspphb9n`#4+Wdp{(-Y*+Ei z5p!p9rZoB^T`WIc$Bg{(=3e0sZi}?;wxOdcupX0crw&w7Do^O{#~Qd)^ysb7lOaP& zYFHC2TOtpe=C(`WSZ6`tD~X+zL)ei%T!6MU&wdOCj&)%(d!=%s`hud ztnS@(0#w?JOTkka-e#v{qHk%4Kv`elLX(!BQ?Sk36JvxFr^MfXJ*(9`@Q&|zX%4Li z^Jw2202R z!-H5Plqzp>nnnLGk1cZ8E8zQV9nZpB(2=G$-Pue9J*gi~fFP7t6+ zbSy?rG&wc>+}|VDRnDxHT+0 zvHAdc{L{BmkxzRJQUXib(S*<8D`Zo>`{~i4;wi_XgK`wi)=NSJhnKvLePu5ri!KylV3@A-Na7`q zdV6yds8lxDy(2m`|7H8cDxYzT|E7iea|3EkQUH_rPLJ~Q68c$Skz2A*KrmWO3bv|U4h1xWK zGM?bC{}jpW%v7g0u7{HeeYfN4oE2>DkL~}AW?njK= z$u?TnmrJ0>41$8|JkVXg<6MTDWm_KhZr7_q>E=gR^ato!|2cW5vh6ftV}l_L$i=1S zT6P=pPaAA4iJ(}?l95WJHnCnxA>v3x=tqS`V;mM7#Tayid-58gGU^%I z)YZ}0SWyeDrnk6(8Y7NYvVQ&yaalVk_Srp7{e!Lv5;F`GeMjk##b-I^0>S+n*@s(m zboQA9W*7MN@Ck+qK6o?D{VZ)-^JuDeu_B!nO#q!eg7NdxTCXn+S8@_Ow>N@ey;VCb zQbF@7zwq)AU#XaNnJ@6u36BOiJZCYY-t#Ipc*y_xkstOl!(W2$hQLu4Z-P0|S?9n| ze*JeQYW<2%elOYu(+_dt-#X5B(^;?AmE~Uqb}z*zdN~5a37?+N0na&jC*EYUKYovd zBpCez6jaY3tN6R0Bo5J_c@h*3h;48DA>EX+N|10VdS^J_6kRiXX}|v0wqJ=nEhodd zHL^2Mx-J@{bjk2rmEW^B9z-dVAKcz9L;JYXU=Oghs*LCz*z%l1{vVEnD&yyv9bKDO z(qDsyAw>tPm2Y#~k-jKr#Kq7S5!&EV#tepU^+zYp>3dat=sdA^M*8wqf9VvNY%f+bC?lOX6{`iwt<|(I@)BkX^Y-A%%nAk=-K&Q1Qa+r!nt#d4LeE`d$M_)mVf&Zli^`5?})f-3KO>5tC8zlU8~ zTpQdcikln45)E{LCB3GEsSODt^PXdZfT@r&{%&N73fR~-0~Tx*SkRu}^LW$SsxMaR zH~+Wa@k8c7G{-b8Q>ftXJo$(*N7~Ju#u&ZOU!Zmbb||4I95?bhPJ;n}_2->&tidDw zB2}J4$;21Bw_;} zMig{h3PX%RimT`;8uKI?-R7YFvXrv6W+^!JmiE~VeM(P#C%=*O09fGBOzn7p{Lvv>xmNqW%Bsp^p8buW<#13b_&(x zf2T2wV!tTrxfb%Ehsj`-zdDYJM|q&(D(STlj<~UL-p*ZU{NE0kb_{$bO5>>KeqP`x z>Kc*szznSuI-rS|glHW{e`x7gcInTkNy0ra(k)xNd)licpoHykPyLP}Qq(9{>9(z` zg;c{=Z(GVCdx2pQ^nlGxUvOjuB?p5+Ba=hQ$izl?_gI7;L+=aJ#UTZ z{&++TW&t=m6h#VSBxkVf!!thuk%WVF^mZjMk%ndb(-7}|6dw3Y!hGmPM?v!mqqFQY?GHK%0>dtv3L8r|fCBO3Nz*D5 zq!|0am9{BGb?0nDyJ6JwgduD)JXTXVUKx`#LqpMiggwFPa=7c#)4^fMgH(Mdz}@n` zmF_oh45zPs-Q2k$jEMKiP~|sQa%3u;`bI@|sLalH8e1DCn1`1&GfA}eFxxZ)i=Pq; z;^qS8C`J?c_j)>^6i3a*bv4O*3GOEX@G7ao$Ao*mwS-p0H@mDSR?2I@fu9ijZPc`-JA#N zsXzZjIo*l|wF+Fq1l{cc(r z`OY7@-UJYWVx-uRBXY_0HP&l@4K2|0T#4YF;gwW@;4 z^Q7z^J_H7$g&oDpr?(U5Pz1zQBkCj3Jw9*{l~I;|iU7*K3e^91>%^H*!ML~o;uG2sQ)uzucn)cp<5+ij{cMgzsDSopWp~)Gby??6IaI%~wCt z8tt=--VdVN((Js(kXx?b@>kO)Y`r z^HfnLU6VN@y{C;~3QEj`F@dM0&qSO>;Cs2<&KFyrmzy1LsEn7P+()ffS$;tiu!bmj;WU+;LwwNy36q}Qcxg{*9L z?k<;~DE0w41A-e__q1l{=s;-2q4P|uRnr<&0c9&McrqixUsZoNqKYJR zU9t&E-(YBwm%oVFs9)(bdET~~=)Bp8!VkA;!H_htRjnqlTIh{_GAXqFPPXC=Ss%pf z0J;3SWkO6up?luDb#9sW8Tr@f^Ngio5sKx8WfCwLN`r5S6p@=7QQRAOyd5Nm&d&ad zwSh=^eln=Y=H=W=sE|;}mS*i~m-{c|fumgEa~Pi$!Nuhmys?o}YqeLYQB;PP8<6}y zG&-KhZgF&NOgEiB4Pw+%R~Fbg4r%DAVR@g$jR<5I9`&jvYj5;9f(Om-S3;M~aSy5_ zE;xFJB5>o0e0Zo#W?L@t2iXt*JYS80^j7BFz7#RVw?2C2y`uvy@*269_GOf z!h7RS_%@;N-<8aAX%hxZV|tylP|_##wuSzATh%-H(wI}) zThLRdV>(`}4Ka#i;d>b|^lIHU*By(w$CIU`EIAAlH3q*`mk`%xRb+*OpObz3LrGzV zt1~zVK^a8eDp~_hI>tl@@AD`fB*qvD*RnDH@Y<$S!hr*ZI5Lipa7Y^FeEI_U10VtN z%?Fv{d)cD{$~tD4W>3|aq!u~9LqUb>+bMR4Z`y{#52#NT+&q=(DzIfUDTU(ROv(}2 zcZ)Bx5Gg#hVr&>?Nf|F=s|QJYt85b*4mNCyrC8U7oFTnxO8J)L)x7D}Rw(FiQHcD~ z6WA6~Bx(F0?G`YSfSLcXC?X`J@l3uRbAy?^yAv3;-xB`)otgkB$a7t-$S@{PeM)27 z!feffEIqLQ1fYK!=skZn10N2i}fyi-0Cptc^9d=O6_T| zO6ETjmBD{CD$|##*x5wVDLb{hiZd0$F>i$Pe(JIZC$d(hc*30$e4bgG()y?OF-*5J z&xu>dBU@uDqZjp_0K5%hO^lw8j)?fI*R3zZN`1v@@*(Fk`XTni0dTLj?&=tKa5>HIo$br-puUzjh-6|cqy$emsXA5G0PGUf zyN9yjW}Oz?qbWk;oK$Di5u*}0dx;na1B z40<1Y3j-JpmZ`sEY4)_m-8TT`JC`_DdI*ttf6D5`f;9McpjS(VC^-C1lFRmxp_vKB8p=(hB| zu4gV_`A|vJpogi8A+t={V7rcn?BFfoE{O64?7xot)GHD zkw7Zq35BGCeC1n2Lg56wzhsFHx%1O6BE7~djgh#zH=*(HXp4r<0y;Z>&ZdG^v_;pX zp+{Oq8g$b~R)8@v#a-X?d8@~>?p3NWYEq%tvk0x&nHvg7@Zpl@t@)JUA$#Q7Ve5H5 zcX7+A&J@#J&8A1sb;u7-e)t);X!kUQt(NOv*=rm=9l#tGAw+WaW>?#;MDW}+qLqkn zL9O#5u_}T8w7qWGV^4Rd*;Lw7JENs2-IKv)2eZH>{qU@v8GCC`JIsGt0%(w*1id?% zaCpqz>G=oFYea-0WsN2CG+jFBEN&Ju;YJY4yfK?j)RbM%M0)34D&tPr!3y`11l#czSS9938Ei3~qC zqaD1J;OLDrJgw0`6idHx$uhgspzo#_-5Y*%-slR{xZR+QB=^0n;4Yh_MdYees(ybc zcpT~iFsI2|ZxylPI9+UDUu_`KT8eF;h+1_~Ir_3(cx$D-&}L@WB4Wjkg+3U~SCCGW z*V;n&OHnVx)M|verV^GD-@fdP9%_X|2e+y`$x|?4d>$3<6hV`o^Fb{b<2i6L!YA+7 z$JI>I*@|Kh+LS};T3o{|;UJ$Yrbw&7U>@_zz#sNg_Y?3RcW$M|G;OU;6B{Zq=`4f^ zHBQPN7oA*`x>yp9^P6t0d+{y)2=EVeaJz1=+yxEi-brsD+TK%1DHgh)5#Ra&o-xq` zOwBk?-W`m4>@ezOvhF}>eu=a60 zOtzq3xguJ&y6mj_Nomp|P&S@-Dss**Sa9*#_4)7*R)%ia>|3RKQBC=~h5#m$uh#|7 zk2i@h=&AVFB8k8qR(}trvYX|8C?Xxjf`bJQ$6r`Q7t${0@J&T&*!s2Nl1S!SezvC5 zw=%=#D^{pH!n&3@DVqnk)D_sqXv0|@)83k^)5stke*7R%yFvEhfPkWnM&^jnCBjyg z$|NW2hF6AF(QU%vv@O!Oqj|_{&}UV-kQsvA=@9*!&%7wU?DUegXKUhesQ)ub5z)=a zOO~NH3P-oY00}%|F~8nCS0*)g}vW7awThlpyPCclDFj2d14E>x&j z4FEZ-H=mMjcs#Gnb?fvfs%={jvT>Rv1}UNiv}}{ulHH!40>c%nPU^YSqa52Gr@JlF zbzHc5VK)?>SoySsF7y90h=XZV5E} zG_<;i_;USK77dJA#(6E6=tm8@QEAccsMAx2BpUZeOJGFc+*eR%Hk0#+yMGXHSMK)} z?bwijkxH+E_Y!n6!hx^=B`OSD#~pmcWX`qeLmL%aN+gpW#2Xto8flEuD7HA!$B!LH zF8QwzEDNa!&_B{5Xw%gzhN==oU~Tdts2yXbOk(t~u!V78KrBXvWYMd9EQPU{DXIu6 zev4}m96NCk7#hiov2D)%q1q&uJ!aK0lm5tlUJof@Z0^luL>!3BYR4Bhab%qOJ{euj zl285mdIOC6#$rHGk98(=v|G;C;(I2*C<^Vm{|Yk0--4#+NbRMSsN z|93PQdK>P|MQb9JMKln>GkBZ1%L*Y5sy`d^5-RJZKSUy=9mL(kxICoyDQTw?VkM8Y z(Xf(3L(6V1%_C4;j41Q&+pZ7ch%@dp+m(ZVKT~P`b|CTJ8OAm3m9y6R7Y}A_=WW zaMs>k-+W)5{{8YLV~NA`gvoD32ZyyD{_I#4B*b-zG$eko*fKp;f;|c(VRqH~f?7Ta z7HC}1{<8XZC;DYSde9Ddp6>9%@C|fuQLwHn5mhqR+G#-pP7yD8){PM67aG?#$E(xc zH@&~S|2Vyz>e`|&U}w?^lQi}J<%;Li5G_NMq+<%CA{qr<@D{12vx*;;sqa!|k2_Vr zKf%nCy+oqS7ARfZoJL12dzs;_XePHml)P79G3tuUMNp372f67~l{qsK>%({a9G3_i z%4^K5gQc_AyhUi)^bQ@*`Ncn~P1^j7yDXEZW>BS=4)LaKP0|MCX{IhT=C=LH40odr z?*%}S#FKajHluATy5U*2-Fl2})m`7ToInj{VuYztS^`stVLoS7cA7m4cO_#szE3%K z32~~?Jcn-W!TIz@O9FF?_h!Bf)9OxC-|g2NCH81nO;$gy?v;b1%1(WDuMXUk_3y4P zchteq#Kko1Qf;&zEr;7>CwbUU@|qc41|0QGozA@@Vn@SNYXt2TcWx)6$N1>O} zvA^<;!R22g$feMh2_)wGOCY}fakC*`s*l&L^C<1FlI;Q!qyPgeUZ>q-!fQ5O@Xz>B zPP%CQyC<_DSll(05Izhwf!05rgD!%1vOaN0 zNdbV~R$7cLpyx<#qvwiOhq{q|4n>}Q`PJ5v@~{gsq%p_<0^;UH)Dq zb|NC{=HD3NLCL-$$&rs_C(W{}H1M!V!u17AdChHqI4s-w$eBqf`$IJRu~#==I}(pF zH$Le-|J3&LF1a68;K96tdxC{HWLi;(re^<3i`OkFrwqEqr^_6733tGLiDpsVQM6ua zyH&&ue-)-5jkM6Xz`b4ZvWR<~1T>nIp9r!Tk|5WEUDb?Zj@h(o#w;abryM;|617mQ^ zF^26le7T!{lhM1`lMm#VNOaWG|6Z~5j}i|XubKcvUmDlJNzt!Z*%31K&GEDAJ@wfes(-@maO7$4 zV_fBK<17$HKkzQuMxNzv5gu6>7YiBGRw@rUHel04YtL2do9AXu-t8=QT{Z$X4W+s3 zvDqp;Hn6HTb62VQ717r}#9Qn%lW^_58|gZFH-_K(LNjIgV< z%SGxv1dG@ful3#A2hHKkyBf1K-@*e4e2Qi7UCn4prl+$gwjW}2CWIDW1szmu#1nLz z!Ix3|6ljaDnwCAAmp$*Q3+DtrOg-F|jecEWTUDI-Xho{nuNL^9F7rRThJ|j8>15;4sJtgfUXjUYbT%e)TUI91CMC z6Z72ug#09#Tky(gm;+Tc_)I68;oeWTy4YDUizH-Qa9(A0cm-wL8-SJ1{6RMhj0Npp zMLg^f&@Ow^d(aL38jbLggl5jQ<8ZBA+jFo4VyFIdqJCy-zHix_MNx*@>?a)QCu|Vp zaSc!z{;pTi!fp7Q$^A98{>#(`_-W2gH6Y!t-SF;t?GZA_c05n}up;O0R^;b9=K1}s zOux}Ura@9W{R+dG)+CbFp#o(q=*MQuMJSvMQj!|cZI9DP*e~|yHU<(U1?+vs)|}arNw+;0OY=ejwC&(3Yb{ ziUT)Uc4De@9OgV}D`y%%np()#>Zq6PJp5HFn|hL=x(VIuDG)vo($I^vrMrDQJrV84 zjl+bm>pWrKea-!TPRDDcn }lP6L*{2WOC?5%#y_-wK?YtxNQoJr96Tww5*p8AGfXD>8K z18g4GGU~rMue`%BN;I)%(5R-_lgaKk)U&CjyRlz>jZ6VpCwJMF7cT5Le~yM%_+XJ< z->2-aZo!Q&%+Pd3#U^?lP`{L^LRex!urp%*n<~naL$8?eFbGf`bn$^D2%RCMfoQ0( z<8fNzXN1QSIYh)$&Eg#%+@S=J#mm=y@@iFMCglm%R5}{5*k5RAvlbdFG`sjdllbW_Nh4&YQ)f-u|0YlBJ`JVlW@0W-X0F7uabyYDmOI=cgc+ zMmQ(1V3?He_5U6qEzViwCt?l-OC_e;6GS=q5~jA}+O%%CSAW>no=@H0R%U8;#H06g zzzEE(!jsaCT4`pIJ-K_Xp)u|yCr5lbD+h_XEb{S%VZ{ZM{U>;p{La_5N?GC!QDOroly*6IRrI+%{_5|m%MKEv;`LGFP2;qwDp&0WIkHJ;ZScWbTX2brlHU+{zDM|KP$6}{+ciQ+PC=1Axv4RE5G<`E71;cgkP&Z z3w~065kn3&Qse-g)@AXMBQ+)jC=9ZtYub*`T>_R}y%Id;@M5Mn>n};%Rs_8-Ay8X) zUa7WQ;?aooD(Nky^_@F{8;6&vmn;14zGf1IF?J)t>yolc@BZ>*{d08CrByeKYJnbE z_KTqlvB3zWHGuE=kiK`i!W0|X)hZ@^wHKtLDu79}8&_Qqv#hdCE5Y{^=k6B{83A|l z_sXU1L6`LA{js-`U`+&vA`8de)X-$f4=lY<1~}Tvv)L>+Jx%n|2=Dy2>l*Nl6xq!7 zAuRI0JL$HZRQpg2=jm7+NDwc$4&wTAoADdstRHVqy8B?m8p`W0`1(3()Psf<`Dj(t zawrvv@yk90FJ4Nsg%Ly4wMtmQilYl=S-N8~;@T{QdQy)_cU1XQzL|9aI!(5^vX-=? zOP>)z@Lw+(N7tIg_DgDX=<>xiSCycS4tFw)T?ylDk*Xlkda+-#zok!Y!sB4N_x1;=! zVRv1Day=*00+Wr`INS?1g9Py~`GKJS2hhekV#}1Lw$wxAO~y}TK{p=s9XkMI{JJm$0* zvsxqW`Y?E%0xKz00R+1Ryww_dy#0p&vkMYkRMP^F8wvVVoEW(U;f^GjU-bXq3?y;R zGOk8k8bj>V{EBX5uNYnKI=L5yxL1tYV8WzDGmjIF6QUEv+m_#5*>*mUONm7-IE1gs z8cj#i_~JblGn;qmF04cz6uF3YBGpVEf!y3qhjf(+F0#*q{GU6wzG2iRD>%w*qRa#x zI6a;SuxE>C!>pgq{d{vH2ST{Ue5>2fTL)VFe#sOwt9o_fL#bvpVYb*dR#z#*8(;Y# zCNe5cddVlxA!4J>H&CLHw00_eh`sG7<0wsXoJjPIm`7a0HUFg)Tix2WJ1PEf_rO*j zLt0?Q0s5ZF=x9cEOFq;*n#V^;zZ@z`T5lfKo({TU&L{&FFd0Ag_vYLDEEYr0K=}88 z$oiw1cW_4n)mXV~d|jLOIv>}l+DR;gfj`yK?4sksUQY{7Wk>A+x0Cf<2hQZ76Lx^m zRVLWQCkeHl58*VM<)Wbd*x+EH=r6c)emMIG8)q9d)h~DQP%*VUAx*#@m3O`O>dDjn zv6#ac%)Mp*pbmP@lE%P%QTM&ar>}C88d6Nwk&~^wTvolfEaSHG zK3y>TwgiNHeJ2U59E7gk+L3fZi$H2#u9##@k=fo33A<{2SL3v2hP2ttgkp@-0mV2C zc{8fS?+|Cqj8lL6yeZ%kStMXvAit}{V^d@o(I%y27skI6vw!m$-Up6VyV05!mVb#~$CR_Q%5q2svQeV;sMKmG z4e(%^vTb|5A~QaZcjD02-24~38x+c zSu9jv*TOx)Ss>E0J->Nh2SkWyuf0q}P-5=f-#n=IkI6+Jun%J+GclceyH@QmY?rRK z+YS^oy`fsoL9m2te7G**l}W&Vn?idVaFAD`2hVkAungy@@8x6Wud?L!t^Au14c=Uz z(N65S_o4-TmyjGi>BW=N82YV{Jt#7;ZVXsdo}>A}qO37}U8rfNh32g(K0#12L+ERq z@lk>tdot!f0AbSz8*fwNf`%siUhFf}#A%Y*DV)!K)#`c+9*8Pl*A42*w#o#W2P2+V z8yu)=36Dvb*6Gm2BHuwyQ+%eMfYme4^EXLOc72?zo%iBgYqP!%awz*QT6epfXGYmC z2SrtMzU#8{*`)HGtlqCd^)=iIFu@1|M6!gZvFL(xz#4-FQ>OY zRCTWaf6R8_FFs9i+y8FxUi@e`H~8^Y#Ko!xb8UDwQ1-N#_%PjTn>d&QmwL_fvYx`} z)mZ%cABdE|mi#1X25Zz9)t25_siDH6FA5Yda~Agu`{`Gl~^`-0<&rh3JBcHZxwix z^HfBNo;3Mha&I=S$KBH93PEu0gmF@Sy6#(1I#a*xTt)6mN1)*qVBg4n&sF0IyShut zkLtK~v37Il^XT2f20BQ?>#UtXcrZWm7h?yjga(zu(yPN{(0y|7)T^%QG@2aV9fU+U z&c9Nt)ufrx*ARR-dDs!?qWK9vNQ0VIe)U(aZfM#2bN?@XWMF?4ziLkS(8X`nY{LM9 z3~zXq#hsSs!PkuoOGDRN(L%swhkla8s)1r}S*m*UYF9g9Tl!T$c%2BiiET^&nT^;K zBxw4Cr}tr*^}BXoVMi|9r_sST`vT8beMA6v1bWJ~RW6TyY#r|KVRoaPC)j;lEpQqp z&31N`9!@RzGP)Npz0JV-g@^6|$O416#dGT6Yj>*A)CK(G)}u;*?+LyBz-n4Fti0m} zDNE&8Rkd)Hoeg?X&dDqDa@8s-UNNFc%8P@kwC2K`>S-xXyktV2Rcq<75KE(v;W;B>4C=5 zhKVqv2mj(@+33WJGs^!SZv-0QH|8kMX>Iu1_R=AVlKBut9Y%C%Ov&DOeydq5)h?7p`>_?j4@sjlm zK~iLk;CqOY&(yU_Zfv&w-5?N1aqCpos}PhIh}YEnHb#=$<4+`Y%z!iF^SPrq03tJ9 zXiRdlfZ4h&9v>@!Z919Pn<3Cf#oWjiR!BSL^-9fi^}^vsd+n9n0rB9p&{bk>=a{I3 zlg`|3<&1nh8cTt`hK|KKesc2Y?#XUgCT~;T_Ch|zQhZ>tDY|?3hM&0r`TR?&#CNSr z@tcSu-{;(iKIaUHuk~O5-)dOIwu${O3!we&-_C**k#B;u30(+2!l3>W3fZh8_`0F$ zORUoaYq~T~Ge>r`Pa(8>?xp@_{33^%XZ7J$sF>rTV0(l8E(@BsM(C$G_I!WYKMc{} zMn>RXb2j!jlBM-g)j_R{ntbX&_h59f>rYduD z=r-$VYfZ8>T|CEyO$iL^9{g~~Xt1MXd1S1w*t;4Wk^qU?k%D8FOLsM`;Vh>z6Xp5{ z%82m{2RoAUjcaH^7Yy}Ci-$nCA8eUgy_NgJqQxoD>Z;QOshRBKd=bHM$vLs0s*YCvEqiSS z8UX6um3`k$Q0dU*Rc|7^Q4+JZo)<5CWK#h1e)3pv8VZ@~Oc78ujI>h5S=HrrE+lNJ z%Pez&XZgIFA7$9{eusq9iwkWwv%z&fW3LM0*#T!@thRLaZYy}G8%6~3rTFC3SM>>B zaP?*RxGG0}u~_wHXA+q(vHohF+-$QPEEEFXX<_8tZPr2z%K@w^3b3B*kA25;IPHAQ zBx=G@F8bL;vN_OQQsYP0E24wiS^r$1#8;hA+m&RpsH(1q%eym4RJJIK?B8U&(#Tnl zxGkvTl}xxbRBM-7=Qb>w`0jUxgGYo!Ha+QYo9$NE70+=MJT_m>MV~MPHQI9C0wc^E z@M-1YPfW+sv^}hZ?6GRJ^-I1!%NGg%_xz#`KAb*UZB)U^{{9?YrzkrtS zu##{4h!@*C0AD0CCF^<5CH#&+#Fo|W;@NIJ(ES@~CW-gfikSt)OWZZNL@Eq4XUX~b zMlB^PmUerCB{_CB^&S6QMdy?%zhl*Nr7OlTR=s))qi*<=Z`0-QBcn|lNFZ~jV+O3- zv=)DD2Wiis-ooLGk2YhBc3rtRYa=nYSk-{ALDF>0Gryi%2j7=@tqKytd<+%>*z!W> zqaAim>OQ~iE-f!IJ=n`c)=NE%jN~s+KLR*Y>HhmgVH^oHoN!zMg_@ ze#)3GSN%II;Cme<3F{R|;u!#0=?NOtS>A)yY*GieC;==0Gcvq6?Y*of-@A+CPM-1* zrK67T==XD-4_4hRN3*EVe9P;!^a8?;Dmxl+W(|Y!$%g}uv$&UTX026Eg1QgBBXu$ zPpL3My=3x_U+>-FSrp^2Qg+OY`q>m4~`b3lwD;+2+2B!dEz{>0Z<-OT90HF+q)brYZy0)LrI5jHs z!rtqZTmjnE^Z@}CGvF{*v3)GGnoG+A|2}obG{orWz~u**3KC==V;zz$K5CB#|J?$4 z;)&VQxZ}5cclZi@QOvx=;4M_=v26IgA#X_9O)RrztcY{GfTWu_rGz6%w=l(Vu_gaLiA_}7`-(W?0)<|S z0kQ>NX=&e(q1$1%pn{8;M8M=Q&Z@e)ddjd)c^OT_-7*u%jttrzX;JWnRpTk$w7;L|30;v92{GmjDJX>7) zC0S(Td08y6z_8tJuEuky{NmL4m}SqWPIAG`zZpWi@GL*$JU$Jvb7KzGaPIJuXzRbn z-;QW_8|`}at=T0@GfP(lJefUr?PF-OZIq-sh``rDEjOHhK-dKjzUMPBc9&CcU_>pB zFUpqJ2BcQxiW9ClGH&nM1bE?K+}$LBhFe9!ND~`7X7i3cB?+0}D(crP^e&F;g@St_ z|HWQcl5iNu0yG~nA|6(?>_jyoTacSN%EW)2t1dq-;hC9HQ)EHKv(YZf(iisqIdf%t zT&hi48FzmgLA7yYIyLe}C^bTIfQl%>uUi1WMWC~Y*k@Ju_18j|xM5`?Sa zAxY0uscZUO*@>X{Y?!&~(}p(lthY>s*?o zm)5D^8*H&`3UXE`?E32ODb9UdriVXeFx#55HfWjt-Z`PcejC|f{O#!^Sw8Z<`q4M< zK%>Zp!HAn4@3SSRw@-3jp~rpli_+qp75198zDE;(pqVXP$L3m9p~oMvPoon%Fs@_z z(mQkd)uIrn-dr`@<=t)*JalVC~F`35vN3X%Wn4P76@mGa9X+U-&e33^^GBVpDjx-vBmQjUP%8Tm}^kqei}??sBH5Ijy%#R!+)lHel&65T`02Zue6anf;+Gaa!vvpR^ick& zm@UH^+40Uf%ur|cjf~{4$98&{PF%9oX-empK~d%7B;7RJ2?BPp%UNzM%@=rF(0fiM zonZu!;L-Vdn#Ze0D0Y4jrSGTKl{u;ve@_P=y`|n49qc?R{;4f7QRPBW2woX~|Io#X z+CyG_=VVhra|)Nil-vAD_O>rl^j9k&A65Cga8{r#(v2f1#%Vl9Izut=L@>H)-l2E`Y|hO6U}bL2_f zRq>yUk2Ws@pYIK~$uV^jpQEZxWs}`78Ac<{Rs8+{Y~4884SUy5vHmukI-ps_73aqx zFSet|>QRSIF1_(#M@StDcubiDb(k=rJ>F%Za-88#4d0HxjqNiwd)%0*O$gwXU%oFW z5fT?!Xt=hqjl3TH6QX{00$Ny;5*vt)OI4JPQ`G$Up{}8kDh&Z4;vu~X6y1>UawVXh zbURn!LIKYJZwCt@LakYsUls_@;^jV3E`BDqCLhtsV1%*EcMq&-7(K{dqEav ziWh<6m0!W6?m#~y4v<^C-!4a zx=Ysol!v}ywW@@mqy@r1bWFnw3)I=i4BLs2!W<-0bc9bjQZ<6SUy3C}zT9~;_*ZQ% z#5v&E0{WXiJ;2Ao_O*vjg`&Ew>EcHNi2iEL9FC2VzrAM7FkeSo)%bi*fqKB$#73{1 zx6Ws@8tc+|oBF${@oVpsL!vy{d!Cgyzjup~V@fA_fkN~0L-n`L+1nwk#T9J!1OtLF z-viRsB;|8R(;9BfD*#`gQ1qH!`6vT#%f%}WBM{a zT!ZQ+d;*cKOU z@&y1vdkeze@asuCF0cBmXYX&r6j+Y4KTbfu5fnaJXnWE*JHZm2IQ-qS#sSy;0h0l; z__z>LOQvfa(<_9 z80$@2`O~Li4 z_%)_9iJ>DoD+570(~jqxe}pz9P((JsJ$~OB-x_F&C{aRF2@~m2&hKhtiJa-#@v8b@ zC}v-Ea^bY$jDbyN?2$`bKeY<%ekJ#r;{kakFaZ=uHt}kSEu@*%T6A^%W>nay%Zxs`GC&RUo&An+iZ`7T*q2>0S;K9*_{;O_Jv=rc zPk8c|!=qlO_9jaLM9Azk*#pfoo)94)o~gGG?wDpgY$Cb?9b}{f$d^J*OB6e42v>KW zx!GpMG}#Lln~y@8XGCVv`kEAxgL)r?9hI3T8lCgNuaM=@Ct!|Ls_G(j;rsz9mA^7K z-7*zzMWUsSphJw$hlRRQ{rTmOlj_qx)btgibx9Hm0H(3kU5mS`!{q}|BS8Gx)q{SZ ztD95ml6|t(fkMSrY)Tt5CQBZl5~-YiUU*x{OO<~o=E0FLy5GJTQKHG9*S2-t7X~|f z4R{tfyRCGkpk9IdZB8?qL}GQfYSuT!JK?BVct-c^`$Tn&SZ9T@PpPM3u*TI&e$44C zryIqIu7;TKj1di)uI^dHaQc1s`jES=(aEUM_Q0b!q0g#(%`YwIivbdR59lF5oR;$} zz!br84T--lz0+%0?`diaFFO~vqv6x}182l-@bqVYFQr4Gkfy&A!xMd?PJC+AHeh;7 zDur(!!SwrajCuU_dpLi8EC2HA%?G>o5_n3nb&{$!AO$k!aMSU44L5s?RezlI2$O(0 z!}-Rxujs3)Js28vTI>$+Vig{GX@n8K#kWFS9n=7y~B-+c$$D0gF? zM2iGkx2Iy7_uFQs~^hEE^RK)^EtD@u6!YK6lbBK)^~>V+zdB=z*1<_{*UdFeAaX4 zj`Je2m*>H!ixMMJ2U^*QWI32o;9D2RWoL}#AODsxB^VB*F^|EsHILg0deF{;2*|`* zlnVpRbW^eEK%pQvH>oS_`ijr_jbOdr)a>WOncT8jfjcD0UYgRchaMA!D8KNKQ)1mFh?a;p-;U?ho6FkXg7IoI&*a|YhR!}Ky>7(c~G=3!0Rxfn1R1a9j%nc2CTqM_IR7z;{{(>GYhpt)lrrI${5E)!g>4cyM zC=e~GhHJE+Uo>{1jM@6nwiG8p{E{|L@k5`mqWNl2U#;b-B-4`-qC>YJUE_7e3j-1> zDr>??a)ev++SiL;p~&E+Qe`1@Vqy)3oeqm*%k|!#PIARTh`kNNc#kz-3_>*E6$1~PrD_KoZ23> zZG@@I-!diM2x@ZJ6u%tIfGi+;_AxyH0r5x`x=up(s_~WIlHcR z$6ooeUu4eOwa*~?EqZm*Pq!?p%E>lbPTK?0WvKYz$~%=Z4X;l3rLt1jP3Y{K~sz z>e2`k+=AI}lsDTrwtDTXG_1;$WZSSgiWS^kUi0YE1H*Z82fdCeC7tt!#$ikR7S6{8 zo@EgUf6827f0wA00%R`Ya4qqBPHx)+mC8f7lqBeK7HeD58g32U`Ht-GZ6_~=ovb9S z8c8f1XKbFe*;a?wXjrSANBg2mV5e3NGPNVOt^afPO?r);^kAKp6OSnkt|ZuT`@yVI z)=zsxOzS~&W4IK3^IczcNPY5VzjjKXo;W^s7sea&i6M>|hkhN~{aH4GVU3|M#^-7U zYlmM@r@60$`%*mdAUffJ1=m#=Q>eu;Ogzd=8JEpU&Ako_Qp4GBd9>=U`%$%oloHVQ zLlP{vKa8-E7xxw_6@2Q<#fwrO=Z0*C)oupL)2uJOAPeqUd#2S~l&vAfeo)z&By+`H zi+}3Up&n58>nTL4aZEE9MdCn?IIc8lOiiJ2kLK$2*46E1zWK$RJgtz$ofqx=F+ou) zwI%Sg)2SqUgRd^d@3i1fOn^fxY4{`-w*Ws0!&nk1@ABuq4e(_xp-qH0KRlKU^ES6w z_T|P22WGqpd4U|eA*p5#Nzg8U&>mP~kr3CeaeN`x} zi~JsBV>_K3vBM>^noQM4$Hhdh+>TfGxSuy-V@qRPan3<7uIAy$qcp0FhJqZ=9)GDP zOT~t^*o>&pNLAt@+3)-UycT3+i5l78m3GXDN2h%6LNWLbwPOKSr6rO5Ou-d9jaJJi`FE*U!sS#1+=HG(GGEWAG+I=s|O5}z!@>gpdOSol$JIF&P%k}_V3 z;pc5a@BP|ACGD$7#;ZO;g0m9PTBx46UxDBew@cUNCg19QYz(z|b_B(Px}@)Yx^J7NPcfMtPY|UW^A}9~&g01D?%PJiv0*p=KFda! zg?BJJ>N4$h`OQ?L4U{vaV)*uOe7?2|-fb|xaxCWcI%u!QSJw=+eqCz3-2Dl4AiARV zhug?^#Uc2w88!812dSGNj=Wqw3wqn#07NN})~ba*ex2?7gh+`bX*TIR&bvd(Z8Pe5 zhtImfr2yV4O(1^V{YOg%>KL_+{~@hrW22zNw1}Qm35mhh*5QneN9WcYFV< z5b(1n{wTYf<2adn6T>cRc+q-?wun^vZkc&M*((<_!qZ(pzql!dA2Aq?Y0+JMl?SkD zV`a|pOV``o48QCSUtmbCV&zJ1U?o=nX>cmyk)T9zs4g9YJ7dC%l6|4ixI0rx@7lEX zxy3apXXb|WOSHWv_vKJE&@Q>gG#}`1$fZ3Lwa%}MBl}eoh={u<$RUs6Aeu6h9QsJ? zdtQdM+IIE!rd{#9ZAaCHMwJ7saM?OUYIQmr*&&%Y<>oD=W2>Hdf=ibvk9rf~<#i4Hf z5|Fe9hb89_OU}7W96}pdW4RW#(|t9DmB%GXzl@^|W-)`EMFjX9*LHisr3_MylcR^B z=M$Mtlqr3NcsRe~zsD!K&(d#y8aIWGdKzA85cxmJ`*M4|d=S(KE-L1KDb;Y;9YjND zhtp<%06B5nSJY(c$nj5gVo86itM!H>{RW^!LbG6PX%uR%UqdfnpZgBUuA7-;UD~5l z?vQXjT0kW$H999jd|0OBr9?QF|MI18T6MxsQP938b>UJH4k7S{-E_~ugC7-R`3j5; zpDHJd#>S!TE($|pb*qdyl#I%$ek3=nOK`6l`VPi-%?d^3*4&H}XFKZ*EtWt6%D{pL zeUA~?=AX09%oW0*L8PZ+RQ%8H)}_KS3(n;~drz52!tYmWk?kqhMv;B}6gd(rjDz}J zF~6?jrJ@&wYFweC5?)yLd%5tR`}3i;?h_J{u1%VIbNwavcM?guM$Y$`Lph+I1zc~K z2Igp}-{_Mw%XHCXLg&uIgehOfNaBP02}mOdRv(7lY~Vkp^MAcen%ik#`}WDEmHrMD z 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

u!vSU7Fl5&UpB=6Tz0Egu%CwMz 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 _^JgX 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 z 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 NONLhrO3pci{|}~)>JI<_ literal 0 HcmV?d00001 diff --git a/docs/assets/ad_retrieval_use_case_overview.png b/docs/assets/ad_retrieval_use_case_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..ae60a605517418ce360bbf28f8e8ed75b7ff9788 GIT binary patch literal 251156 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

1@sj>spu27%_SI&>qMbb@0CZ$Vi@~nEvK02 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 z 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�*cy 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 $*ltnKSDs97W^%x~xQL9q#-KBf$J+ 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

{$0i%mU$nRF^3Skw49^#bTuX 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 literal 0 HcmV?d00001 diff --git a/docs/assets/ad_retrieval_walkthrough.png b/docs/assets/ad_retrieval_walkthrough.png new file mode 100644 index 0000000000000000000000000000000000000000..b016d433cf9202bd05cccce3b0cc3b5ad1e356b2 GIT binary patch literal 262447 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

Nktzv50WXT;hj+=i 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 z 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 Currently, file groups are only supported for [SNAPSHOT](loading_data.md#snapshot-files) files. + +## Naming scheme + +FILE GROUP NAME FORMAT: +``_``_``_OF_`` + +VARIABLE DESCRIPTION: + +- `FILE_TYPE`: type of the file. Currently, this can only be SNAPSHOT. +- `LOGICAL_TIMESTAMP`: is a 16 digit monotonic clock that only moves forward. a larger value means + a more recent file. +- `PART_FILE_INDEX`: a 5 digit number that represents the index of the file in the file group. + each index can only be used with a single part file name, loading part files sharing an index is + not guaranteed to be correct. valid range is `[0..NUM_PART_FILES-1]`. +- `NUM_PART_FILES`: a 6 digit number that represents the total number of part files in a file + group. valid range is `[1..100,000]`. + +VALID EXAMPLES: + +- `SNAPSHOT_1705430864435450_00000_OF_000010` is the first part file in a snapshot file group with + 10 part files. +- `SNAPSHOT_1705430864435450_00009_OF_000010` is the last part file in a snapshot file group with + 10 part files. + +There is a util function to generate split file names conforming to this naming scheme from +components [ToFileGroupFileName(...)](/public/data_loading/filename_utils.h#L67) + +## Server startup and snapshot file groups + +During server startup, the server looks for the most recent, complete snapshot file group and loads +that first. Then, it continues with loading most recent delta files not included in the snapshot +file group. A file group is considered complete is all of it's part files are in the storage at load +time. For example, the following set of part files a complete snapshot file group of size 2: + +`complete_group = ["SNAPSHOT_1705430864435450_00000_OF_000002", "SNAPSHOT_1705430864435450_00001_OF_000002"]` diff --git a/docs/loading_data.md b/docs/data_loading/loading_data.md similarity index 67% rename from docs/loading_data.md rename to docs/data_loading/loading_data.md index 4dff2d83..deebff18 100644 --- a/docs/loading_data.md +++ b/docs/data_loading/loading_data.md @@ -1,66 +1,31 @@ -> FLEDGE has been renamed to Protected Audience API. To learn more about the name change, see the -> [blog post](https://privacysandbox.com/intl/en_us/news/protected-audience-api-our-new-name-for-fledge) +# Loading data into the Key/Value server -# Load data into the FLEDGE Key/Value server +There are two ways to populate data in the server. -The FLEDGE Key/Value server is used to send real-time signals to the buyers and the sellers during a -FLEDGE auction. +- The standard path is by uploading files to a cloud file storage service. The standard upload is + the authoritative, high bandwidth and persistent source of truth. +- The other way is via a low latency path. To apply such an update, you should send an update to a + dedicated broadcast topic. -There are two ways to populate data in the server. The standard path is by uploading files to a -cloud file storage service. The standard upload is the authoritative, high bandwidth and persistent -source of truth. - -The other way is via a low latency path. To apply such an update, you should send an update to a -dedicated broadcast topic. - -This doc explains the expected file format, and processes to perform the common data loading -operations. Please note the following: +The data format specification is defined [here](/docs/data_loading/data_format_specification.md). +This doc talks about how to load the data. - We provide a [C++ library reference implementation](#using-the-c-reference-library-to-read-and-write-data-files) and a [CLI tool](#using-the-cli-tool-to-generate-delta-and-snapshot-files) that can be used to generate (or write) and read data files. - The reference library and CLI tool are ready to use as-is, or you can write your own libraries. -- The data generation part is a general process that applies to all cloud providers, but the - uploading instructions are for AWS only. - -# Data files - -There are two types data files consumed by the server, (1) delta files and (2) snapshot files. In -both cases, newer key/value pairs supersede existing key/value pairs. - -## Delta files - -Delta filename must conform to the regular expression `DELTA_\d{16}`. See -[constants.h](../public/constants.h) for the most up-to-date format. More recent delta files are -lexicographically greater than older delta files. Delta files have the following properties: - -- Consists of key/value mutation events (updates/deletes) for a fixed time window. The events are - in the format of Flatbuffers ([Schema](/public/data_loading/data_loading.fbs)). -- Each mutation event is associated with a `logical_commit_timestamp`, larger timestamp indicates - a more recent record. -- `logical_commit_timestamp` of the records have no relation with their file's name. It is - acceptable to also use timestamps in file names for ordering purposes for your convenience but - the system makes no assumption on the relation between the record timestamps and the file names. -- There are two types of mutation events: (1) UPDATE which introduces/modifies a key/value record, - and (2) DELETE which deletes an existing key/value record. -- There are no enforced size limits for delta files, but smaller files are faster to read. -- Server instances continually watch for newer delta files and update their in-memory caches. - -## Snapshot files - -Snapshot filename must conform to the regular expression `SNAPSHOT_\d{16}`. See -[constants.h](../public/constants.h). for the most up-to-date format. More recent snapshot files are -lexicographically greater than older snapshot files. SNpashot files have the following properties: - -- Uses the same file format as delta files and are only read at server startup time. -- Generated from: - - compacting a set of delta files by merging multiple mutation events for the same key such - that the resulting snapshot consists of only UPDATE mutation events. - - compacting a base snapshot file together with a set of delta files that are not in the base - snapshot file. -- Contains the entire set of key/value records since the beginning of time. -- There are no enforced size limits for snapshot files. +- The data generation part is a general process that applies to all cloud providers. + +# Before you start: choose your file format + +Currently the files can be in one of multiple formats. When deploying the system, set the data +format parameter to instruct the system to read data files as the specified format. + +- For AWS: Set the [Terraform var](/docs/AWS_Terraform_vars.md) data_loading_file_format. +- For Local: Set flag `--data_loading_file_format` + ([defined here](/components/cloud_config/parameter_client_local.cc)). +- For GCP: To be supported. # Experimenting with sample data @@ -77,9 +42,7 @@ Confirm that the sample data file `DELTA_\d{16}` has been generated. # Using the CLI tool to generate delta and snapshot files -The data CLI is located under: `//tools/data_cli`. First build the cli using the following command -(Note that to build the cli to use `generate_snapshot` command with data in AWS S3, use -`--//:platform=aws`.): +The data CLI is located under: `//tools/data_cli`. First build the cli using the following command: ```sh -$ builders/tools/bazel-debian run //production/packaging/tools:copy_to_dist --//:instance=local --//:platform=local @@ -178,10 +141,8 @@ And to generate a snapshot from a set of delta files, run the following command values with your own values): ```sh --$ export GLOG_logtostderr=1; -export DATA_DIR=; +-$ export DATA_DIR=; docker run -it --rm \ - --env GLOG_logtostderr \ --volume=/tmp:/tmp \ --volume=$DATA_DIR:$DATA_DIR \ --user $(id -u ${USER}):$(id -g ${USER}) \ @@ -193,35 +154,32 @@ docker run -it --rm \ --starting_file=DELTA_0000000000000001 \ --ending_delta_file=DELTA_0000000000000010 \ --snapshot_file=SNAPSHOT_0000000000000001 + --stderrthreshold=0 ``` The output snapshot file will be written to `$DATA_DIR`. # Using the C++ reference library to read and write data files -Data files are written using the [Riegeli](https://github.com/google/riegeli) format and data -records are stored as [Flatbuffers](https://google.github.io/flatbuffers/). The record schema is -here: [Flatbuffer record schema](public/data_loading/data_loading.fbs). - The C++ reference library implementation can be found under: -[C++ data file readers](../public/data_loading/readers) and -[C++ data file writers](../public/data_loading/writers). To write snapshot files, you can use -[Snapshot writer](../public/data_loading/writers/snapshot_stream_writer.h) and to write delta files, -you can use [Delta writer](../public/data_loading/writers/delta_record_stream_writer.h). Both files -can be read using the -[data file reader](../public/data_loading/readers/delta_record_stream_reader.h). The source and -destination of the provided readers and writers are required to be `std::iostream` objects. +[C++ data file readers](/public/data_loading/readers) and +[C++ data file writers](/public/data_loading/writers). To write snapshot files, you can use +[Snapshot writer](/public/data_loading/writers/snapshot_stream_writer.h) and to write delta files, +you can use [Delta writer](/public/data_loading/writers/delta_record_stream_writer.h). Both files +can be read using the [data file reader](/public/data_loading/readers/delta_record_stream_reader.h). +The source and destination of the provided readers and writers are required to be `std::iostream` +objects. # Writing your own C++ data libraries Feel free to use the C++ reference library provided above as examples if you want to write your own data library. Keep the following things in mind: -- Make sure the output files adhere to the [delta](#delta-files) and [snapshot](#snapshot-files) - file properties listed above. +- Make sure the output files adhere to the + [specification](/docs/data_loading/data_format_specification.md). - Snapshot files must be written with metadata specifying the starting and ending filenames of records included in the snapshot. See - [SnpashotMetadata proto](../public/data_loading/riegeli_metadata.proto). + [SnpashotMetadata proto](/public/data_loading/riegeli_metadata.proto). # Upload data files to AWS @@ -242,7 +200,7 @@ You can use the AWS CLI to upload the sample data to S3, or you can also use the Confirm that the file is present in the S3 bucket: -![the delta file listed in the S3 console](assets/s3_delta_file.png) +![the delta file listed in the S3 console](/docs/assets/s3_delta_file.png) ## Upload data files to GCP @@ -250,7 +208,7 @@ Similar to AWS, the server in GCP watches a Google Cloud Storage (GCS) bucket co Terraform config. New files in the bucket will be automatically uploaded to the server. You can uploade files to your GCS bucket through Google Cloud Console. -![files listed in the Google Cloud Console](assets/gcp_gcs_bucket.png) +![files listed in the Google Cloud Console](/docs/assets/gcp_gcs_bucket.png) Alternatively, you can use [gsutil tool](https://cloud.google.com/storage/docs/gsutil) to upload files to GCS. For example: @@ -260,11 +218,44 @@ export GCS_BUCKET=your-gcs-bucket-id gsutil cp DELTA_* gs://${GCS_BUCKET} ``` -## Integrating file uploading with your data source for AWS +## Organizing data files using prefixes + +### Intended use case + +By default, the server automatically monitors and loads data files uploaded to the S3 or GCS bucket. +However, since the server expects the file names to monotonically increase and will not read files +older than the previous file it has read, it can be challenging if there are more than one data +ingestion pipelines creating delta files independently, because this would require them to +coordinate the file names to make sure the server reads all the files. And the coordination adds +extra complexity and latency. + +This feature allows multiple data ingestion pipelines to operate completely independently. + +### Allow listing prefixes + +Prefixes need to be allow listed before the server can continuously monitor and load new files under +them. The allowlist is a comma separated list of prefixes and is controlled via a server flag +`data_loading_blob_prefix_allowlist`: + +- For AWS, see docs here: [AWS vars](/docs/AWS_Terraform_vars.md) +- For GCP, see docs here: [GCP vars](/docs/GCP_Terraform_vars.md) +- For local, set the flag: `--data_loading_blob_prefix_allowlist` + [defined here](/components/cloud_config/parameter_client_local.cc). + +For example, to add `prefix1` and `prefix2` to the allow list, set +`data_loading_blob_prefix_allowlist` to `prefix1,prefix2`. With this setup the server will continue +to load data files at the main bucket level, and will also monitor and load files that start with +`prefix1` or `prefix2`, e.g., `prefix1/DELTA_001` and `prefix2/DELTA_001`. + +### Important things to note -AWS provides libraries to communicate with S3, such as the -[C++ SDK](https://aws.amazon.com/sdk-for-cpp/). As soon as a file is uploaded to a watched bucket it -will be read into the service, assuming that it has a higher logical commit timestamp. +- Records from files with different prefixes are merged in the internal cache so two records with + the same key (from different prefixes) will conflict with each other. These collisions should be + managed when writing records into data files, e.g., use keys `prefix1:foo0` and `prefix2:foo0` + for writing the records to data files and at query time. +- The server keeps track of the most recent loaded file separately for each prefix. +- The server does garbage collection of deleted records using a separate max cutoff timestamp for + each prefix. # Realtime updates @@ -273,7 +264,7 @@ delta file to a dedicated broadcast topic. ## AWS -![Realtime design](assets/realtime_design.png) +![Realtime design](/docs/assets/realtime_design.png) In the case of AWS it is a Simple Notification Service (SNS) topic. That topic is created in terraform @@ -293,9 +284,9 @@ The setup is similar to AWS above. The differences are in terminology: - Sqs->Subscription - EC2->VM -In the case of AWS it is a PubSub topic. That topic is created in terraform -[here](../production/terraform/gcp/services/realtime/main.tf) Delta files contain multiple rows, -which allows you to batch multiple updates together. There is a +In the case of GCP it is a PubSub topic. That topic is created in terraform +[here](/production/terraform/gcp/services/realtime/main.tf) Delta files contain multiple rows, which +allows you to batch multiple updates together. There is a [limit](https://cloud.google.com/pubsub/quotas#resource_limits) of 10MB for the message size. Each data server is subscribed to the topic through @@ -310,10 +301,10 @@ data loading path. If it is not, then that update can be lost, for example, duri The standard upload is the authoritative and persistent source of truth, and the low latency update allows to speed up the update latency. -![Realtime sequence](assets/realtime_sequence.png) +![Realtime sequence](/docs/assets/realtime_sequence.png) As per the diagram below, first you should -[write](<(#using-the-c-reference-library-to-read-and-write-data-files)>) the updates to a delta file +[write](#using-the-c-reference-library-to-read-and-write-data-files) the updates to a delta file that will be uploaded via a standard path later. The purpose of this step is to guarantee that this record won't be missed later. @@ -366,5 +357,5 @@ gcloud pubsub topics publish "$topic_arn" --message "$file" ### Cpp -Check out this sample [tool](../components/tools/realtime_updates_publisher.cc) on how to insert low +Check out this sample [tool](/components/tools/realtime_updates_publisher.cc) on how to insert low latency updates. diff --git a/docs/realtime_updates_capabilities.md b/docs/data_loading/realtime_updates_capabilities.md similarity index 95% rename from docs/realtime_updates_capabilities.md rename to docs/data_loading/realtime_updates_capabilities.md index bbf46ce3..85834d3a 100644 --- a/docs/realtime_updates_capabilities.md +++ b/docs/data_loading/realtime_updates_capabilities.md @@ -4,9 +4,9 @@ A parameter ([AWS](https://github.com/privacysandbox/fledge-key-value-service/blob/7f3710b1f1c944d7879718a334afd5cb8f80f3d9/production/terraform/aws/environments/kv_server.tf#L51), -[GCP](../docs/GCP_Terraform_vars.md#L96)) sets the size of the thread pool that reads off a queue. -The bigger that number is, the smaller the batch size can be. It is preferred to use a larger batch -size where possible. +[GCP](/docs/GCP_Terraform_vars.md#L96)) sets the size of the thread pool that reads off a queue. The +bigger that number is, the smaller the batch size can be. It is preferred to use a larger batch size +where possible. ### AWS @@ -46,7 +46,7 @@ While similar logic applies, the GCP SDK has superior performance due to To get to a higher QPS we can have multiple threads reading off a queue. This is a parameter ([AWS](https://github.com/privacysandbox/fledge-key-value-service/blob/7f3710b1f1c944d7879718a334afd5cb8f80f3d9/production/terraform/aws/environments/kv_server.tf#L51), -[GCP](../docs/GCP_Terraform_vars.md#L96)) that our solution exposes. It can be increased to match +[GCP](/docs/GCP_Terraform_vars.md#L96)) that our solution exposes. It can be increased to match specific QPS requirements and underlying hardware - based on the number of cores. ## Batching @@ -219,5 +219,6 @@ histogram_quantile(0.5,rate(Latency_bucket{event="ReceivedLowLatencyNotification You can query the prometheus the same way it's done for AWS. Note that KV server doesn't expose `AwsSqsReceiveMessageLatency`, and `AWS` in the metric name should be substituted with `GCP`. -You can also use the UI [dashboard](../production/terraform/gcp/realtime_pubsub_dashboard.json). -Make sure to replace PROJECT_ID and ENVIRONMENT with your values. +You can also use the UI +[dashboard](/production/terraform/gcp/dashboards/realtime_pubsub_dashboard.json). Make sure to +replace PROJECT_ID and ENVIRONMENT with your values. diff --git a/docs/deployment/deploying_locally.md b/docs/deployment/deploying_locally.md index f36276be..486090e9 100644 --- a/docs/deployment/deploying_locally.md +++ b/docs/deployment/deploying_locally.md @@ -5,13 +5,13 @@ This article is for adtech engineers who want to test the Key/Value server locally. Deploying production servers in this way is not recommended, please see the -[AWS deployment guide](deploying_on_aws.md) instead. +[AWS deployment guide](deploying_on_aws.md) or [GCP deployment guide](deploying_on_gcp.md) instead. To learn more about FLEDGE and the Key/Value server, take a look at the following documents: - [FLEDGE Key/Value server explainer](https://github.com/WICG/turtledove/blob/main/FLEDGE_Key_Value_Server_API.md) - [FLEDGE Key/Value server trust model](https://github.com/privacysandbox/fledge-docs/blob/main/key_value_service_trust_model.md) -- [FLEDGE explainer](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) +- [FLEDGE explainer](https://developers.google.com/privacy-sandbox/relevance/protected-audience) - [FLEDGE API developer guide](https://developer.chrome.com/blog/fledge-api/) > The instructions written in this document are for running a test Key/Value server that does @@ -51,7 +51,7 @@ From the Key/Value server repo folder, execute the following command: We provide a default UDF implementation that is loaded into the server at startup. -To use your own UDF, refer to the [UDF Delta file documentation](./generating_udf_files.md) to +To use your own UDF, refer to the [UDF Delta file documentation](/docs/generating_udf_files.md) to generate a UDF delta file. Include the delta file in your local delta directory (see below). @@ -73,10 +73,9 @@ their contents on startup and continue to watch them while it is running. ## Start the server ```sh -GLOG_alsologtostderr=1 GLOG_v=4 \ ./bazel-bin/components/data_server/server/server \ --delta_directory=/tmp/deltas \ - --realtime_directory=/tmp/realtime + --realtime_directory=/tmp/realtime --v=4 --stderrthreshold=0 ``` The server will start up and begin listening for new delta and realtime files in the directories diff --git a/docs/deployment/deploying_on_aws.md b/docs/deployment/deploying_on_aws.md index c9fcacca..83d0e38b 100644 --- a/docs/deployment/deploying_on_aws.md +++ b/docs/deployment/deploying_on_aws.md @@ -118,7 +118,7 @@ Take a note of the AMI ID from the output as it will be used for Terraform later We provide a default UDF implementation that is loaded into the server at startup. -To use your own UDF, refer to the [UDF Delta file documentation](./generating_udf_files.md) to +To use your own UDF, refer to the [UDF Delta file documentation](/docs/generating_udf_files.md) to generate a UDF delta file. Upload this UDF delta file to the S3 bucket that will be used for delta files before attempting to @@ -245,8 +245,8 @@ scope for this documentation. # Loading data into the server -Refer to the [FLEDGE Key/Value data loading guide documentation](./loading_data.md) for loading data -to be queried into the server. +Refer to the [FLEDGE Key/Value data loading guide documentation](/docs/data_loading/loading_data.md) +for loading data to be queried into the server. # Common operations @@ -297,7 +297,7 @@ grpcurl --protoset dist/query_api_descriptor_set.pb -d '{"raw_body": {"data": "' ## SSH into EC2 -![how a single SSH instance is used to log into multiple server instances](assets/ssh_instance.png) +![how a single SSH instance is used to log into multiple server instances](../assets/ssh_instance.png) ### Step 1: SSH into the SSH EC2 instance @@ -309,7 +309,7 @@ proceeding. We will need either the instance id (if connecting using EC2 instanc the public IP dns (if connecting using own key and SSH client) of the SSH instance and both can be retrieved from the EC2 dashboard. -![where to find the instance id or public dns for the EC2 instance](assets/ec2_instance_id_and_public_dns.png) +![where to find the instance id or public dns for the EC2 instance](../assets/ec2_instance_id_and_public_dns.png) Confirm that you can SSH into your SSH EC2 instance by following the instructions on [Connect using EC2 Instance Connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-methods.html). @@ -477,8 +477,8 @@ Alternatively, you can SSH into an existing server instance and start the Docker 1. Run the docker container ```sh - docker run -d --init --rm --env GLOG_v=5 --network host --security-opt=seccomp=unconfined \ - --entrypoint=/init_server_basic bazel/production/packaging/aws/data_server:server_docker_image -- --port 50051 + docker run -d --init --rm --network host --security-opt=seccomp=unconfined \ + --entrypoint=/init_server_basic bazel/production/packaging/aws/data_server:server_docker_image -- --port 50051 --v=5 ``` ## Viewing Telemetry @@ -537,4 +537,4 @@ The resources are allocated by specifying the per-TEE values in the terraform va ## How is private communication configured? -See this [doc](private_communication_aws.md) for more details. +See this [doc](/docs/private_communication_aws.md) for more details. diff --git a/docs/deployment/deploying_on_gcp.md b/docs/deployment/deploying_on_gcp.md index b5f62035..e258f70c 100644 --- a/docs/deployment/deploying_on_gcp.md +++ b/docs/deployment/deploying_on_gcp.md @@ -149,8 +149,8 @@ deploy to, and update the `[[REGION]].tfvars.json` with Terraform variables for The description of each variable is described in [GCP Terraform Vars doc](/docs/GCP_Terraform_vars.md). -Note that variable `tls-key` and `tls-cert` are not in `[[REGION]].tfvars.json`. Please supply these -with a `secrets.auto.tfvar` file under `production/terraform/gcp/environments/`. +Note that variable `tls_key` and `tls_cert` are not in `[[REGION]].tfvars.json`. Please supply these +with a `secrets.auto.tfvars` file under `production/terraform/gcp/environments/`. Update the `[[REGION]].backend.conf`: @@ -158,6 +158,31 @@ Update the `[[REGION]].backend.conf`: [Set up GCS bucket for Terraform states](#set-up-gcs-bucket-for-terraform-states) step. - `prefix` - Set a path/to/directory to contain the Terraform state. +## Bidding and Auction services integration within the same VPC + +If you're integrating with Bidding and Auction services (B&A), you are likely going to be reusing +the same VPC (virtual private cloud) and Service Mesh (internal LB). + +Hence, you need to set these two parameters to true: `use_existing_service_mesh`, +`use_existing_vpc`. + +You also need to set these parameters to proper values: `existing_service_mesh`, `existing_vpc_id`. +Example value: + +`existing_service_mesh`: `projects/your-project/locations/global/meshes/your-mesh` +`existing_vpc_id`: `projects/your-project/global/networks/your-vpc` + +Other things to keep in mind + +- CIDR range needs to be different for each server deployment (B&A, each kv). It is specified with + `regions_cidr_blocks` +- `enable_external_traffic` can be set to false. In this case, several other terraform vars can be + [ignored](../GCP_Terraform_vars.md) +- `regions_use_existing_nat` -- note that the NAT for each region can only be set up once under + the same VPC. If a NAT has already been set up in an existing server deployment (by specifying + an empty set of the `regions_use_existing_nat`), other sever deployments in the same region(s) + need to specify the region(s) in the `regions_use_existing_nat` var. + ## Apply Terraform From the Key/Value server repo folder, run: @@ -183,13 +208,13 @@ builders/tools/terraform -chdir=production/terraform/gcp/environments apply --va At the end, to destroy all the GCP resources: ```sh -builders/tools/terraform -chdir=production/terraform/gcp/environments destroy --var-file=--var-file=${ENVIRONMENT}/${REGION}.tfvars.json +builders/tools/terraform -chdir=production/terraform/gcp/environments destroy --var-file=${ENVIRONMENT}/${REGION}.tfvars.json ``` # Loading data into the server -Refer to the [FLEDGE Key/Value data loading guide documentation](./loading_data.md) for loading data -to be queried into the server. +Refer to the [FLEDGE Key/Value data loading guide documentation](/docs/data_loading/loading_data.md) +for loading data to be queried into the server. # Common operations @@ -259,7 +284,7 @@ BODY='{ "metadata": { "hostname": "example.com" }, "partitions": [{ "id": 0, "co ### Option 2: Via service mesh In short, service mesh is an internal load balancer. So, it is only available internally within a -VPC. The Kv-server is set up with a service mesh as a backend service and we can query it internally +VPC. The KV Server is set up with a service mesh as a backend service and we can query it internally using proxyless gRPC. Normally, you would send proxyless gRPC queries from other internal servers (e.g., a bidding server). For this demo, however, we are setting up a temporary server within the same VPC and service mesh as the Kv-server for demonstration purposes. @@ -272,7 +297,7 @@ export ENVIRONMENT=your_environment ``` Then, use the following command to set up a client VM within the same VPC network as your already -deployed Kv-server. Note that you still need to manually replace `[[your_environment]]` at the end +deployed KV Server. Note that you still need to manually replace `[[your_environment]]` at the end of the script. ```sh diff --git a/production/terraform/README.md b/docs/deployment/working_with_terraform.md similarity index 71% rename from production/terraform/README.md rename to docs/deployment/working_with_terraform.md index 16d29562..32b0a8fb 100644 --- a/production/terraform/README.md +++ b/docs/deployment/working_with_terraform.md @@ -1,8 +1,9 @@ ## Directory Structure -### AWS +### AWS/GCP -The root directory for Terraform templates is `${project_root}/production/terraform/aws`. +The root directory for Terraform templates is `${project_root}/production/terraform/${platform}`, +where `${platform}` is eigher `aws` or `gcp`. Within the root directory, the templates are organized as follows: @@ -16,4 +17,5 @@ Within the root directory, the templates are organized as follows: ### Usage Terraform operations should be performed on a particular environment. See the -[environment guide](/production/terraform/aws/environments/README.md) for actual usage. +[AWS environment guide](/production/terraform/aws/environments/README.md) and +[GCP environment guide](/production/terraform/gcp/environments/README.md) for actual usage. diff --git a/docs/developing_the_server.md b/docs/developing_the_server.md index f7d66502..ad636737 100644 --- a/docs/developing_the_server.md +++ b/docs/developing_the_server.md @@ -3,7 +3,7 @@ # FLEDGE K/V Server developer guide -## Data Server +## Develop and run the server for AWS platform in your local machine The data server provides the read API for the KV service. @@ -11,7 +11,7 @@ The data server provides the read API for the KV service. > Attention: The server can run locally (in or outside of Docker) while specifying `aws` as platform, in which case it will > contact AWS based on the local AWS credentials. However, this requires the AWS environment to be -> set up first following the [AWS deployment guide](/docs/deploying_on_aws.md). You might need to +> set up first following the [AWS deployment guide](/docs/deployment/deploying_on_aws.md). You might need to > set up the following parameters in the AWS System Manager: > > | Parameter Name | Value | @@ -76,7 +76,7 @@ The data server provides the read API for the KV service. 1. Run the container. Port 50051 can be used to query the server directly through gRPC. --environment must be specified. The server will still read data from S3 and the server uses environment to find the S3 bucket. The environment is configured as part of the - [AWS deployment process](/docs/deploying_on_aws.md). + [AWS deployment process](/docs/deployment/deploying_on_aws.md). Set region. The region should be where your environment is deployed: @@ -137,12 +137,65 @@ grpc_cli call localhost:50051 kv_server.v1.KeyValueService.GetValues \ curl http://localhost:51052/v1/getvalues?kv_internal=hi ``` +## Develop and run the server for GCP platform in your local machine + +The server can run locally while specifying `gcp` as platform. However, certain GCP resources (such +as parameters, GCS data bucket) are still required and please follow +[GCP deployment guide](/docs/deployment/deploying_on_gcp.md) to set up the GCP environment first. + +### Run the server locally inside a docker container + +#### Build the image + +From the kv-server repo folder, execute the following command + +```sh +builders/tools/bazel-debian run //production/packaging/gcp/data_server:copy_to_dist --config=local_instance --config=gcp_platform +``` + +#### Load the image into docker + +```sh +docker load -i dist/server_docker_image.tar +``` + +#### Start the server + +```sh +docker run --init -v "$HOME/.config/gcloud/application_default_credentials.json":/root/.config/gcloud/application_default_credentials.json:ro --network host --add-host=host.docker.internal:host-gateway --privileged --rm bazel/production/packaging/gcp/data_server:server_docker_image --gcp_project_id=${GCP_PROJECT_ID} --environment=${GCP_ENVIRONMENT} +``` + +where `${GCP_PROJECT_ID}` is your GCP project_id and `${GCP_ENVIRONMENT}` is the environment name +for your GCP resources. + +### Interact with the server + +- If the parameter `enable_external_traffic` (Terraform variable) is set to true, we can query the + server via the envoy port: + +```sh +./grpcurl -insecure -d '{"kv_internal":"hi"}' localhost:51052 kv_server.v1.KeyValueService.GetValues +``` + +- Alternatively, if `enable_external_traffic` is false, we can directly query the server port: + +```sh +./grpcurl -plaintext -d '{"kv_internal":"hi"}' localhost:50051 kv_server.v1.KeyValueService.GetValues +``` + +Note that you may need to set the path to your `grpcurl` tool, or install `grpcurl` if you haven't +done so already + +```sh +curl -L https://github.com/fullstorydev/grpcurl/releases/download/v1.8.1/grpcurl_1.8.1_linux_x86_64.tar.gz | tar -xz +``` + ## Develop and run the server inside AWS enclave The KV service instance should be set up by following the deployment guide -([AWS](/docs/deploying_on_aws.md)). For faster iteration, enclave image of the server is also -produced under `dist/`. Once the system has been started, iterating on changes to the server itself -only requires restarting the enclave image: +([AWS](/docs/deployment/deploying_on_aws.md)). For faster iteration, enclave image of the server is +also produced under `dist/`. Once the system has been started, iterating on changes to the server +itself only requires restarting the enclave image: 1. Copy the new enclave EIF to an AWS EC2 instance that supports nitro enclave. Note: The system has a SSH instance that a developer can access. From there the user can access actual server EC2 diff --git a/docs/generating_udf_files.md b/docs/generating_udf_files.md index b242098f..e714f6f9 100644 --- a/docs/generating_udf_files.md +++ b/docs/generating_udf_files.md @@ -47,10 +47,11 @@ Tools to generate UDF delta files and test them are in the `tools/udf` directory 1. Build the executables: ```sh - -$ builders/tools/bazel-debian run //production/packaging/tools:copy_to_dist_udf + -$ builders/tools/bazel-debian build -c opt //tools/udf/udf_generator:udf_delta_file_generator ``` -2. Generate a UDF delta file using the `dist/debian/udf_delta_file_generator` executable. +2. Generate a UDF delta file using the `bazel-bin/tools/udf/udf_generator/udf_delta_file_generator` + executable. Flags: @@ -63,13 +64,13 @@ Tools to generate UDF delta files and test them are in the `tools/udf` directory Example: ```sh - -$ dist/debian/udf_delta_file_generator --output_dir="$PWD" --udf_file_path="path/to/my/udf/udf.js" + -$ bazel-bin/tools/udf/udf_generator/udf_delta_file_generator --output_dir="$PWD" --udf_file_path="path/to/my/udf/udf.js" ``` ### Option 2. Generating your own delta file You can use other options to generate delta files, e.g. using the -[`data_cli` tool](./loading_data.md). +[`data_cli` tool](/docs/data_loading/loading_data.md). The delta file must have a `DataRecord` with a `UserDefinedFunctionsConfig` as its record. diff --git a/docs/inline_wasm_udfs.md b/docs/inline_wasm_udfs.md index 6fde4d48..6d9a35e4 100644 --- a/docs/inline_wasm_udfs.md +++ b/docs/inline_wasm_udfs.md @@ -187,7 +187,7 @@ To test the UDF delta file, use the provided UDF tools. ```sh UDF_DELTA=path/to/udf/delta - GLOG_v=10 dist/debian/udf_delta_file_tester --input_arguments="$TEST_KEY" --kv_delta_file_path="$KV_DELTA" --udf_delta_file_path="$UDF_DELTA" + dist/debian/udf_delta_file_tester --input_arguments="$TEST_KEY" --kv_delta_file_path="$KV_DELTA" --udf_delta_file_path="$UDF_DELTA" --v=10 ``` See the [generating UDF files doc](./generating_udf_files.md#3-test-the-udf-delta-file) for more diff --git a/docs/profiling_the_server.md b/docs/profiling_the_server.md index 25140ec6..797f349e 100644 --- a/docs/profiling_the_server.md +++ b/docs/profiling_the_server.md @@ -53,7 +53,6 @@ docker run \ --env CPUPROFILE=/data/profiles/server.cpu.prof \ --env CPUPROFILESIGNAL=12 \ --env LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \ - --env GLOG_logtostder=1 \ --volume=/data:/data \ --network=host \ --security-opt=seccomp=unconfined \ @@ -61,7 +60,8 @@ docker run \ --cpus=4 \ --entrypoint=/server \ bazel/production/packaging/local/data_server:server_profiling_docker_image \ - --port 50051 -delta_directory=/data --realtime_directory=/data/realtime + --port 50051 -delta_directory=/data --realtime_directory=/data/realtime \ + --stderrthreshold=0 ``` **STEP 2:** Run the following command to profile the server for 10 seconds and generate a CPU @@ -106,7 +106,6 @@ docker run \ --env HEAPPROFILE=/data/profiles/server.heap.hprof \ --env CPUPROFILESIGNAL=12 \ --env LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc.so \ - --env GLOG_logtostder=1 \ --volume=/data:/data \ --network=host \ --security-opt=seccomp=unconfined \ @@ -114,7 +113,8 @@ docker run \ --cpus=4 \ --entrypoint=/server \ bazel/production/packaging/local/data_server:server_profiling_docker_image \ - --port 50051 -delta_directory=/data --realtime_directory=/data/realtime + --port 50051 -delta_directory=/data --realtime_directory=/data/realtime \ + --stderrthreshold=0 ``` The command above may generate a number of heap profiles during startup depending on how much data @@ -185,7 +185,6 @@ List of pre-defined events (to be used in -e): ```bash docker run \ -it --init --rm --name=server-profiling-container \ - --env GLOG_logtostder=1 \ --volume=/data:/data \ --network=host \ --security-opt=seccomp=unconfined \ @@ -193,7 +192,8 @@ docker run \ --cpus=4 \ --entrypoint=/server \ bazel/production/packaging/local/data_server:server_profiling_docker_image \ - --port 50051 -delta_directory=/data --realtime_directory=/data/realtime + --port 50051 -delta_directory=/data --realtime_directory=/data/realtime \ + --stderrthreshold=0 ``` **STEP 2:** Run the following command to grap the PID of the server process running inside the diff --git a/docs/protected_app_signals/ad_retrieval_overview.md b/docs/protected_app_signals/ad_retrieval_overview.md new file mode 100644 index 00000000..ddfef42f --- /dev/null +++ b/docs/protected_app_signals/ad_retrieval_overview.md @@ -0,0 +1,407 @@ +## Background + +This document provides a detailed overview of the Ad Retrieval server, which is a server-side +component of the Protected App Signals (PAS) API. The Ad Retrieval server must run within a trusted +execution environment (TEE). + +The PAS API flow at a high level is: + +1. Ad tech companies would curate signals from a variety of sources, including first-party data, + contextual data, and third-party data. +1. The signals would be stored securely on the device. +1. Ad tech companies would have access to the signals during a Protected Auction to serve relevant + ads. +1. **Ad tech custom logic deployed in Trusted execution environments can access the signals to do + real-time ad retrieval and bidding** +1. Buyers submit ads with bids to sellers for final scoring to choose a winning ad to render. + +The Ad retrieval service is a proposed solution for performing ad and creative targeting, as listed +in the step #4. In general, ad retrieval typically includes ads matching, filtering, scoring, +ranking and top-k selection. It is developed based on the Trusted Execution Environment (TEE) on +selected public cloud platforms. + +Today, the ad retrieval service is implemented as part of the Privacy Sandbox's TEE key value +service. The privacy characteristics conform to +[the KV service trust model](https://github.com/privacysandbox/fledge-docs/blob/main/key_value_service_trust_model.md). + +## Use case overview + +Today ad techs curate data from multiple sources and use this data to choose relevant ads for the +user. Ad requests are typically accompanied by an AdID and publisher provided data (ex OpenRTB) +which is used to determine most relevant ads for the user based on a user profile keyed on the AdiD. + +Ad candidates are filtered down to a few relevant ones for an ad request. This is the retrieval +phase and the focus of this document. Bids are computed for the selected set of ads. Top bids are +sent to the seller who would score and pick the winning ad. The winning ad is then rendered by the +client. + +![alt_text](../assets/ad_retrieval_use_case_overview.png 'use case overview') + +#### Figure 1: High level layout of the retrieval flow + +An ad tech has up to N active ad campaigns at any given time. To choose relevant ads for each ad +retrieval request an ad tech needs to filter the ads to the top K number of ads. To do this ad techs +typically follow a multi stage filtering process that looks something like the following: + +![alt_text](../assets/ad_retrieval_filter_funnel.png 'image_tooltip') + +### Coarse-grained selection + +Ad techs filter a large number of ads (e.g., 10,000s) to a few (e.g., 1,000s) using device and +publisher provided signals, such as geo, ad slot type size, language, etc. + +### Filtering + +This filtering will further reduce the number of ads that are eligible to be shown. For example, ad +tech uses real-time signals, such as the amount of budget left for campaigns and other information, +status of the campaign (active / inactive etc.) to further reduce the number of ads from thousands +to hundreds. + +### Lightweight scoring and Top-K selection + +From the remaining ad candidates, ad techs can reduce the number of results returned by scoring, +sorting, and truncating the list to the top K results. The lightweight scores can be computed using +inputs such as the embedding vectors sent in via the request and per-ad embedding vectors stored in +the Ads dataset. The set of candidates fetched at this stage can be further narrowed down during a +bidding phase using more powerful inference capabilities. The bidding phase is outside the scope of +the retrieval service. + +Note: This is just a general overview of the ad tech selection process. The specific steps and +processes involved may vary depending on the individual ad tech implementation. + +## Ad retrieval walkthrough + +![alt_text](../assets/ad_retrieval_walkthrough.png 'walkthrough') + + + +#### Figure 2: Typical setup in one region. Components in and surrounded by purple are part of the PAS system. Components in yellow are developed by the ad tech operator, proprietary to the ad tech and may differ vastly between one ad tech and another. + + + +### How to deploy the system and load data to it + +This section describes how data is loaded into the server. + +#### Deployment + +The ad tech builds the service system by downloading the source code from the +[Github repository](https://github.com/privacysandbox/fledge-key-value-service) and following the +documentation in the repository. + +The ad tech deploys the system to a supported public cloud of their choice. At time of publication, +the system will be available on GCP and AWS. + +The service runs in the same network as the Bidding & Auction services. The Bidding & Auction +services' configuration is updated to send Ad retrieval requests to the Ad Retrieval service. + +#### Data loading + +The ad retrieval service consumes data generated from ad techs' own custom data generation systems. +The retrieval service development team anticipates that various forms of data generation systems +will be used, such as periodic batch files, constant streaming files or pub/sub: + +- For data that require low latency propagation, such as budgets, the ad tech bundles the data + into a specific format of file and pushes the data file into the Cloud's pub/sub system, which + then pushes the data to the servers. +- For data that does not require low latency propagation, the ad tech writes them into the same + format of files, uploads them to the Cloud's blob storage system such as AWS S3, and the service + will pick up the data. + +For privacy reasons, the service loads data into its RAM and serves requests from there. + +For details, see +"[Loading data into the key value server](https://team.git.corp.google.com/kiwi-air-force-eng-team/kv-server/+/refs/heads/main/docs/loading_data.md)". + +#### Data model + +The dataset in the server RAM is organized as simple key value pairs. The key is a string and value +can either be a string or a set of strings. + +##### Design pattern: Ad hierarchy + +The data model being simple KV pairs does not mean all data must be flat. The ad tech may use a +hierarchical model such as Campaign -> Ad Group -> Creative. The ad tech can use these as key prefix +to represent "tables" such as `"campaign_123_adgroup"`. + +At request processing time, it is up to the ad tech to perform "joins". The ad tech can first look +up certain campaigns, then look up ad groups for those campaigns. This allows ad techs to store +campaign-level information only once in the campaign entry. + +##### Design pattern: use "set" to represent a group of values, such as ads or ad groups that share a certain feature + +There are 2 categories of values: singular values and sets. + +- Singular values are the classic building blocks of key value lookups. The server can load rows + of data where each row is a key value pair and the value is a singular value. It is the most + versatile way to model the data. +- Sets are supported to optimize common operations used in ad matching, which includes union, + intersection and difference. During data loading, each row can specify which type the row uses. + In this case the key value pair is a key and a set of values. + +A typical use case of sets is to have the key be a feature (or the lack of such feature) and its set +the group of ads/campaigns/etc that satisfies this feature. During request processing, the request +may be looking for ads that have M features and do not have N features. The user-defined retrieval +logic (described below) would perform an intersection and difference of the ads of the features. +Using sets and the accompanying RunQuery API (described below) would save the user the effort of +implementing this. + +##### Design pattern: versioning + +Cross-key transactions are not supported. A mutation to one key value pair is independent of other +updates. If a collection of ads metadata requires query integrity, such that the queried ads +metadata of certain campaign or customer must be from the same data snapshot (This only applies to +when the upstream data generation is periodic batch based), one way to ensure it is to append a +per-snapshot version to each key, and have a specific key/value pair for the version. During +lookups, the version key is first queried so the version can be used to construct the actual ad +metadata query. + +The ad tech defines the order of mutations by adding a logical timestamp to each mutation. The +service accepts/evicts records based on the timestamp but there is no guarantee that the service +receives the mutations in chronological order. + +### Processing requests with ad tech-specific User Defined Functions + +This phase explains the components 2 ("input signals") and 3 ("retrieval logic") in +[Figure 1](#figure-1-high-level-layout-of-the-retrieval-flow) + +Ad techs cannot change the server code inside TEE. But the server provides a flexible way to execute +custom logic through "User defined functions" ("UDF"). Today, UDF supports JavaScript or WebAssembly +(WASM) which supports many languages. + +A UDF is not a single function. Similar to running a JavaScript on a web page load where it can call +other dependency libraries during the execution, a UDF entrypoint is invoked by the server on each +retrieval request and it can call other functions possibly defined in other files and make use of +all the language constructs like polymorphism, templating, etc. A UDF refers to the collection of +all the code that will run inside the server. + +The ad tech writes the UDF code in their preferred way, converts it into a file with specific format +recognizable by the service, and stores it in a Cloud blob storage location. The service monitors +the location and picks up the UDF as it appears. + +There is one UDF invocation per request (one request per auction). + +#### UDF API + +In the initial version, the server uses a JavaScript wrapper layer even if WASM is used. So for +`byte[]` inputs, the input will be a base64 encoded string. + +```javascript +string HandleRequest( + requestMetadata, + protectedSignals, + deviceMetadata, + contextualSignals, + contextualAdIds, +) +``` + +- requestMetadata: JSON. Per-request server metadata to the UDF. Empty for now. +- protectedSignals: arbitrary string, originating from the device, and passed from the bidding + service. The Protected App Signals can be decoded in the UDF to produce embeddings for top K ads + ranking, and other information useful for the retrieval. Note: at this stage the Protected App + Signals would be decoded and unencrypted. +- deviceMetadata: JSON object containing device metadata forwarded by the Seller's Ad Service. See + the + [B&A documentation](https://github.com/privacysandbox/fledge-docs/blob/main/bidding_auction_services_api.md#metadata-forwarded-by-sellers-ad-service) + for further details. + - `X-Accept-Language`: language used on the device. + - `X-User-Agent`: User Agent used on the device. + - `X-BnA-Client-IP`: Device IP address. + - Example: + +```javascript +{ + "X-Accept-Language": "en-US", + "X-User-Agent": "ExampleAgent", + "X-BnA-Client-IP": "1.1.1.1" +} +``` + +- [contextualSignals:](https://github.com/privacysandbox/bidding-auction-servers/blob/b222e359f09de60f0994090f7a57aa796e927345/api/bidding_auction_servers.proto#L945) + arbitrary string originated from the contextual bidding server operated by the same DSP. The UDF + is expected to be able to decode the string and use it. Contextual Signals may contain any + information such as ML model version information for the protected embedding passed in via + Protected App Signals. +- contextualAdIds: JSON object containing an optional list of ad ids. +- Output: string. This will be sent back to the bidding service and passed into the `generateBid` + function for bid generation. + +![alt_text](../assets/ad_retrieval_udf.png 'ad_retrieval_udf') + +#### Figure 3: Zoomed-in view of request path. + +#### API available to UDF + +While the server invokes the UDF to process requests, the UDF has access to a few APIs provided by +the server to assist certain operations. The usage will be explained concretely in the example +below. + +- `getValues([key_strings])`: Given a list of keys, performs lookups in the loaded dataset and + returns a list of values corresponding to the keys. +- `runQuery(query_string)`: UDF can construct a query to perform set operations, such as union, + intersection and difference. The query uses keys to represent the sets. The keys are defined as + the sets are loaded into the dataset. See the exact grammar + [here](https://github.com/privacysandbox/fledge-key-value-service/blob/main/components/query/parser.yy). + +For more information, see +[the UDF spec](https://github.com/privacysandbox/fledge-docs/blob/main/key_value_service_user_defined_functions.md). + +#### Example + +The server is loaded with a large amount of ad candidates and their associated metadata. In this +case we limit the metadata to an embedding and a budget. The embedding is a vector of floats and the +budget is an integer USD cents. + +We also have loaded the server with a mapping for each Protected App Signals and Device Metadata to +every ad that is associated with it (ex. `Games->[ad_group1, ad_group2, ad_group3,...]`) + +The Protected App Signals contains a list of the of apps that have store signals on the device by +the adtech. As a reminder signals are only availbe to teh adtech whom stored the signals.. + +Ad tech writes the UDF for example in C++. They can use all the C++ primitives such as classes, +polymorphism, templates, etc. They use a compiler to compile the code into WASM. The entrypoint of +the code has access to the input. + +The UDF goes through multiple stages of logic. + +#### Stage 1: Ad matching + +The ad tech wants to find some ads related to the types of apps installed on a device, in this +example "games" and "news". This can be done by first looking up the mapping, finding the list of +ads associated with games and news then performing an intersection of the 2 lists. + +The service provides a few APIs to UDFs to access loaded data. The above operation can be done by +using the "RunQuery" API. The ad tech constructs a query to find the intersection of ad groups +associated with certain app types: `"games & news"` and calls `RunQuery("games & news")` which +returns `["ad group1", "ad group3"]`. + +##### Design pattern: Campaign liveness + +The ad tech wants to exclude certain ad groups from bidding if budget disallows. They model this +with a key/value pair where the key is "`disabled_adgroups`" and the value is a list of ad groups +`["ad group123", "ad group789", ...]`. + +The ad tech updates this key with the low latency pub/sub mechanism whenever an ad group encounters +a budget issue. During ad matching, the ad tech performs a difference in its query to exclude these +ad groups, e.g., `RunQuery("(games & news) - disabled_adgroups")`. + +#### Stage 2: Filtering + +A large set of ad candidates is produced by the matching stage. These candidates are then fed into a +filtering stage, which can contain any number of filters. One example of a filter is negative +filtering, which is used to prevent an app-install ad from being shown for an app that is already +installed on the device. + +The device can store the information of "apps already installed on the device" as the protected app +signals sent to the ad retrieval server. The specific design of the information can take various +forms, such as a collection of hashes of the package names of the apps. + +If the filter computes that the corresponding app of an ad candidate exists on the device, the ad +candidate is dropped. + +#### User constraints + +If the dataset is large, the ad tech would need to enable sharding to store it in multiple clusters +of machines. RunQuery and GetValues APIs would perform remote calls which are slower than local +executions when the dataset is small. + +### Scoring and Top-K selection (Last UDF stage) + +This operation corresponds to the previously mentioned lightweight scoring and Top-K selection, +which happens after the ad matching and filtering. + +#### Design pattern: Dot product selection + +Suppose the ad tech wants to perform dot-product based scoring. + +After filtering, the UDF has a relatively small amount of ads and their metadata. + +The UDF logic uses GetValues API to query the ad embeddings of these ads, previously loaded into the +system through earlier phases. + +In the request input there are the protected user embeddings generated by the Bidding Service using +Protected App Signals. The UDF could perform a dot product between user and contextual embedding and +an ad embedding that would come from the retrieval data to rank the results and select top K ads. + +The UDF may perform another GetValues call on the K ads to look up additional metadata. The UDF then +aggregates them and constructs the final response according to the UDF return value format and +returns it. The service takes the return value and returns it to the calling Bidding service, which +performs further bid generation logic. + +#### Design pattern: versioning of embeddings + +The ad tech should specify which version of model/embedding should be used so in the case they are +using +[model factorization](https://developer.android.com/design-for-safety/privacy-sandbox/protected-app-signals#model-factorization) +the ad embeddings and user embeddings' versions match. The version can be passed in as part of the +per_buyer_signals in the request. In the dataset, the embeddings' keys have a version. The UDF +constructs the keys by conforming to the naming convention defined by the ad tech. For example the +version in the request may be `"5"` and the UDF can build a key: `"ad_123_embedding_v5"`. + +#### Design pattern: linear regression ML selection + +Instead of a dot product, the ad tech may link some ML libraries with their UDF and perform ML-based +selection. While in general this follows the same flow as the dot-product, there are multiple +constraints for this approach. + +#### User constraints: + +The retrieval server does not provide additional ML support on the server framework level. I.e., all +ML operations can only be performed within the UDF space. If WASM has constraints supporting e.g., +Tensorflow, it will be constraints of the retrieval service. There is also no accelerator. + +Same as data and UDF, the ML model must be looked up before using it. The lookup may be a remote +call. And the model needs to be small to not contend with other RAM usage, which is prominent in the +retrieval server. + +Data loading has propagation delay. It may take minutes for every replica of the retrieval server to +have key/value pairs of a certain version. When setting the version in the request, it is better to +not set the latest version right away lest the lookups fail. This applies in general to all lookups. +However, the server will provide visibility to the state of the data loading. + +### Ad retrieval output + +The UDF should return a string on success. The string is returned to the bidding server which then +passes it to the `generateBid()` function. Although the string can just be a simple string, most +likely the string should be a serialized object whose schema is defined by each ad tech on their +own. There is no constraint on the schema as long as the ad tech's `generateBid()` logic can +recognize and use the string. + +The reason that the string likely should be a serialized object is we expect that the use case would +require the ad retrieval server output to contain a dictionary of ad candidates and their +information. The ad candidates can be keyed by certain unique ids and the information may contain +the ad embeddings, and various pieces to construct the final rendering URL, such as the available +sizes of the ad. The `generateBid()` function may then generate the final render URL +`https://example.com/ads/4/123?w=1200&h=628` using contextual information. + +Example schema: + +```javascript +{ // Ad candidates and their information + "123": { + "campaign": 4, + "sizes": [[1200, 628], [1200, 1500]], + "embeddings": "ZXhhbXBsZSBlbWJlZGRpbmc=", + "rank": 1 + }, + "456": { + "campaign": 6, + "sizes": [[1200, 628], [1200, 1500]], + "embeddings": "YW5vdGhlciBleGFtcGxlIGVtYmVkZGluZw==" + "rank": 2 + } +} +``` + +## Concrete example and specification + +The service codebase has an end-to-end example in the context of information retrieval. + +Documentation: + +- High level: +- Lower level: + +Getting started & Example: + diff --git a/docs/protected_app_signals/examples/BUILD.bazel b/docs/protected_app_signals/examples/BUILD.bazel new file mode 100644 index 00000000..7ff15a52 --- /dev/null +++ b/docs/protected_app_signals/examples/BUILD.bazel @@ -0,0 +1,63 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@bazel_skylib//rules:run_binary.bzl", "run_binary") + +package(default_visibility = [ + "//production/packaging/tools:__subpackages__", + "//tools:__subpackages__", +]) + +run_binary( + name = "generate_delta", + srcs = [ + ":ad_retrieval.csv", + ], + outs = [ + "DELTA_0000000000000001", + ], + args = [ + "format_data", + "--input_file", + "$(location :ad_retrieval.csv)", + "--input_format", + "CSV", + "--output_file", + "$(location DELTA_0000000000000001)", + "--output_format", + "DELTA", + ], + tags = ["manual"], + tool = "//tools/data_cli", +) + +run_binary( + name = "ad_retrieval_udf", + srcs = [ + ":ad_retrieval_udf.js", + ], + outs = [ + "DELTA_0000000000000002", + ], + args = [ + "--udf_file_path", + "$(location :ad_retrieval_udf.js)", + "--output_path", + "$(location DELTA_0000000000000002)", + "--logical_commit_time", + "1700000000", + ], + tags = ["manual"], + tool = "//tools/udf/udf_generator:udf_delta_file_generator", +) diff --git a/docs/protected_app_signals/examples/ad_retrieval.csv b/docs/protected_app_signals/examples/ad_retrieval.csv new file mode 100644 index 00000000..1d774d4a --- /dev/null +++ b/docs/protected_app_signals/examples/ad_retrieval.csv @@ -0,0 +1,2 @@ +key,logical_commit_time,mutation_type,value,value_type +ad_id1,1680815895468056,UPDATE,ad_id1_value,string diff --git a/docs/protected_app_signals/examples/ad_retrieval_udf.js b/docs/protected_app_signals/examples/ad_retrieval_udf.js new file mode 100644 index 00000000..389f5ecd --- /dev/null +++ b/docs/protected_app_signals/examples/ad_retrieval_udf.js @@ -0,0 +1,30 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function HandleRequest(requestMetadata, protectedSignals, deviceMetadata, contextualSignals, contextualAdIds) { + let protectedSignalsKeys = []; + const parsedProtectedSignals = JSON.parse(protectedSignals); + for (const [key, value] of Object.entries(parsedProtectedSignals)) { + protectedSignalsKeys.push(key); + } + const kv_result = JSON.parse(getValues(protectedSignalsKeys)); + if (kv_result.hasOwnProperty('kvPairs')) { + return kv_result.kvPairs; + } + const error_message = 'Error executing handle PAS:' + JSON.stringify(kv_result); + console.error(error_message); + throw new Error(error_message); +} diff --git a/docs/protected_app_signals/onboarding_dev_guide.md b/docs/protected_app_signals/onboarding_dev_guide.md new file mode 100644 index 00000000..a0b3fe2b --- /dev/null +++ b/docs/protected_app_signals/onboarding_dev_guide.md @@ -0,0 +1,210 @@ +# Protected App Signals, Ad Retrieval developer guide + +This Ad Retrieval guide is a natural extension of this PAS +[guide](https://developer.android.com/design-for-safety/privacy-sandbox/guides/protected-audience/protected-app-signals#ad-retrieval) +which assumes a BYOS set up, but it could use a TEE Ad Retrieval server instead. + +This guide provides sample retrieval and lookup usecases. To support these usecases, details on how +to set up a test environment in the same VPC as the B&A server and reuse the same mesh, upload a +sample UDF function and upload a sample Delta file are given. + +For an overview of the Protected App Signals API, read the design +[proposal.](https://developer.android.com/design-for-safety/privacy-sandbox/protected-app-signals) + +For an overview of the Key Value/ Ad Retrieval Protected App Signals, read the the following +[doc.](/docs/protected_app_signals/ad_retrieval_overview.md) + +## Deploying an Ad Retrieval server + +[GCP.](/docs/deployment/deploying_on_gcp.md) Please follow the `B&A integration within the same VPC` +section there. + +AWS support is coming later. + +## Example Setup + +This script is an extension of the example scenario listed +[here.](https://developer.android.com/design-for-safety/privacy-sandbox/guides/protected-audience/protected-app-signals#example-setup) + +Consider the following scenario: using the Protected App Signals API, an ad tech stores relevant +signals based on user app usage. In our example, signals are stored that represent in-app purchases +from several apps. During an auction, the encrypted signals are collected and passed into a +Protected Auction running in B&A. The buyer's UDFs running in B&A use the signals to fetch ad +candidates and compute a bid. + +The fetching happens by calling the Ad Retrieval server. + +### Loading data + +For privacy reasons, the server loads all necessary data in the form of files into its RAM and +serves all requests with the in-RAM dataset. It loads the existing data at startup and updates its +in-RAM dataset as new files appear. + +The files are called "Delta files", which are similar to database journal files. Each delta file has +many rows of records and each record can be an update or delete. + +The delta file type is [Riegeli](https://github.com/google/riegeli) and the record format is +[Flatbuffers](https://flatbuffers.dev/). + +Now let's add some data. There is some example data in +[examples/ad_retrieval.csv](./examples/ad_retrieval.csv). The +[build definition](./examples/BUILD.bazel) has predefined the command to use `data_cli` to generate +the data. + +```sh +./builders/tools/bazel-debian build //docs/protected_app_signals/examples:generate_delta +``` + +##### GCP + +Upload the delta file to the bucket + +```sh +export GCS_BUCKET=your-gcs-bucket-id +gsutil cp bazel-bin/docs/protected_app_signals/examples/DELTA_0000000000000001 gs://${GCS_BUCKET} +``` + +More [details](../data_loading/loading_data.md#upload-data-files-to-gcp) + +### Retrieval Path + +Note that this is a simplistic example created for an illustrative purpose. The retrieval case can +get more complicated. + +See this [example](/getting_started/examples/sample_word2vec/). The sample demonstrates how you can +query for a set of words, taking advantage of the native set query support, and sort them based on +scoring criteria defined by word similarities, using embeddings. + +### UDF + +This [script](examples/ad_retrieval_udf.js) looks up and returns values for the keys specified in +`protectedSignals`. + +```javascript +function HandleRequest(requestMetadata, protectedSignals, deviceMetadata, contextualSignals) { + let protectedSignalsKeys = []; + const parsedProtectedSignals = JSON.parse(protectedSignals); + for (const [key, value] of Object.entries(parsedProtectedSignals)) { + protectedSignalsKeys.push(key); + } + const kv_result = JSON.parse(getValues(protectedSignalsKeys)); + if (kv_result.hasOwnProperty('kvPairs')) { + return kv_result.kvPairs; + } + const error_message = 'Error executing handle PAS:' + JSON.stringify(kv_result); + console.error(error_message); + throw new Error(error_message); +} +``` + +#### Loading the UDF + +UDFs are also ingested through Delta files. Build the delta file: + +```sh +./builders/tools/bazel-debian build //docs/protected_app_signals/examples:ad_retrieval_udf +``` + +##### GCP + +Upload the delta file to the bucket + +```sh +export GCS_BUCKET=your-gcs-bucket-id +gsutil cp bazel-bin/docs/protected_app_signals/examples/DELTA_0000000000000002 gs://${GCS_BUCKET} +``` + +More [details](../data_loading/loading_data.md#upload-data-files-to-gcp) + +#### Input + +```proto +partitions { + arguments { + data { + string_value: "{\"ad_id1\":1}" + } + } + arguments { + data { + struct_value { + fields { + key: "X-Accept-Language" + value { + string_value: "en-US,en;q=0.9" + } + } + fields { + key: "X-BnA-Client-IP" + value { + string_value: "104.133.126.32" + } + } + fields { + key: "X-User-Agent" + value { + string_value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + } + } + } + } + } + arguments { + data { + string_value: "{\"h3\": \"1\"}" + } + } + arguments { + } +} +``` + +#### Output + +```proto +single_partition { + string_output: "{\"ad_id1\":{\"value\":\"ad_id1_value\"}}" +} +``` + +### Lookup Path + +The list of IDs is provided by the buyer in the contextual path. The server will lookup the data +associated with the IDs. + +No need to load a UDF here, since the default UDF for PAS usecase should handle that. + +#### Input + +```proto +metadata { + fields { + key: "is_pas" + value { + string_value: "true" + } + } +} +partitions { + arguments { + data { + list_value { + values { + string_value: "ad_id1" + } + values { + string_value: "ad_id2" + } + } + } + } +} +``` + +#### Output + +```proto +single_partition { + string_output: "{\"ad_id1\":{\"value\":\"ad_id1_value\"},\"ad_id2\":{\"status\":{\"code\":5,\"message\":\"Key not found: ad_id2\"}}}" +} +``` diff --git a/docs/integrating_with_fledge.md b/docs/protected_audience/integrating_with_fledge.md similarity index 97% rename from docs/integrating_with_fledge.md rename to docs/protected_audience/integrating_with_fledge.md index f4e51b3f..ad603585 100644 --- a/docs/integrating_with_fledge.md +++ b/docs/protected_audience/integrating_with_fledge.md @@ -51,7 +51,7 @@ navigator.joinAdInterestGroup(interestGroup, 7 * kSecsPerDay); The ad interest group properties can be inspected in DevTools. Open the **Application** tab and select **Interest Group** from the sidebar. -![inspecting bidding signals in devtools](assets/devtools_bidding_signals.png) +![inspecting bidding signals in devtools](../assets/devtools_bidding_signals.png) When the auction is executed at a later time, the browser will use the keys defined when the user was added to an interest group to query the Key/Value server. The trusted bidding signal values will @@ -177,7 +177,7 @@ scoreAd(adMetadata, bid, adSelectionConfig, sellerSignals, trustedScoringSignals # Bidding and Auction Services with Key-Value Integration -![architecture of FLEDGE services](assets/fledge_services_architecture.png) +![architecture of FLEDGE services](../assets/fledge_services_architecture.png) Bidding & Auction services is a way to allow FLEDGE computation to take place on cloud servers in a trusted execution environment, rather than running locally on a user's device. Bidding & Auction diff --git a/docs/working_with_terraform.md b/docs/working_with_terraform.md deleted file mode 120000 index 61432b3f..00000000 --- a/docs/working_with_terraform.md +++ /dev/null @@ -1 +0,0 @@ -../production/terraform/README.md \ No newline at end of file diff --git a/getting_started/examples/sample_word2vec/README.md b/getting_started/examples/sample_word2vec/README.md index 875810ca..3b1c56df 100644 --- a/getting_started/examples/sample_word2vec/README.md +++ b/getting_started/examples/sample_word2vec/README.md @@ -75,10 +75,9 @@ Build the local server: Run the local server: ```sh -GLOG_alsologtostderr=1 \ ./bazel-bin/components/data_server/server/server \ --delta_directory=/tmp/deltas \ - --realtime_directory=/tmp/realtime + --realtime_directory=/tmp/realtime --stderrthreshold=0 ``` ## Send a query diff --git a/getting_started/quick_start.md b/getting_started/quick_start.md index ab89db41..befbbafc 100644 --- a/getting_started/quick_start.md +++ b/getting_started/quick_start.md @@ -137,8 +137,8 @@ The delta file type is [Riegeli](https://github.com/google/riegeli) and the reco [Flatbuffers](https://flatbuffers.dev/). Now let's add some data. There is some example data in -[tools/udf/udf_tester/example_data.csv](/tools/udf/udf_tester/example_data.csv). The -[build definition](/getting_started/examples/canonical_examples/BUILD.bazel) has predefined the +[/getting_started/examples/canonical_examples/example_data.csv](/getting_started/examples/canonical_examples/example_data.csv). +The [build definition](/getting_started/examples/canonical_examples/BUILD.bazel) has predefined the command to use `data_cli` to generate the data. ```sh @@ -173,7 +173,7 @@ curl http://localhost:51052/v1/getvalues?keys=example_key } ``` -See [here](/docs/loading_data.md) for more information about data loading. +See [here](/docs/data_loading/loading_data.md) for more information about data loading. ## Use `User Defined Functions (UDF)` to process requests @@ -205,7 +205,7 @@ cp bazel-bin/getting_started/examples/canonical_examples/DELTA_0000000000000002 (Similar to the data file, the UDF file can also be generated with building the tool specified in the build target and running it with your own command line flags. See -[details](docs/generating_udf_files.md).) +[details](/docs/generating_udf_files.md).) And query: @@ -263,7 +263,7 @@ function getKeyGroupOutputs(hostname, udf_arguments) { } function HandleRequest(executionMetadata, ...udf_arguments) { - logMessage(JSON.stringify(executionMetadata)); + console.log(JSON.stringify(executionMetadata)); const keyGroupOutputs = getKeyGroupOutputs(executionMetadata.requestMetadata.hostname, udf_arguments); return {keyGroupOutputs, udfOutputApiVersion: 1}; } @@ -315,8 +315,8 @@ At this point we have looked at all the basic components. See the following spec advanced topics and features: - [Writing WebAssembly User defined functions:](/docs/inline_wasm_udfs.md) -- [Deploying on AWS](/docs/deploying_on_aws.md) -- [Deploying on GCP](/docs/deploying_on_gcp.md) -- [Sharding](/docs/sharding.md) -- [Working with Terraform](/docs/working_with_terraform.md) +- [Deploying on AWS](/docs/deployment/deploying_on_aws.md) +- [Deploying on GCP](/docs/deployment/deploying_on_gcp.md) +- [Sharding](https://github.com/privacysandbox/protected-auction-services-docs/blob/main/key_value_service_sharding.md) +- [Working with Terraform](/docs/deployment/working_with_terraform.md) - [UDF binary data API](/docs/udf_read_apis_with_binary_data.md) diff --git a/getting_started/quick_start_assets/docker-compose.yaml b/getting_started/quick_start_assets/docker-compose.yaml index bf8bbd4f..8b886131 100644 --- a/getting_started/quick_start_assets/docker-compose.yaml +++ b/getting_started/quick_start_assets/docker-compose.yaml @@ -19,11 +19,9 @@ services: context: ../.. dockerfile: getting_started/quick_start_assets/Dockerfile.server network_mode: host - environment: - - GLOG_alsologtostderr=1 - - GLOG_v=9 security_opt: - seccomp:unconfined + command: --v=9 --stderrthreshold=0 volumes: - ../../dist/deltas:/tmp/deltas diff --git a/infrastructure/testing/BUILD.bazel b/infrastructure/testing/BUILD.bazel index b7bc24d0..76ea12e2 100644 --- a/infrastructure/testing/BUILD.bazel +++ b/infrastructure/testing/BUILD.bazel @@ -22,12 +22,12 @@ cc_binary( deps = [ ":protocol_testing_helper_server_cc_grpc", "//public:constants", - "@com_github_google_glog//:glog", "@com_github_google_quiche//quiche:binary_http_unstable_api", "@com_github_google_quiche//quiche:oblivious_http_unstable_api", "@com_github_grpc_grpc//:grpc++", "@com_github_grpc_grpc//:grpc++_reflection", # for grpc_cli "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", ], ) diff --git a/infrastructure/testing/protocol_testing_helper_server.cc b/infrastructure/testing/protocol_testing_helper_server.cc index 4eac2f2f..ec136142 100644 --- a/infrastructure/testing/protocol_testing_helper_server.cc +++ b/infrastructure/testing/protocol_testing_helper_server.cc @@ -16,10 +16,10 @@ #include #include "absl/flags/flag.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" -#include "glog/logging.h" #include "grpcpp/ext/proto_server_reflection_plugin.h" #include "grpcpp/grpcpp.h" #include "infrastructure/testing/protocol_testing_helper_server.grpc.pb.h" diff --git a/production/packaging/aws/build_and_test b/production/packaging/aws/build_and_test index ff75fefe..6be00c9b 100755 --- a/production/packaging/aws/build_and_test +++ b/production/packaging/aws/build_and_test @@ -33,6 +33,8 @@ function _print_runtime() { exit ${STATUS} } +declare MODE=prod + function usage() { local exitval=${1-1} cat &>/dev/stderr < Mode can be prod or nonprod. Default: ${MODE} environment variables (all optional): WORKSPACE Set the path to the workspace (repo root) @@ -71,6 +74,11 @@ while [[ $# -gt 0 ]]; do BUILD_AND_TEST_ARGS+=("--with-tests") shift ;; + --mode) + MODE="$2" + shift + shift + ;; --verbose) BUILD_AND_TEST_ARGS+=("--verbose") set -o xtrace @@ -115,7 +123,7 @@ if ! [[ -r ${WORKSPACE}/production/packaging/build_and_test_all_in_docker && -x printf "build_and_test script not found at location: %s/production/packaging/build_and_test_all_in_docker\n" "${WORKSPACE}" &>/dev/stderr fail "build_and_test not found" fi -if ! "${WORKSPACE}"/production/packaging/build_and_test_all_in_docker "${BUILD_AND_TEST_ARGS[@]}" --instance aws; then +if ! "${WORKSPACE}"/production/packaging/build_and_test_all_in_docker "${BUILD_AND_TEST_ARGS[@]}" --instance aws --mode "${MODE}"; then fail "Failed to run build_and_test_all_in_docker" fi diff --git a/production/packaging/aws/data_server/BUILD.bazel b/production/packaging/aws/data_server/BUILD.bazel index 5c1d5dc7..b60c3563 100644 --- a/production/packaging/aws/data_server/BUILD.bazel +++ b/production/packaging/aws/data_server/BUILD.bazel @@ -12,16 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -load( - "@io_bazel_rules_docker//container:container.bzl", - "container_image", - "container_layer", -) -load("@io_bazel_rules_docker//contrib:test.bzl", "container_test") +load("@container_structure_test//:defs.bzl", "container_structure_test") +load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball") load( "@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files", + "pkg_mklink", ) load("@rules_pkg//pkg:tar.bzl", "pkg_tar") load("@rules_pkg//pkg:zip.bzl", "pkg_zip") @@ -40,7 +37,7 @@ pkg_files( pkg_files( name = "kmstool_enclave_executables", srcs = [ - "@google_privacysandbox_servers_common//scp/cc/cpio/client_providers/kms_client_provider/src/aws:kms_cli", + "@google_privacysandbox_servers_common//src/cpio/client_providers/kms_client_provider/aws:kms_cli", ], attributes = pkg_attributes(mode = "0555"), prefix = "/cpio/bin", @@ -49,16 +46,25 @@ pkg_files( pkg_files( name = "kmstool_enclave_libs", srcs = [ - "@google_privacysandbox_servers_common//scp/cc/cpio/client_providers/kms_client_provider/src/aws:libnsm_so", + "@google_privacysandbox_servers_common//src/cpio/client_providers/kms_client_provider/aws:libnsm_so", ], attributes = pkg_attributes(mode = "0444"), prefix = "/cpio/lib", ) +# Create a symlink between where kmstool_enclave_cli expects shell to be +# (/bin/sh) and where it actually is on our image (/busybox/sh). +pkg_mklink( + name = "busybox_sh_symlink", + link_name = "/bin/sh", + target = "/busybox/sh", +) + server_binaries = [ ":kmstool_enclave_executables", ":kmstool_enclave_libs", ":server_executables", + ":busybox_sh_symlink", ] pkg_zip( @@ -75,43 +81,18 @@ pkg_tar( pkg_tar( name = "libnsm-tar", srcs = [ - "@google_privacysandbox_servers_common//scp/cc/cpio/client_providers/kms_client_provider/src/aws:libnsm_so", + "@google_privacysandbox_servers_common//src/cpio/client_providers/kms_client_provider/aws:libnsm_so", ], mode = "0444", package_dir = "/cpio/lib", visibility = ["//visibility:public"], ) -container_layer( - name = "server_binary_layer", - directory = "/", - tars = [ - ":libnsm-tar", - ":server_binaries_tar", - ], -) - -# Create a symlink between where kmstool_enclave_cli expects shell to be -# (/bin/sh) and where it actually is on our image (/busybox/sh). -container_layer( - name = "kmstool_enclave_cli_layer", - symlinks = { - "/bin/sh": "/busybox/sh", - }, - tars = [ - ":libnsm-tar", - ], -) - # This image target is meant for testing running the server in an enclave using. # # See project README.md on how to run the image. -container_image( +oci_image( name = "server_docker_image", - architecture = select({ - "@platforms//cpu:arm64": "arm64", - "@platforms//cpu:x86_64": "amd64", - }), base = select({ "@platforms//cpu:arm64": "@runtime-debian-debug-nonroot-arm64//image", "@platforms//cpu:x86_64": "@runtime-debian-debug-nonroot-amd64//image", @@ -123,32 +104,34 @@ container_image( "--", # Note: these ports must correspond with those specified in envoy.yaml. "--port=50051", - # These affect PCR0, so changing these would result in the loss of - # access to private keys for decryption. - "--public_key_endpoint='https://d3gf5400xe31j1.cloudfront.net/v1alpha/publicKeys'", - "--primary_coordinator_private_key_endpoint='https://uun5qzrqvj.execute-api.us-east-1.amazonaws.com/stage/v1alpha/encryptionKeys'", - "--secondary_coordinator_private_key_endpoint='https://ddqkl8ay59.execute-api.us-east-1.amazonaws.com/stage/v1alpha/encryptionKeys'", - "--primary_coordinator_region='us-east-1'", - "--secondary_coordinator_region='us-east-1'", + # These affect PCR0, so changing these would result in the loss of ability to communicate with + # the downstream components + "--public_key_endpoint=https://publickeyservice.pa.aws.privacysandboxservices.com/.well-known/protected-auction/v1/public-keys", + "--stderrthreshold=0", ], - env = {"GLOG_logtostderr": "1"}, - layers = [ - "@google_privacysandbox_servers_common//scp/cc/aws/proxy/src:proxify_layer", + tars = [ + "@google_privacysandbox_servers_common//src/aws/proxy:libnsm_and_proxify_tar", "//production/packaging/aws/resolv:resolv_config_layer", - ":server_binary_layer", - ":kmstool_enclave_cli_layer", + ":libnsm-tar", + ":server_binaries_tar", ], ) -container_test( +oci_tarball( + name = "server_docker_tarball", + image = ":server_docker_image", + repo_tags = ["bazel/production/packaging/aws/data_server:server_docker_image"], +) + +container_structure_test( name = "structure_test", size = "medium", configs = ["test/structure.yaml"], driver = "tar", - image = ":server_docker_image", + image = ":server_docker_tarball", ) -container_test( +container_structure_test( name = "commands_test", size = "small", configs = ["test/commands.yaml"], @@ -166,14 +149,15 @@ genrule( name = "copy_to_dist", srcs = [ ":server_artifacts", - ":server_docker_image.tar", + ":server_docker_tarball", "//public/query:query_api_descriptor_set", ], outs = ["copy_to_dist.bin"], cmd_bash = """cat << EOF > '$@' mkdir -p dist/debian cp $(execpath :server_artifacts) dist/debian -cp $(execpath :server_docker_image.tar) $(execpath //public/query:query_api_descriptor_set) dist +cp $(execpath :server_docker_tarball) dist/server_docker_image.tar +cp $(execpath //public/query:query_api_descriptor_set) dist # retain previous server_docker_image.tar location as a symlink ln -rsf dist/server_docker_image.tar dist/debian/server_docker_image.tar builders/tools/normalize-dist diff --git a/production/packaging/aws/data_server/ami/BUILD.bazel b/production/packaging/aws/data_server/ami/BUILD.bazel index b64d93b4..3e294b2f 100644 --- a/production/packaging/aws/data_server/ami/BUILD.bazel +++ b/production/packaging/aws/data_server/ami/BUILD.bazel @@ -21,7 +21,7 @@ pkg_zip( "//components/aws:sqs_lambda.tar", "//production/packaging/aws/otel_collector:aws-otel-collector.rpm", "//production/packaging/aws/otel_collector:aws_otel_collector_cfg", - "@google_privacysandbox_servers_common//scp/cc/aws/proxy/src:proxy", + "@google_privacysandbox_servers_common//src/aws/proxy", ], ) diff --git a/production/packaging/aws/data_server/bin/BUILD.bazel b/production/packaging/aws/data_server/bin/BUILD.bazel index 4397d2c3..f105dfb8 100644 --- a/production/packaging/aws/data_server/bin/BUILD.bazel +++ b/production/packaging/aws/data_server/bin/BUILD.bazel @@ -7,6 +7,7 @@ package(default_visibility = [ cc_binary( name = "init_server_basic", srcs = ["init_server_main.cc"], + malloc = "@com_google_tcmalloc//tcmalloc", deps = [ "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", diff --git a/production/packaging/aws/data_server/nitro-pcr0/amd64.json b/production/packaging/aws/data_server/nitro-pcr0/amd64.json index ad2c912a..615805cc 100644 --- a/production/packaging/aws/data_server/nitro-pcr0/amd64.json +++ b/production/packaging/aws/data_server/nitro-pcr0/amd64.json @@ -1 +1 @@ -{"PCR0":"25c773b4d9cc6f2316dfd9565b2895367360db465d5519f240a5ffa3059952d3ffb72d75e2cf508bdb08652e696aaafb"} +{"PCR0":"76ece8efa6f8c5723edd64b3a0464ec72411651be7642c951c73bcbc42863a8f1c376970726109c0e718b2f0d326e39d"} diff --git a/production/packaging/aws/otel_collector/otel_collector_config.yaml b/production/packaging/aws/otel_collector/otel_collector_config.yaml index 887e5136..9c8aca32 100644 --- a/production/packaging/aws/otel_collector/otel_collector_config.yaml +++ b/production/packaging/aws/otel_collector/otel_collector_config.yaml @@ -37,14 +37,20 @@ processors: send_batch_size: 50 batch/metrics: timeout: 60s + batch/logs: + timeout: 60s exporters: + awscloudwatchlogs: + log_group_name: "kv-server-log-group" + log_stream_name: "kv-server-log-stream" awsxray: index_all_attributes: true awsemf: namespace: 'KV-Server' resource_to_telemetry_conversion: enabled: true + retain_initial_value_of_delta_metric: true prometheusremotewrite: endpoint: "https://aps-workspaces.$REGION.amazonaws.com/workspaces/$WORKSPACE_ID/api/v1/remote_write" auth: @@ -65,5 +71,8 @@ service: receivers: [otlp] processors: [batch/metrics] exporters: [prometheusremotewrite] - + logs: + receivers: [otlp] + processors: [batch/logs] + exporters: [awscloudwatchlogs] extensions: [health_check,sigv4auth] diff --git a/production/packaging/build_and_test_all_in_docker b/production/packaging/build_and_test_all_in_docker index 4ebca8fd..4cca3b85 100755 --- a/production/packaging/build_and_test_all_in_docker +++ b/production/packaging/build_and_test_all_in_docker @@ -45,6 +45,7 @@ declare -i SKIP_PRECOMMIT=0 declare -i RUN_TESTS=0 declare INSTANCE=local declare PLATFORM=aws +declare MODE=prod function usage() { local -r -i exitval=${1-1} @@ -53,6 +54,7 @@ usage: ${BASH_SOURCE[0]} --instance Instance can be local or aws. Default: ${INSTANCE} --platform Platform can be aws or local. Default: ${PLATFORM} + --mode Mode can be prod or nonprod. Default: ${MODE} --precommit-only Run precommit then exit --no-precommit Skip precommit checks --with-tests Also runs tests before building @@ -103,6 +105,11 @@ while [[ $# -gt 0 ]]; do shift shift ;; + --mode) + MODE="$2" + shift + shift + ;; --verbose) VERBOSE=1 shift @@ -112,7 +119,7 @@ while [[ $# -gt 0 ]]; do esac done -BAZEL_EXTRA_ARGS="${BAZEL_EXTRA_ARGS} --config=${INSTANCE}_instance --config=${PLATFORM}_platform" +BAZEL_EXTRA_ARGS="${BAZEL_EXTRA_ARGS} --config=${INSTANCE}_instance --config=${PLATFORM}_platform --config=${MODE}_mode" if [[ ${VERBOSE} -eq 1 ]]; then set -o xtrace @@ -135,7 +142,7 @@ set -o errexit trap _collect_logs EXIT function _collect_logs() { declare -r -i status\$? - declare -r filename=${INSTANCE}-${PLATFORM}-logs.zip + declare -r filename=${INSTANCE}-${PLATFORM}-${MODE}-logs.zip printf 'Collecting bazel logs [%s]... [status: %d]\n' \${filename@Q} \${status} &>/dev/stderr bazel ${BAZEL_STARTUP_ARGS} run ${BAZEL_EXTRA_ARGS} //:collect-logs \${filename} exit \${status} diff --git a/production/packaging/gcp/build_and_test b/production/packaging/gcp/build_and_test index 845b8378..36d7fae9 100755 --- a/production/packaging/gcp/build_and_test +++ b/production/packaging/gcp/build_and_test @@ -33,6 +33,8 @@ function _print_runtime() { exit ${STATUS} } +declare MODE=prod + function usage() { local exitval=${1-1} cat &>/dev/stderr < Mode can be prod or nonprod. Default: ${MODE} environment variables (all optional): WORKSPACE Set the path to the workspace (repo root) @@ -64,6 +67,11 @@ while [[ $# -gt 0 ]]; do BUILD_AND_TEST_ARGS+=("--with-tests") shift ;; + --mode) + MODE="$2" + shift + shift + ;; --verbose) BUILD_AND_TEST_ARGS+=("--verbose") set -o xtrace @@ -102,6 +110,6 @@ fi source "${BUILDER}" || fail "Failed to source builder.sh" printf "==== Running build_and_test_all_in_docker =====\n" -if ! "${WORKSPACE}"/production/packaging/build_and_test_all_in_docker "${BUILD_AND_TEST_ARGS[@]}" --instance gcp --platform gcp; then +if ! "${WORKSPACE}"/production/packaging/build_and_test_all_in_docker "${BUILD_AND_TEST_ARGS[@]}" --instance gcp --platform gcp --mode "${MODE}"; then fail "Failed to run build_and_test_all_in_docker" fi diff --git a/production/packaging/gcp/data_server/BUILD.bazel b/production/packaging/gcp/data_server/BUILD.bazel index c5cbc5f8..6e9e90ff 100644 --- a/production/packaging/gcp/data_server/BUILD.bazel +++ b/production/packaging/gcp/data_server/BUILD.bazel @@ -128,17 +128,12 @@ container_image( }), entrypoint = [ "/init_server_basic", - # These affect PCR0, so changing these would result in the loss of - # access to private keys for decryption. - "--public_key_endpoint='https://publickeyservice-test1.bas-kms.xyz/v1alpha/publicKeys'", - "--primary_coordinator_private_key_endpoint='https://privatekeyservice-test1.bas-kms.xyz/v1alpha/encryptionKeys'", - "--secondary_coordinator_private_key_endpoint='https://privatekeyservice-test2.bas-kms.xyz/v1alpha/encryptionKeys'", - "--primary_coordinator_region='us-central1'", - "--secondary_coordinator_region='us-central1'", + # These affect PCR0, so changing these would result in the loss of ability to communicate with + # the downstream components + "--public_key_endpoint=https://publickeyservice-a.pa-3.gcp.privacysandboxservices.com/.well-known/protected-auction/v1/public-keys", + "--stderrthreshold=0", ], env = { - "GLOG_logtostderr": "1", - "GLOG_stderrthreshold": "0", "GRPC_DNS_RESOLVER": "native", }, labels = {"tee.launch_policy.log_redirect": "always"}, diff --git a/production/packaging/gcp/data_server/bin/BUILD.bazel b/production/packaging/gcp/data_server/bin/BUILD.bazel index 0e4ca491..c8adb46f 100644 --- a/production/packaging/gcp/data_server/bin/BUILD.bazel +++ b/production/packaging/gcp/data_server/bin/BUILD.bazel @@ -11,6 +11,7 @@ cc_binary( "//:local_instance": ["-DINSTANCE_LOCAL=1"], "//conditions:default": [], }), + malloc = "@com_google_tcmalloc//tcmalloc", deps = [ "//components/cloud_config:instance_client", "//components/cloud_config:parameter_client", diff --git a/production/packaging/gcp/data_server/bin/init_server_main.cc b/production/packaging/gcp/data_server/bin/init_server_main.cc index 519a5ac1..34df2cb1 100644 --- a/production/packaging/gcp/data_server/bin/init_server_main.cc +++ b/production/packaging/gcp/data_server/bin/init_server_main.cc @@ -42,7 +42,7 @@ ABSL_FLAG(std::string, environment, "NOT_SPECIFIED", "Environment name."); // The environment flag is defined for local instance (in instance_client_local) // but not for GCP instance, hence the difference here. -void PrepareTlsKeyCertForEnvoy() { +bool PrepareTlsKeyCertForEnvoy() { // Initializes GCP platform and its parameter client. kv_server::PlatformInitializer platform_initializer; std::unique_ptr parameter_client = @@ -58,19 +58,50 @@ void PrepareTlsKeyCertForEnvoy() { "GetEnvironment", kv_server::LogMetricsNoOpCallback()); } + kv_server::ParameterFetcher parameter_fetcher(environment, *parameter_client); + if (bool enable_external_traffic = + parameter_fetcher.GetBoolParameter("enable-external-traffic"); + !enable_external_traffic) { + return false; // Envoy is not needed if we don't serve external traffic. + } + // Prepares TLS cert and key for the connection between XLB and envoy. Per // GCP's protocol, the certificate here is not verified and it's ok to use a // self-signed cert. // (https://cloud.google.com/load-balancing/docs/ssl-certificates/encryption-to-the-backends#secure-protocol-considerations) - kv_server::ParameterFetcher parameter_fetcher(environment, *parameter_client); std::string tls_key_str = parameter_fetcher.GetParameter("tls-key"); std::string tls_cert_str = parameter_fetcher.GetParameter("tls-cert"); + if (tls_key_str == "NOT_PROVIDED" || tls_cert_str == "NOT_PROVIDED") { + LOG(ERROR) << "TLS key/cert are not provided!"; + exit(1); + } std::ofstream key_file("/etc/envoy/key.pem"); std::ofstream cert_file("/etc/envoy/cert.pem"); key_file << tls_key_str; cert_file << tls_cert_str; key_file.close(); cert_file.close(); + return true; +} + +void StartKvServer(int argc, char* argv[]) { + std::vector server_exec_args = {"/server"}; + for (int i = 1; i < argc; ++i) { + server_exec_args.push_back(argv[i]); + } + server_exec_args.push_back(nullptr); + LOG(INFO) << "Starting KV-Server"; + execv(server_exec_args[0], &server_exec_args[0]); + LOG(ERROR) << "Server failure:" << std::strerror(errno); +} + +void StartEnvoy() { + LOG(INFO) << "Starting Envoy"; + std::vector envoy_exec_args = {"/usr/local/bin/envoy", "--config-path", + "/etc/envoy/envoy.yaml", "-l", "warn"}; + envoy_exec_args.push_back(nullptr); + execv(envoy_exec_args[0], &envoy_exec_args[0]); + LOG(ERROR) << "Envoy failure:" << std::strerror(errno); } int main(int argc, char* argv[]) { @@ -80,30 +111,20 @@ int main(int argc, char* argv[]) { // errors. The unrecognized flags will later be used by "/server". absl::ParseAbseilFlagsOnly(argc, argv, positional_args, unrecognized_flags); - PrepareTlsKeyCertForEnvoy(); - - // Starts Envoy and server in separate processes - if (const pid_t pid = fork(); pid == 1) { - LOG(ERROR) << "Fork failure!"; - return errno; - } else if (pid == 0) { - LOG(INFO) << "Starting Envoy"; - std::vector envoy_exec_args = { - "/usr/local/bin/envoy", "--config-path", "/etc/envoy/envoy.yaml", "-l", - "warn"}; - envoy_exec_args.push_back(nullptr); - execv(envoy_exec_args[0], &envoy_exec_args[0]); - LOG(ERROR) << "Envoy failure:" << std::strerror(errno); - } else { - sleep(5); - std::vector server_exec_args = {"/server"}; - for (int i = 1; i < argc; ++i) { - server_exec_args.push_back(argv[i]); + if (PrepareTlsKeyCertForEnvoy()) { + // Starts Envoy and server in separate processes + if (const pid_t pid = fork(); pid == 1) { + LOG(ERROR) << "Fork failure!"; + return errno; + } else if (pid == 0) { + StartEnvoy(); + } else { + sleep(5); + StartKvServer(argc, argv); } - server_exec_args.push_back(nullptr); - LOG(INFO) << "Starting KV-Server"; - execv(server_exec_args[0], &server_exec_args[0]); - LOG(ERROR) << "Server failure:" << std::strerror(errno); + } else { + // Only starts server if envoy is not needed. + StartKvServer(argc, argv); } return errno; } diff --git a/production/packaging/local/data_server/BUILD.bazel b/production/packaging/local/data_server/BUILD.bazel index 79bb3edf..c2d02cf3 100644 --- a/production/packaging/local/data_server/BUILD.bazel +++ b/production/packaging/local/data_server/BUILD.bazel @@ -55,8 +55,8 @@ container_image( "--port=50051", "--delta_directory=/data", "--realtime_directory=/data/realtime", + "--stderrthreshold=0", ], - env = {"GLOG_logtostderr": "1"}, layers = [ ":profiling_tools_layer", "//production/packaging/gcp/data_server:server_binary_layer", @@ -85,8 +85,8 @@ container_image( "--port=50051", "--delta_directory=/data", "--realtime_directory=/data/realtime", + "--stderrthreshold=0", ], - env = {"GLOG_logtostderr": "1"}, layers = [ "//production/packaging/gcp/data_server:server_binary_layer", ], diff --git a/production/packaging/tools/request_simulation/bin/start_request_simulation_system b/production/packaging/tools/request_simulation/bin/start_request_simulation_system index e11dbeb6..ecd35254 100644 --- a/production/packaging/tools/request_simulation/bin/start_request_simulation_system +++ b/production/packaging/tools/request_simulation/bin/start_request_simulation_system @@ -15,4 +15,4 @@ set -o errexit -GLOG_logtostderr=1 /request_simulation/bin/request_simulation_system_main ${EXTRA_FLAGS} +/request_simulation/bin/request_simulation_system_main ${EXTRA_FLAGS} --stderrthreshold=0 diff --git a/production/terraform/aws/environments/demo/us-east-1.tfvars.json b/production/terraform/aws/environments/demo/us-east-1.tfvars.json index 9ba17556..aa0cb895 100644 --- a/production/terraform/aws/environments/demo/us-east-1.tfvars.json +++ b/production/terraform/aws/environments/demo/us-east-1.tfvars.json @@ -1,9 +1,11 @@ { + "add_missing_keys_v1": true, "autoscaling_desired_capacity": 4, "autoscaling_max_size": 6, "autoscaling_min_size": 4, "backup_poll_frequency_secs": 300, "certificate_arn": "cert-arn", + "data_loading_blob_prefix_allowlist": ",", "data_loading_file_format": "riegeli", "data_loading_num_threads": 16, "enclave_cpu_count": 2, @@ -13,6 +15,7 @@ "healthcheck_healthy_threshold": 3, "healthcheck_interval_sec": 30, "healthcheck_unhealthy_threshold": 3, + "http_api_paths": ["/v1/*", "/v2/*", "/healthcheck"], "instance_ami_id": "ami-0000000", "instance_type": "m5.xlarge", "logging_verbosity_level": 0, @@ -21,7 +24,10 @@ "metrics_export_timeout_millis": 500, "num_shards": 1, "primary_coordinator_account_identity": "", + "primary_coordinator_private_key_endpoint": "https://privatekeyservice-a.pa-1.aws.privacysandboxservices.com/v1alpha", + "primary_coordinator_region": "us-east-1", "prometheus_service_region": "us-east-1", + "public_key_endpoint": "https://publickeyservice.staging-pa-1.aws.privacysandboxservices.com/v1alpha/publicKeys", "realtime_updater_num_threads": 4, "region": "us-east-1", "root_domain": "demo-server.com", @@ -32,11 +38,15 @@ "s3client_max_connections": 64, "s3client_max_range_bytes": 8388608, "secondary_coordinator_account_identity": "", + "secondary_coordinator_private_key_endpoint": "https://privatekeyservice-b.pa-2.aws.privacysandboxservices.com/v1alpha", + "secondary_coordinator_region": "us-east-1", "server_port": 51052, "sqs_cleanup_image_uri": "123456789.dkr.ecr.us-east-1.amazonaws.com/sqs_lambda:latest", "sqs_cleanup_schedule": "rate(6 hours)", "sqs_queue_timeout_secs": 86400, "ssh_source_cidr_blocks": ["0.0.0.0/0"], + "telemetry_config": "mode: PROD", + "udf_min_log_level": 0, "udf_num_workers": 2, "use_external_metrics_collector_endpoint": false, "use_real_coordinators": false, diff --git a/production/terraform/aws/environments/demo/us-west-1.tfvars.json b/production/terraform/aws/environments/demo/us-west-1.tfvars.json index 1b5870af..ba64ac5c 100644 --- a/production/terraform/aws/environments/demo/us-west-1.tfvars.json +++ b/production/terraform/aws/environments/demo/us-west-1.tfvars.json @@ -1,9 +1,11 @@ { + "add_missing_keys_v1": true, "autoscaling_desired_capacity": 4, "autoscaling_max_size": 6, "autoscaling_min_size": 4, "backup_poll_frequency_secs": 300, "certificate_arn": "cert-arn", + "data_loading_blob_prefix_allowlist": ",", "data_loading_file_format": "riegeli", "data_loading_num_threads": 16, "enclave_cpu_count": 2, @@ -20,7 +22,10 @@ "metrics_export_timeout_millis": 500, "num_shards": 1, "primary_coordinator_account_identity": "", + "primary_coordinator_private_key_endpoint": "https://privatekeyservice-a.pa-1.aws.privacysandboxservices.com/v1alpha", + "primary_coordinator_region": "us-east-1", "prometheus_service_region": "us-east-1", + "public_key_endpoint": "https://publickeyservice.staging-pa-1.aws.privacysandboxservices.com/v1alpha/publicKeys", "realtime_updater_num_threads": 4, "region": "us-west-1", "root_domain": "demo-server.com", @@ -30,6 +35,8 @@ "s3client_max_connections": 64, "s3client_max_range_bytes": 8388608, "secondary_coordinator_account_identity": "", + "secondary_coordinator_private_key_endpoint": "https://privatekeyservice-b.pa-2.aws.privacysandboxservices.com/v1alpha", + "secondary_coordinator_region": "us-east-1", "server_port": 51052, "sqs_cleanup_image_uri": "123456789.dkr.ecr.us-east-1.amazonaws.com/sqs_lambda:latest", "sqs_cleanup_schedule": "rate(6 hours)", diff --git a/production/terraform/aws/environments/kv_server.tf b/production/terraform/aws/environments/kv_server.tf index 8931fa89..2cc19747 100644 --- a/production/terraform/aws/environments/kv_server.tf +++ b/production/terraform/aws/environments/kv_server.tf @@ -38,6 +38,7 @@ module "kv_server" { enclave_memory_mib = var.enclave_memory_mib enclave_enable_debug_mode = var.enclave_enable_debug_mode run_server_outside_tee = var.run_server_outside_tee + add_missing_keys_v1 = var.add_missing_keys_v1 # Variables related to autoscaling and load balancing. autoscaling_desired_capacity = var.autoscaling_desired_capacity @@ -66,33 +67,42 @@ module "kv_server" { metrics_collector_endpoint = var.metrics_collector_endpoint metrics_export_interval_millis = var.metrics_export_interval_millis metrics_export_timeout_millis = var.metrics_export_timeout_millis + telemetry_config = var.telemetry_config # Variables related to prometheus service prometheus_service_region = var.prometheus_service_region prometheus_workspace_id = var.prometheus_workspace_id # Variables related to data loading. - data_loading_num_threads = var.data_loading_num_threads - s3client_max_connections = var.s3client_max_connections - s3client_max_range_bytes = var.s3client_max_range_bytes - data_loading_file_format = var.data_loading_file_format + data_loading_num_threads = var.data_loading_num_threads + s3client_max_connections = var.s3client_max_connections + s3client_max_range_bytes = var.s3client_max_range_bytes + data_loading_file_format = var.data_loading_file_format + data_loading_blob_prefix_allowlist = var.data_loading_blob_prefix_allowlist # Variables related to sharding. num_shards = var.num_shards use_sharding_key_regex = var.use_sharding_key_regex sharding_key_regex = var.sharding_key_regex - # Variables related to UDF exeuction. + # Variables related to UDF execution. udf_num_workers = var.udf_num_workers udf_timeout_millis = var.udf_timeout_millis + udf_min_log_level = var.udf_min_log_level # Variables related to coordinators - use_real_coordinators = var.use_real_coordinators - primary_coordinator_account_identity = var.primary_coordinator_account_identity - secondary_coordinator_account_identity = var.secondary_coordinator_account_identity + use_real_coordinators = var.use_real_coordinators + primary_coordinator_account_identity = var.primary_coordinator_account_identity + secondary_coordinator_account_identity = var.secondary_coordinator_account_identity + primary_coordinator_private_key_endpoint = var.primary_coordinator_private_key_endpoint + secondary_coordinator_private_key_endpoint = var.secondary_coordinator_private_key_endpoint + primary_coordinator_region = var.primary_coordinator_region + secondary_coordinator_region = var.secondary_coordinator_region + public_key_endpoint = var.public_key_endpoint # Variables related to logging logging_verbosity_level = var.logging_verbosity_level + enable_otel_logger = var.enable_otel_logger } output "kv_server_url" { diff --git a/production/terraform/aws/environments/kv_server_variables.tf b/production/terraform/aws/environments/kv_server_variables.tf index 4e4951cb..48a68fc9 100644 --- a/production/terraform/aws/environments/kv_server_variables.tf +++ b/production/terraform/aws/environments/kv_server_variables.tf @@ -156,6 +156,12 @@ variable "metrics_export_timeout_millis" { type = number } +variable "telemetry_config" { + description = "Telemetry configuration to control whether metrics are raw or noised. Options are: mode: PROD(noised metrics), mode: EXPERIMENT(raw metrics), mode: COMPARE(both raw and noised metrics), mode: OFF(no metrics)" + default = "mode: PROD" + type = string +} + variable "realtime_updater_num_threads" { description = "Number of realtime threads." type = number @@ -202,6 +208,10 @@ variable "route_v1_requests_to_v2" { type = bool } +variable "add_missing_keys_v1" { + description = "Add missing keys v1." + type = bool +} variable "use_real_coordinators" { description = "Use real coordinators." @@ -262,3 +272,46 @@ variable "udf_timeout_millis" { default = 5000 type = number } + +variable "udf_min_log_level" { + description = "Minimum log level for UDFs. Info = 0, Warn = 1, Error = 2. The UDF will only attempt to log for min_log_level and above. Default is 0(info)." + default = 0 + type = number +} + +variable "enable_otel_logger" { + description = "Whether to enable otel logger." + type = bool + default = true +} + +variable "data_loading_blob_prefix_allowlist" { + description = "A comma separated list of prefixes (i.e., directories) where data is loaded from." + default = "," + type = string +} + +variable "primary_coordinator_private_key_endpoint" { + description = "Primary coordinator private key endpoint." + type = string +} + +variable "primary_coordinator_region" { + description = "Primary coordinator region." + type = string +} + +variable "secondary_coordinator_private_key_endpoint" { + description = "Secondary coordinator private key endpoint." + type = string +} + +variable "secondary_coordinator_region" { + description = "Secondary coordinator region." + type = string +} + +variable "public_key_endpoint" { + description = "Public key endpoint. Can only be overriden in non-prod mode." + type = string +} diff --git a/production/terraform/aws/modules/kv_server/main.tf b/production/terraform/aws/modules/kv_server/main.tf index bf09cf69..63043ecb 100644 --- a/production/terraform/aws/modules/kv_server/main.tf +++ b/production/terraform/aws/modules/kv_server/main.tf @@ -141,32 +141,44 @@ module "ssh" { } module "parameter" { - source = "../../services/parameter" - service = local.service - environment = var.environment - s3_bucket_parameter_value = module.data_storage.s3_data_bucket_id - bucket_update_sns_arn_parameter_value = module.data_storage.sns_data_updates_topic_arn - realtime_sns_arn_parameter_value = module.data_storage.sns_realtime_topic_arn - backup_poll_frequency_secs_parameter_value = var.backup_poll_frequency_secs - use_external_metrics_collector_endpoint = var.use_external_metrics_collector_endpoint - metrics_collector_endpoint = var.metrics_collector_endpoint - metrics_export_interval_millis_parameter_value = var.metrics_export_interval_millis - metrics_export_timeout_millis_parameter_value = var.metrics_export_timeout_millis - realtime_updater_num_threads_parameter_value = var.realtime_updater_num_threads - data_loading_num_threads_parameter_value = var.data_loading_num_threads - s3client_max_connections_parameter_value = var.s3client_max_connections - s3client_max_range_bytes_parameter_value = var.s3client_max_range_bytes - num_shards_parameter_value = var.num_shards - udf_num_workers_parameter_value = var.udf_num_workers - udf_timeout_millis_parameter_value = var.udf_timeout_millis - route_v1_requests_to_v2_parameter_value = var.route_v1_requests_to_v2 - use_real_coordinators_parameter_value = var.use_real_coordinators - primary_coordinator_account_identity_parameter_value = var.primary_coordinator_account_identity - secondary_coordinator_account_identity_parameter_value = var.secondary_coordinator_account_identity - data_loading_file_format_parameter_value = var.data_loading_file_format - logging_verbosity_level_parameter_value = var.logging_verbosity_level - use_sharding_key_regex_parameter_value = var.use_sharding_key_regex - sharding_key_regex_parameter_value = var.sharding_key_regex + source = "../../services/parameter" + service = local.service + environment = var.environment + s3_bucket_parameter_value = module.data_storage.s3_data_bucket_id + bucket_update_sns_arn_parameter_value = module.data_storage.sns_data_updates_topic_arn + realtime_sns_arn_parameter_value = module.data_storage.sns_realtime_topic_arn + backup_poll_frequency_secs_parameter_value = var.backup_poll_frequency_secs + use_external_metrics_collector_endpoint = var.use_external_metrics_collector_endpoint + metrics_collector_endpoint = var.metrics_collector_endpoint + metrics_export_interval_millis_parameter_value = var.metrics_export_interval_millis + metrics_export_timeout_millis_parameter_value = var.metrics_export_timeout_millis + telemetry_config = var.telemetry_config + realtime_updater_num_threads_parameter_value = var.realtime_updater_num_threads + data_loading_num_threads_parameter_value = var.data_loading_num_threads + s3client_max_connections_parameter_value = var.s3client_max_connections + s3client_max_range_bytes_parameter_value = var.s3client_max_range_bytes + num_shards_parameter_value = var.num_shards + udf_num_workers_parameter_value = var.udf_num_workers + udf_timeout_millis_parameter_value = var.udf_timeout_millis + udf_min_log_level_parameter_value = var.udf_min_log_level + route_v1_requests_to_v2_parameter_value = var.route_v1_requests_to_v2 + add_missing_keys_v1_parameter_value = var.add_missing_keys_v1 + use_real_coordinators_parameter_value = var.use_real_coordinators + primary_coordinator_account_identity_parameter_value = var.primary_coordinator_account_identity + secondary_coordinator_account_identity_parameter_value = var.secondary_coordinator_account_identity + primary_coordinator_private_key_endpoint_parameter_value = var.primary_coordinator_private_key_endpoint + secondary_coordinator_private_key_endpoint_parameter_value = var.secondary_coordinator_private_key_endpoint + primary_coordinator_region_parameter_value = var.primary_coordinator_region + secondary_coordinator_region_parameter_value = var.secondary_coordinator_region + public_key_endpoint_parameter_value = var.public_key_endpoint + + + data_loading_file_format_parameter_value = var.data_loading_file_format + logging_verbosity_level_parameter_value = var.logging_verbosity_level + use_sharding_key_regex_parameter_value = var.use_sharding_key_regex + sharding_key_regex_parameter_value = var.sharding_key_regex + enable_otel_logger_parameter_value = var.enable_otel_logger + data_loading_blob_prefix_allowlist = var.data_loading_blob_prefix_allowlist } module "security_group_rules" { @@ -203,6 +215,7 @@ module "iam_role_policies" { module.parameter.use_external_metrics_collector_endpoint_arn, module.parameter.metrics_export_interval_millis_parameter_arn, module.parameter.metrics_export_timeout_millis_parameter_arn, + module.parameter.telemetry_config_parameter_arn, module.parameter.realtime_updater_num_threads_parameter_arn, module.parameter.data_loading_num_threads_parameter_arn, module.parameter.s3client_max_connections_parameter_arn, @@ -210,15 +223,24 @@ module "iam_role_policies" { module.parameter.num_shards_parameter_arn, module.parameter.udf_num_workers_parameter_arn, module.parameter.route_v1_requests_to_v2_parameter_arn, + module.parameter.add_missing_keys_v1_parameter_arn, module.parameter.data_loading_file_format_parameter_arn, module.parameter.logging_verbosity_level_parameter_arn, module.parameter.use_real_coordinators_parameter_arn, module.parameter.use_sharding_key_regex_parameter_arn, - module.parameter.udf_timeout_millis_parameter_arn] + module.parameter.udf_timeout_millis_parameter_arn, + module.parameter.udf_min_log_level_parameter_arn, + module.parameter.enable_otel_logger_parameter_arn, + module.parameter.data_loading_blob_prefix_allowlist_parameter_arn] coordinator_parameter_arns = ( var.use_real_coordinators ? [ module.parameter.primary_coordinator_account_identity_parameter_arn, - module.parameter.secondary_coordinator_account_identity_parameter_arn + module.parameter.secondary_coordinator_account_identity_parameter_arn, + module.parameter.primary_coordinator_private_key_endpoint_parameter_arn, + module.parameter.secondary_coordinator_private_key_endpoint_parameter_arn, + module.parameter.primary_coordinator_region_parameter_arn, + module.parameter.secondary_coordinator_region_parameter_arn, + module.parameter.public_key_endpoint_parameter_arn ] : [] ) metrics_collector_endpoint_arns = ( @@ -245,4 +267,5 @@ module "iam_group_policies" { module "dashboards" { source = "../../services/dashboard" environment = var.environment + region = var.region } diff --git a/production/terraform/aws/modules/kv_server/variables.tf b/production/terraform/aws/modules/kv_server/variables.tf index 4ea1b94d..39364af9 100644 --- a/production/terraform/aws/modules/kv_server/variables.tf +++ b/production/terraform/aws/modules/kv_server/variables.tf @@ -78,7 +78,7 @@ variable "autoscaling_min_size" { variable "autoscaling_wait_for_capacity_timeout" { type = string - default = "10m" + default = "20m" } variable "sqs_cleanup_image_uri" { @@ -157,6 +157,12 @@ variable "metrics_export_timeout_millis" { type = number } +variable "telemetry_config" { + description = "Telemetry configuration to control whether metrics are raw or noised. Options are: mode: PROD(noised metrics), mode: EXPERIMENT(raw metrics), mode: COMPARE(both raw and noised metrics), mode: OFF(no metrics)" + default = "mode: PROD" + type = string +} + variable "realtime_updater_num_threads" { description = "The number of threads for the realtime updater." type = number @@ -203,6 +209,11 @@ variable "route_v1_requests_to_v2" { type = bool } +variable "add_missing_keys_v1" { + description = "Add missing keys v1." + type = bool +} + variable "use_real_coordinators" { description = "Will use real coordinators. `enclave_enable_debug_mode` should be set to `false` if the attestation check is enabled for coordinators. Attestation check is enabled on all production instances, and might be disabled for testing purposes only on staging/dev environments." type = bool @@ -258,3 +269,44 @@ variable "udf_timeout_millis" { default = 5000 type = number } + +variable "udf_min_log_level" { + description = "Minimum log level for UDFs. Info = 0, Warn = 1, Error = 2. The UDF will only attempt to log for min_log_level and above. Default is 0(info)." + type = number +} + +variable "enable_otel_logger" { + description = "Whether to enable otel logger." + type = bool + default = true +} + +variable "data_loading_blob_prefix_allowlist" { + description = "A comma separated list of prefixes (i.e., directories) where data is loaded from." + type = string +} + +variable "primary_coordinator_private_key_endpoint" { + description = "Primary coordinator private key endpoint." + type = string +} + +variable "secondary_coordinator_private_key_endpoint" { + description = "Secondary coordinator private key endpoint." + type = string +} + +variable "primary_coordinator_region" { + description = "Primary coordinator region." + type = string +} + +variable "secondary_coordinator_region" { + description = "Secondary coordinator region." + type = string +} + +variable "public_key_endpoint" { + description = "Public key endpoint. Can only be overriden in non-prod mode." + type = string +} diff --git a/production/terraform/aws/services/dashboard/main.tf b/production/terraform/aws/services/dashboard/main.tf index 3858c265..76dcf871 100644 --- a/production/terraform/aws/services/dashboard/main.tf +++ b/production/terraform/aws/services/dashboard/main.tf @@ -23,223 +23,509 @@ resource "aws_cloudwatch_dashboard" "environment_dashboard" { { "widgets": [ { - "height": 9, - "width": 9, + "height": 10, + "width": 12, "y": 0, - "x": 9, + "x": 0, "type": "metric", "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"request.count\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "request.count [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 0, + "x": 12, + "type": "metric", + "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,OTelLib,deployment.environment,event,host.arch,service.instance.id,service.name,service.version,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} CacheKey', 'Average', 300)", "id": "e1", "period": 300, "label": "$${PROP('Dim.event')} $${PROP('Dim.service.instance.id')}" } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"SecureLookupRequestCount\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], - "title": "Cache hits", - "period": 300, + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, "yAxis": { "left": { - "showUnits": false, - "min": 0 + "min": 0, + "showUnits": false } - } + }, + "title": "Secure lookup request count [MEAN]" } }, { - "height": 9, - "width": 9, - "y": 0, + "height": 10, + "width": 12, + "y": 10, "x": 0, "type": "metric", "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"request.failed_count_by_status\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.error_code')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "request.failed_count_by_status [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 10, + "x": 12, + "type": "metric", + "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,OTelLib,deployment.environment,event,host.arch,service.instance.id,service.name,service.version,status,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} GetValuesSuccess', 'Average', 300)", "id": "e1", "period": 300, "label": "$${PROP('Dim.service.instance.id')}" } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"request.duration_ms\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], - "title": "GetValuesSuccess", - "period": 300, + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, "yAxis": { "left": { - "showUnits": false, - "min": 0 + "min": 0, + "showUnits": false } - } + }, + "title": "request.duration_ms [MEAN]" } }, { - "height": 9, - "width": 9, - "y": 9, + "height": 10, + "width": 12, + "y": 20, "x": 0, "type": "metric", "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,OTelLib,deployment.environment,event,host.arch,service.instance.id,service.name,service.version,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} GetValuesV1Latency', 'Average', 1)", "id": "e1", "label": "$${PROP('Dim.service.instance.id')}" } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"request.size_bytes\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "stat": "Average", - "period": 300, - "title": "GetValuesV1Latency (nanoseconds)", "yAxis": { "left": { - "label": "", - "showUnits": false, - "min": 0 + "min": 0, + "showUnits": false } - } + }, + "title": "request.size_bytes [MEAN]" } }, { + "height": 10, + "width": 12, + "y": 20, + "x": 12, "type": "metric", - "x": 9, - "y": 9, - "width": 9, - "height": 9, "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,OTelLib,deployment.environment,event,host.arch,service.instance.id,service.name,service.version,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} ConcurrentStreamRecordReader', 'Average', 300)", "id": "e1", "period": 300 } ], - [ { "expression": "SEARCH('{KV-Server,OTelLib,deployment.environment,event,host.arch,service.instance.id,service.name,service.version,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} AwsSqsReceiveMessageLatency', 'Average', 300)", "id": "e2", "period": 300 } ], - [ { "expression": "SEARCH('{KV-Server,OTelLib,deployment.environment,event,host.arch,service.instance.id,service.name,service.version,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} SeekingInputStreambuf', 'Average', 300)", "id": "e3", "period": 300 } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"response.size_bytes\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "stat": "Average", - "period": 300, - "title": "Read latency", "yAxis": { "left": { - "showUnits": false, - "min": 0 + "min": 0, + "showUnits": false + } + }, + "title": "response.size_bytes [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 30, + "x": 0, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"KVUdfRequestError\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.error_code')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false } - } + }, + "title": "Request Errors [MEAN]" } }, { + "height": 10, + "width": 12, + "y": 30, + "x": 12, "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"InternalLookupRequestError\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.error_code')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Internal Request Errors [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 40, "x": 0, - "y": 18, - "width": 9, - "height": 9, + "type": "metric", "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,Noise,deployment.environment,host.arch,label,service.instance.id,service.name,service.version,shard_number,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} total cores', 'Average', 60)", "id": "e1", "period": 60 } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"KVServerError\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.error_code')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "stat": "Average", - "period": 60, - "title": "system.cpu.total_cores[MEAN]", "yAxis": { "left": { - "showUnits": false, - "min": 0 + "min": 0, + "showUnits": false } - } + }, + "title": "Server Non-request Errors [MEAN]" } }, { + "height": 10, + "width": 12, + "y": 40, + "x": 12, "type": "metric", - "x": 9, - "y": 18, - "width": 9, - "height": 9, "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,Noise,deployment.environment,host.arch,label,service.instance.id,service.name,service.version,shard_number,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} main process utilization', 'Average', 60)", "id": "e1", "period": 60 } ], - [ { "expression": "SEARCH('{KV-Server,Noise,deployment.environment,host.arch,label,service.instance.id,service.name,service.version,shard_number,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} total utilization', 'Average', 60)", "id": "e2", "period": 60 } ], - [ { "expression": "SEARCH('{KV-Server,Noise,deployment.environment,host.arch,label,service.instance.id,service.name,service.version,shard_number,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} total load', 'Average', 60)", "id": "e3", "period": 60 } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=(\"ShardedLookupGetKeyValuesLatencyInMicros\" OR \"ShardedLookupGetKeyValueSetLatencyInMicros\" OR \"ShardedLookupRunQueryLatencyInMicros\") Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "stat": "Average", - "period": 60, - "title": "system.cpu.percent[MEAN]", "yAxis": { "left": { - "showUnits": false, - "min": 0 + "min": 0, + "showUnits": false } - } + }, + "title": "Sharded Lookup Latency Microseconds [MEAN]" } }, { + "height": 10, + "width": 12, + "y": 50, + "x": 0, "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"ShardedLookupKeyCountByShard\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.key_shard_num')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Sharded Lookup Key Count By Shard [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 50, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=(\"InternalGetKeyValuesLatencyInMicros\" OR \"InternalGetKeyValueSetLatencyInMicros\" OR \"InternalRunQueryLatencyInMicros\") Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Internal Lookup Latency Microseconds [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 60, "x": 0, - "y": 27, - "width": 9, - "height": 9, + "type": "metric", "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,Noise,deployment.environment,host.arch,label,service.instance.id,service.name,service.version,shard_number,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} system.memory.usage main process', 'Average', 60)", "id": "e1", "period": 60, "label": "$${PROP('Dim.service.instance.id')}" } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=(\"GetValuePairsLatencyInMicros\" OR \"GetKeyValueSetLatencyInMicros\") Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], + "region": "${var.region}", "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "stat": "Average", - "period": 60, - "title": "system.memory.usage_kb for main process[MEAN]", + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Cache Query Latency Microseconds [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 60, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"CacheAccessEventCount\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.cache_access')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Cache Access Event Count [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 70, + "x": 0, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} status Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.status')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Server Retryable Operation Status Count [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 70, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} data_source data_source=(NOT \"realtime\") Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.data_source')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "File Update Stats [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 80, + "x": 0, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=(\"ConcurrentStreamRecordReaderReadShardRecordsLatency\" OR \"ConcurrentStreamRecordReaderReadStreamRecordsLatency\" OR \"ConcurrentStreamRecordReaderReadByteRangeLatency\") Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Data Reader Latency Microseconds[MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 80, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=(\"UpdateKeyValueLatency\" OR \"UpdateKeyValueSetLatency\" OR \"DeleteKeyLatency\" OR \"DeleteValuesInSetLatency\" OR \"RemoveDeletedKeyLatency\") Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('MetricName')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Cache Update Latency Microseconds[MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 90, + "x": 0, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} data_source data_source=\"realtime\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Realtime Update Stats [MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 90, + "x": 12, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=${var.environment} MetricName=\"ReceivedLowLatencyNotifications\" Noise=(\"Raw\" OR \"Noised\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "region": "${var.region}", + "view": "timeSeries", + "stacked": false, + "yAxis": { + "left": { + "min": 0, + "showUnits": false + } + }, + "title": "Realtime Message Processing Latency Microseconds[MEAN]" + } + }, + { + "height": 10, + "width": 12, + "y": 100, + "x": 0, + "type": "metric", + "properties": { + "metrics": [ + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=\"${var.environment}\" Noise=(\"Raw\" OR \"Noised\") MetricName=\"system.cpu.percent\" label=(\"total utilization\" OR \"main process utilization\" OR \"total load\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.label')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] + ], + "view": "timeSeries", + "stacked": false, + "region": "${var.region}", "yAxis": { "left": { "showUnits": false, "min": 0 } - } + }, + "title": "system.cpu.percent [MEAN]" } }, { + "height": 10, + "width": 12, + "y": 100, + "x": 12, "type": "metric", - "x": 9, - "y": 27, - "width": 9, - "height": 9, "properties": { "metrics": [ - [ { "expression": "SEARCH('{KV-Server,Noise,deployment.environment,host.arch,label,service.instance.id,service.name,service.version,shard_number,telemetry.sdk.language,telemetry.sdk.name,telemetry.sdk.version} ${var.environment} system.memory.usage MemAvailable', 'Average', 60)", "id": "e1", "period": 60, "label": "$${PROP('Dim.service.instance.id')}" } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=\"${var.environment}\" Noise=(\"Raw\" OR \"Noised\") MetricName=\"system.memory.usage_kb\" label=(\"MemTotal:\" OR \"main process\")', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.label')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "stat": "Average", - "period": 60, - "title": "system.memory.usage_kb for MemAvailable[MEAN]", + "region": "${var.region}", "yAxis": { "left": { "showUnits": false, "min": 0 } - } + }, + "title": "system.memory.usage_kb [MEAN]" } }, { - "type": "metric", + "height": 10, + "width": 12, + "y": 110, "x": 0, - "y": 36, - "width": 9, - "height": 9, + "type": "metric", "properties": { "metrics": [ - [ { "expression": "SELECT COUNT(ChangeNotifierErrors) FROM SCHEMA(\"KV-Server\", Noise,\"deployment.environment\",error_code,\"host.arch\",\"service.instance.id\",\"service.name\",\"service.version\",shard_number,\"telemetry.sdk.language\",\"telemetry.sdk.name\",\"telemetry.sdk.version\") WHERE \"deployment.environment\" = '${var.environment}' GROUP BY error_code, \"service.instance.id\"", "label": "error rate", "id": "m1", "stat": "Average", "visible": false } ], - [ { "expression": "DIFF(m1)", "label": "error count", "id": "e1" } ] + [ { "expression": "REMOVE_EMPTY(SEARCH('service.name=\"kv-server\" deployment.environment=\"${var.environment}\" Noise=(\"Raw\" OR \"Noised\") MetricName=\"system.cpu.percent\" label=\"total cpu cores\"', 'Average', 60))", "id": "e1", "label": "$${PROP('Dim.Noise')} $${PROP('Dim.label')} $${PROP('Dim.service.instance.id')} $${PROP('Dim.shard_number')}" } ] ], "view": "timeSeries", "stacked": false, - "region": "us-east-1", - "title": "Change notifier error count", - "period": 60, - "stat": "Average", + "region": "${var.region}", "yAxis": { "left": { - "min": 0, - "showUnits": false + "showUnits": false, + "min": 0 } - } + }, + "title": "system.cpu.total_cores [MEAN]" } } ] diff --git a/production/terraform/aws/services/dashboard/variables.tf b/production/terraform/aws/services/dashboard/variables.tf index ec4e6179..7c46199b 100644 --- a/production/terraform/aws/services/dashboard/variables.tf +++ b/production/terraform/aws/services/dashboard/variables.tf @@ -19,3 +19,8 @@ variable "environment" { description = "Assigned environment name to group related resources." type = string } + +variable "region" { + description = "AWS region to deploy to." + type = string +} diff --git a/production/terraform/aws/services/iam_role_policies/main.tf b/production/terraform/aws/services/iam_role_policies/main.tf index cf1c5dd2..3d1d95e1 100644 --- a/production/terraform/aws/services/iam_role_policies/main.tf +++ b/production/terraform/aws/services/iam_role_policies/main.tf @@ -147,7 +147,7 @@ data "aws_iam_policy_document" "sqs_cleanup_lambda_policy_doc" { "sns:List*", "sns:Unsubscribe" ] - resources = [var.sns_data_updates_topic_arn, var.sns_realtime_topic_arn] + resources = ["*"] } statement { sid = "AllowLambdaToManageSQSQueues" diff --git a/production/terraform/aws/services/parameter/main.tf b/production/terraform/aws/services/parameter/main.tf index 58348672..7d6d6450 100644 --- a/production/terraform/aws/services/parameter/main.tf +++ b/production/terraform/aws/services/parameter/main.tf @@ -79,6 +79,13 @@ resource "aws_ssm_parameter" "metrics_export_timeout_millis_parameter" { overwrite = true } +resource "aws_ssm_parameter" "telemetry_config_parameter" { + name = "${var.service}-${var.environment}-telemetry-config" + type = "String" + value = var.telemetry_config + overwrite = true +} + resource "aws_ssm_parameter" "realtime_updater_num_threads_parameter" { name = "${var.service}-${var.environment}-realtime-updater-num-threads" type = "String" @@ -128,6 +135,13 @@ resource "aws_ssm_parameter" "route_v1_requests_to_v2_parameter" { overwrite = true } +resource "aws_ssm_parameter" "add_missing_keys_v1_parameter" { + name = "${var.service}-${var.environment}-add-missing-keys-v1" + type = "String" + value = var.add_missing_keys_v1_parameter_value + overwrite = true +} + resource "aws_ssm_parameter" "use_real_coordinators_parameter" { name = "${var.service}-${var.environment}-use-real-coordinators" type = "String" @@ -151,6 +165,46 @@ resource "aws_ssm_parameter" "secondary_coordinator_account_identity_parameter" overwrite = true } +resource "aws_ssm_parameter" "primary_coordinator_private_key_endpoint_parameter" { + count = (var.use_real_coordinators_parameter_value) ? 1 : 0 + name = "${var.service}-${var.environment}-primary-coordinator-private-key-endpoint" + type = "String" + value = var.primary_coordinator_private_key_endpoint_parameter_value + overwrite = true +} + +resource "aws_ssm_parameter" "secondary_coordinator_private_key_endpoint_parameter" { + count = (var.use_real_coordinators_parameter_value) ? 1 : 0 + name = "${var.service}-${var.environment}-primary-coordinator-region" + type = "String" + value = var.secondary_coordinator_private_key_endpoint_parameter_value + overwrite = true +} + +resource "aws_ssm_parameter" "primary_coordinator_region_parameter" { + count = (var.use_real_coordinators_parameter_value) ? 1 : 0 + name = "${var.service}-${var.environment}-secondary-coordinator-private-key-endpoint" + type = "String" + value = var.primary_coordinator_region_parameter_value + overwrite = true +} + +resource "aws_ssm_parameter" "secondary_coordinator_region_parameter" { + count = (var.use_real_coordinators_parameter_value) ? 1 : 0 + name = "${var.service}-${var.environment}-secondary-coordinator-region" + type = "String" + value = var.secondary_coordinator_region_parameter_value + overwrite = true +} + +resource "aws_ssm_parameter" "public_key_endpoint_parameter" { + count = (var.use_real_coordinators_parameter_value) ? 1 : 0 + name = "${var.service}-${var.environment}-public-key-endpoint" + type = "String" + value = var.public_key_endpoint_parameter_value + overwrite = true +} + resource "aws_ssm_parameter" "data_loading_file_format_parameter" { name = "${var.service}-${var.environment}-data-loading-file-format" type = "String" @@ -186,3 +240,24 @@ resource "aws_ssm_parameter" "udf_timeout_millis_parameter" { value = var.udf_timeout_millis_parameter_value overwrite = true } + +resource "aws_ssm_parameter" "udf_min_log_level_parameter" { + name = "${var.service}-${var.environment}-udf-min-log-level" + type = "String" + value = var.udf_min_log_level_parameter_value + overwrite = true +} + +resource "aws_ssm_parameter" "enable_otel_logger_parameter" { + name = "${var.service}-${var.environment}-enable-otel-logger" + type = "String" + value = var.enable_otel_logger_parameter_value + overwrite = true +} + +resource "aws_ssm_parameter" "data_loading_blob_prefix_allowlist" { + name = "${var.service}-${var.environment}-data-loading-blob-prefix-allowlist" + type = "String" + value = var.data_loading_blob_prefix_allowlist + overwrite = true +} diff --git a/production/terraform/aws/services/parameter/outputs.tf b/production/terraform/aws/services/parameter/outputs.tf index 970408a4..32e9aacf 100644 --- a/production/terraform/aws/services/parameter/outputs.tf +++ b/production/terraform/aws/services/parameter/outputs.tf @@ -54,6 +54,10 @@ output "metrics_export_timeout_millis_parameter_arn" { value = aws_ssm_parameter.metrics_export_timeout_millis_parameter.arn } +output "telemetry_config_parameter_arn" { + value = aws_ssm_parameter.telemetry_config_parameter.arn +} + output "realtime_updater_num_threads_parameter_arn" { value = aws_ssm_parameter.realtime_updater_num_threads_parameter.arn } @@ -82,6 +86,10 @@ output "route_v1_requests_to_v2_parameter_arn" { value = aws_ssm_parameter.route_v1_requests_to_v2_parameter.arn } +output "add_missing_keys_v1_parameter_arn" { + value = aws_ssm_parameter.add_missing_keys_v1_parameter.arn +} + output "use_real_coordinators_parameter_arn" { value = aws_ssm_parameter.use_real_coordinators_parameter.arn } @@ -94,6 +102,26 @@ output "secondary_coordinator_account_identity_parameter_arn" { value = (var.use_real_coordinators_parameter_value) ? aws_ssm_parameter.secondary_coordinator_account_identity_parameter[0].arn : "" } +output "primary_coordinator_private_key_endpoint_parameter_arn" { + value = (var.use_real_coordinators_parameter_value) ? aws_ssm_parameter.primary_coordinator_private_key_endpoint_parameter[0].arn : "" +} + +output "secondary_coordinator_private_key_endpoint_parameter_arn" { + value = (var.use_real_coordinators_parameter_value) ? aws_ssm_parameter.secondary_coordinator_private_key_endpoint_parameter[0].arn : "" +} + +output "primary_coordinator_region_parameter_arn" { + value = (var.use_real_coordinators_parameter_value) ? aws_ssm_parameter.primary_coordinator_region_parameter[0].arn : "" +} + +output "secondary_coordinator_region_parameter_arn" { + value = (var.use_real_coordinators_parameter_value) ? aws_ssm_parameter.secondary_coordinator_region_parameter[0].arn : "" +} + +output "public_key_endpoint_parameter_arn" { + value = (var.use_real_coordinators_parameter_value) ? aws_ssm_parameter.public_key_endpoint_parameter[0].arn : "" +} + output "data_loading_file_format_parameter_arn" { value = aws_ssm_parameter.data_loading_file_format_parameter.arn } @@ -113,3 +141,15 @@ output "sharding_key_regex_parameter_arn" { output "udf_timeout_millis_parameter_arn" { value = aws_ssm_parameter.udf_timeout_millis_parameter.arn } + +output "udf_min_log_level_parameter_arn" { + value = aws_ssm_parameter.udf_min_log_level_parameter.arn +} + +output "enable_otel_logger_parameter_arn" { + value = aws_ssm_parameter.enable_otel_logger_parameter.arn +} + +output "data_loading_blob_prefix_allowlist_parameter_arn" { + value = aws_ssm_parameter.data_loading_blob_prefix_allowlist.arn +} diff --git a/production/terraform/aws/services/parameter/variables.tf b/production/terraform/aws/services/parameter/variables.tf index 91d36660..32228f3b 100644 --- a/production/terraform/aws/services/parameter/variables.tf +++ b/production/terraform/aws/services/parameter/variables.tf @@ -59,6 +59,11 @@ variable "metrics_export_timeout_millis_parameter_value" { type = number } +variable "telemetry_config" { + description = "Telemetry config for exporting raw or noised metrics" + type = string +} + variable "realtime_updater_num_threads_parameter_value" { description = "Amount of realtime notifier threads." type = number @@ -94,6 +99,11 @@ variable "route_v1_requests_to_v2_parameter_value" { type = bool } +variable "add_missing_keys_v1_parameter_value" { + description = "Add missing keys v1." + type = bool +} + variable "use_real_coordinators_parameter_value" { description = "Number of parallel threads for reading and loading data files." type = bool @@ -138,3 +148,43 @@ variable "udf_timeout_millis_parameter_value" { description = "UDF execution timeout in milliseconds." type = number } + +variable "udf_min_log_level_parameter_value" { + description = "Minimum log level for UDFs. Info = 0, Warn = 1, Error = 2. The UDF will only attempt to log for min_log_level and above. Default is 0(info)." + type = number +} + +variable "enable_otel_logger_parameter_value" { + description = "Whether to enable otel logger." + type = bool +} + +variable "data_loading_blob_prefix_allowlist" { + description = "A comma separated list of prefixes (i.e., directories) where data is loaded from." + type = string +} + +variable "primary_coordinator_private_key_endpoint_parameter_value" { + description = "Primary coordinator private key endpoint." + type = string +} + +variable "secondary_coordinator_private_key_endpoint_parameter_value" { + description = "Secondary coordinator private key endpoint." + type = string +} + +variable "primary_coordinator_region_parameter_value" { + description = "Primary coordinator region." + type = string +} + +variable "secondary_coordinator_region_parameter_value" { + description = "Secondary coordinator region." + type = string +} + +variable "public_key_endpoint_parameter_value" { + description = "Public key endpoint. Can only be overriden in non-prod mode." + type = string +} diff --git a/production/terraform/aws/services/security_group_rules/main.tf b/production/terraform/aws/services/security_group_rules/main.tf index a528b9d2..efe01002 100644 --- a/production/terraform/aws/services/security_group_rules/main.tf +++ b/production/terraform/aws/services/security_group_rules/main.tf @@ -157,3 +157,12 @@ resource "aws_security_group_rule" "allow_ec2_to_ec2_endpoint_ingress" { type = "ingress" source_security_group_id = var.instances_security_group_id } + +resource "aws_security_group_rule" "allow_ec2_secure_tcp_egress" { + from_port = 443 + protocol = "TCP" + security_group_id = var.instances_security_group_id + to_port = 443 + type = "egress" + cidr_blocks = ["0.0.0.0/0"] +} diff --git a/production/terraform/gcp/environments/README.md b/production/terraform/gcp/environments/README.md new file mode 100644 index 00000000..e2f209bc --- /dev/null +++ b/production/terraform/gcp/environments/README.md @@ -0,0 +1,23 @@ +# Demo Environment + +An existing demo environment exists for the KV server and can be used as a reference for creating +your own environment (e.g. "dev" and "prod"). + +## Synopsis + +```bash +nano demo/us-east1.backend.conf # Customize backend parameters +nano demo/us-east1.tfvars.json # Customize input variables to suit your demo environment. +terraform init --backend-config=demo/us-east1.backend.conf --var-file=demo/us-east1.tfvars.json --reconfigure +terraform plan --var-file=demo/us-east1.tfvars.json +terraform apply --var-file=demo/us-east1.tfvars.json +``` + +## Configuration Files + +The files which should be modified for your purposes for each environment that you create are: + +- [us-east1.tfvars.json](demo/us-east1.tfvars.json) - an example configuration file for the KV + server. +- [us-east1.backend.conf](demo/us-east1.backend.conf) - contains terraform state bucket location - + should be edited to point to a state bucket you control. diff --git a/production/terraform/gcp/environments/demo/us-east1.tfvars.json b/production/terraform/gcp/environments/demo/us-east1.tfvars.json index b0ec981a..b0cf074d 100644 --- a/production/terraform/gcp/environments/demo/us-east1.tfvars.json +++ b/production/terraform/gcp/environments/demo/us-east1.tfvars.json @@ -1,4 +1,5 @@ { + "add_missing_keys_v1": true, "backup_poll_frequency_secs": 5, "collector_dns_zone": "your-dns-zone-name", "collector_domain_name": "your-domain-name", @@ -7,7 +8,9 @@ "collector_service_port": 4317, "cpu_utilization_percent": 0.9, "data_bucket_id": "your-delta-file-bucket", + "data_loading_blob_prefix_allowlist": ",", "data_loading_num_threads": 16, + "enable_external_traffic": true, "environment": "demo", "envoy_port": 51052, "existing_service_mesh": "", @@ -24,20 +27,29 @@ "min_replicas_per_service_region": 1, "num_shards": 1, "primary_coordinator_account_identity": "EMPTY_STRING", + "primary_coordinator_private_key_endpoint": "https://privatekeyservice-a.pa-3.gcp.privacysandboxservices.com/v1alpha/encryptionKeys", + "primary_coordinator_region": "us-central1", "primary_key_service_cloud_function_url": "EMPTY_STRING", "primary_workload_identity_pool_provider": "EMPTY_STRING", "project_id": "your-project-id", + "public_key_endpoint": "https://publickeyservice.stg-pa.gcp.pstest.dev/.well-known/protected-auction/v1/public-keys", "realtime_updater_num_threads": 1, "regions": ["us-east1"], + "regions_cidr_blocks": ["10.0.3.0/24"], + "regions_use_existing_nat": [], "route_v1_to_v2": false, "secondary_coordinator_account_identity": "EMPTY_STRING", + "secondary_coordinator_private_key_endpoint": "https://privatekeyservice.pa-4.gcp.privacysandboxservices.com/v1alpha/encryptionKeys", + "secondary_coordinator_region": "us-central1", "secondary_key_service_cloud_function_url": "EMPTY_STRING", "secondary_workload_identity_pool_provider": "EMPTY_STRING", "server_dns_zone": "your-server-dns-zone-name", "server_domain_ssl_certificate_id": "your-server-ssl-certificate-id", "server_url": "your-kv-server-url", "service_account_email": "your-service-account-email", + "service_mesh_address": "xds:///kv-service-host", "tee_impersonate_service_accounts": "", + "telemetry_config": "mode: EXPERIMENT", "udf_num_workers": 2, "use_confidential_space_debug_image": false, "use_existing_service_mesh": false, diff --git a/production/terraform/gcp/environments/kv_server.tf b/production/terraform/gcp/environments/kv_server.tf index 6c337b4e..332507de 100644 --- a/production/terraform/gcp/environments/kv_server.tf +++ b/production/terraform/gcp/environments/kv_server.tf @@ -37,6 +37,8 @@ module "kv_server" { service = local.kv_service service_account_email = var.service_account_email regions = var.regions + regions_cidr_blocks = var.regions_cidr_blocks + regions_use_existing_nat = var.regions_use_existing_nat gcp_image_tag = var.gcp_image_tag gcp_image_repo = var.gcp_image_repo kv_service_port = var.kv_service_port @@ -63,34 +65,47 @@ module "kv_server" { num_shards = var.num_shards use_existing_service_mesh = var.use_existing_service_mesh existing_service_mesh = var.existing_service_mesh + service_mesh_address = var.service_mesh_address + enable_external_traffic = var.enable_external_traffic parameters = { - data-bucket-id = var.data_bucket_id - launch-hook = "${local.kv_service}-${var.environment}-launch-hook" - use-external-metrics-collector-endpoint = var.use_external_metrics_collector_endpoint - metrics-collector-endpoint = "${var.environment}-${var.collector_service_name}.${var.collector_domain_name}:${var.collector_service_port}" - metrics-export-interval-millis = var.metrics_export_interval_millis - metrics-export-timeout-millis = var.metrics_export_timeout_millis - backup-poll-frequency-secs = var.backup_poll_frequency_secs - realtime-updater-num-threads = var.realtime_updater_num_threads - data-loading-num-threads = var.data_loading_num_threads - num-shards = var.num_shards - udf-num-workers = var.udf_num_workers - udf-timeout-millis = var.udf_timeout_millis - route-v1-to-v2 = var.route_v1_to_v2 - use-real-coordinators = var.use_real_coordinators - environment = var.environment - project-id = var.project_id - primary-key-service-cloud-function-url = var.primary_key_service_cloud_function_url - primary-workload-identity-pool-provider = var.primary_workload_identity_pool_provider - secondary-key-service-cloud-function-url = var.secondary_key_service_cloud_function_url - secondary-workload-identity-pool-provider = var.secondary_workload_identity_pool_provider - primary-coordinator-account-identity = var.primary_coordinator_account_identity - secondary-coordinator-account-identity = var.secondary_coordinator_account_identity - logging-verbosity-level = var.logging_verbosity_level - use-sharding-key-regex = var.use_sharding_key_regex - sharding-key-regex = var.sharding_key_regex - tls-key = var.tls_key - tls-cert = var.tls_cert + data-bucket-id = var.data_bucket_id + launch-hook = "${local.kv_service}-${var.environment}-launch-hook" + use-external-metrics-collector-endpoint = var.use_external_metrics_collector_endpoint + metrics-collector-endpoint = "${var.environment}-${var.collector_service_name}.${var.collector_domain_name}:${var.collector_service_port}" + metrics-export-interval-millis = var.metrics_export_interval_millis + metrics-export-timeout-millis = var.metrics_export_timeout_millis + backup-poll-frequency-secs = var.backup_poll_frequency_secs + realtime-updater-num-threads = var.realtime_updater_num_threads + data-loading-num-threads = var.data_loading_num_threads + num-shards = var.num_shards + udf-num-workers = var.udf_num_workers + udf-timeout-millis = var.udf_timeout_millis + udf-min-log-level = var.udf_min_log_level + route-v1-to-v2 = var.route_v1_to_v2 + add-missing-keys-v1 = var.add_missing_keys_v1 + use-real-coordinators = var.use_real_coordinators + environment = var.environment + project-id = var.project_id + primary-key-service-cloud-function-url = var.primary_key_service_cloud_function_url + primary-workload-identity-pool-provider = var.primary_workload_identity_pool_provider + secondary-key-service-cloud-function-url = var.secondary_key_service_cloud_function_url + secondary-workload-identity-pool-provider = var.secondary_workload_identity_pool_provider + primary-coordinator-account-identity = var.primary_coordinator_account_identity + secondary-coordinator-account-identity = var.secondary_coordinator_account_identity + primary-coordinator-private-key-endpoint = var.primary_coordinator_private_key_endpoint + primary-coordinator-region = var.primary_coordinator_region + secondary-coordinator-private-key-endpoint = var.secondary_coordinator_private_key_endpoint + secondary-coordinator-region = var.secondary_coordinator_region + public-key-endpoint = var.public_key_endpoint + logging-verbosity-level = var.logging_verbosity_level + use-sharding-key-regex = var.use_sharding_key_regex + sharding-key-regex = var.sharding_key_regex + tls-key = var.tls_key + tls-cert = var.tls_cert + enable-otel-logger = var.enable_otel_logger + enable-external-traffic = var.enable_external_traffic + telemetry-config = var.telemetry_config + data-loading-blob-prefix-allowlist = var.data_loading_blob_prefix_allowlist } } diff --git a/production/terraform/gcp/environments/kv_server_variables.tf b/production/terraform/gcp/environments/kv_server_variables.tf index 0cc6f400..8c0a8ba0 100644 --- a/production/terraform/gcp/environments/kv_server_variables.tf +++ b/production/terraform/gcp/environments/kv_server_variables.tf @@ -34,6 +34,16 @@ variable "regions" { type = set(string) } +variable "regions_cidr_blocks" { + description = "A set of CIDR ranges for all specified regions. The number of blocks here should correspond to the number of regions." + type = set(string) +} + +variable "regions_use_existing_nat" { + description = "Regions that use existing nat. No new nats will be created for regions specified here." + type = set(string) +} + variable "gcp_image_tag" { description = "Tag of the gcp docker image uploaded to the artifact registry." type = string @@ -50,32 +60,34 @@ variable "kv_service_port" { } variable "envoy_port" { - description = "External load balancer will send traffic to this port. Envoy will forward traffic to kv_service_port. Must match envoy.yaml." + description = "External load balancer will send traffic to this port. Envoy will forward traffic to kv_service_port. Must match envoy.yaml. Ignored if `enable_external_traffic` is false." type = number } variable "server_url" { - description = "Kv-serer URL. Example: kv-server-environment.example.com" + description = "Kv-serer URL. Example: kv-server-environment.example.com. Ignored if `enable_external_traffic` is false." type = string } variable "server_dns_zone" { - description = "Google Cloud Dns zone for Kv-serer." + description = "Google Cloud Dns zone for Kv-serer. Ignored if `enable_external_traffic` is false." type = string } variable "server_domain_ssl_certificate_id" { - description = "Ssl certificate id of the Kv-server URL." + description = "Ssl certificate id of the Kv-server URL. Ignored if `enable_external_traffic` is false." type = string } variable "tls_key" { - description = "TLS key. Please specify this variable in a tfvars file (e.g., secrets.auto.tfvars) under the `environments` directory." + description = "TLS key. Please specify this variable in a tfvars file (e.g., secrets.auto.tfvars) under the `environments` directory. Ignored if `enable_external_traffic` is false." + default = "NOT_PROVIDED" type = string } variable "tls_cert" { - description = "TLS cert. Please specify this variable in a tfvars file (e.g., secrets.auto.tfvars) under the `environments` directory." + description = "TLS cert. Please specify this variable in a tfvars file (e.g., secrets.auto.tfvars) under the `environments` directory. Ignored if `enable_external_traffic` is false." + default = "NOT_PROVIDED" type = string } @@ -162,11 +174,22 @@ variable "udf_timeout_millis" { description = "UDF execution timeout in milliseconds." } +variable "udf_min_log_level" { + type = number + default = 0 + description = "Minimum log level for UDFs. Info = 0, Warn = 1, Error = 2. The UDF will only attempt to log for min_log_level and above. Default is 0(info)." +} + variable "route_v1_to_v2" { type = bool description = "Whether to route V1 requests through V2." } +variable "add_missing_keys_v1" { + type = bool + description = "Add missing keys for v1." +} + variable "use_real_coordinators" { type = bool description = "Use real coordinators." @@ -274,3 +297,58 @@ variable "sharding_key_regex" { default = "EMPTY_STRING" type = string } + +variable "service_mesh_address" { + description = "Service mesh address of the KV server." + default = "xds:///kv-service-host" + type = string +} + +variable "enable_otel_logger" { + description = "Whether to use otel logger." + default = true + type = bool +} + +variable "enable_external_traffic" { + description = "Whether to serve external traffic. If disabled, only internal traffic via service mesh will be served." + default = true + type = bool +} + +variable "telemetry_config" { + description = "Telemetry configuration to control whether metrics are raw or noised. Options are: mode: PROD(noised metrics), mode: EXPERIMENT(raw metrics), mode: COMPARE(both raw and noised metrics), mode: OFF(no metrics)" + default = "mode: PROD" + type = string +} + +variable "data_loading_blob_prefix_allowlist" { + description = "A comma separated list of prefixes (i.e., directories) where data is loaded from." + default = "," + type = string +} + +variable "primary_coordinator_private_key_endpoint" { + description = "Primary coordinator private key endpoint." + type = string +} + +variable "secondary_coordinator_private_key_endpoint" { + description = "Secondary coordinator private key endpoint." + type = string +} + +variable "primary_coordinator_region" { + description = "Primary coordinator region." + type = string +} + +variable "secondary_coordinator_region" { + description = "Secondary coordinator region." + type = string +} + +variable "public_key_endpoint" { + description = "Public key endpoint. Can only be overriden in non-prod mode." + type = string +} diff --git a/production/terraform/gcp/environments/terraform.tf b/production/terraform/gcp/environments/terraform.tf index d36e1551..9d99b89e 100644 --- a/production/terraform/gcp/environments/terraform.tf +++ b/production/terraform/gcp/environments/terraform.tf @@ -25,7 +25,7 @@ terraform { google-beta = { source = "hashicorp/google-beta" - version = "5.0.0" + version = "5.12.0" } } } diff --git a/production/terraform/gcp/modules/kv_server/main.tf b/production/terraform/gcp/modules/kv_server/main.tf index b79130ed..a43531d9 100644 --- a/production/terraform/gcp/modules/kv_server/main.tf +++ b/production/terraform/gcp/modules/kv_server/main.tf @@ -14,18 +14,17 @@ * limitations under the License. */ -locals { - kv_server_address = "xds:///kv-service-host" -} - module "networking" { - source = "../../services/networking" - service = var.service - environment = var.environment - regions = var.regions - collector_service_name = var.collector_service_name - use_existing_vpc = var.use_existing_vpc - existing_vpc_id = var.existing_vpc_id + source = "../../services/networking" + service = var.service + environment = var.environment + regions = var.regions + regions_cidr_blocks = var.regions_cidr_blocks + regions_use_existing_nat = var.regions_use_existing_nat + collector_service_name = var.collector_service_name + use_existing_vpc = var.use_existing_vpc + existing_vpc_id = var.existing_vpc_id + enable_external_traffic = var.enable_external_traffic } module "security" { @@ -59,6 +58,7 @@ module "autoscaling" { parameters = var.parameters tee_impersonate_service_accounts = var.tee_impersonate_service_accounts shard_num = count.index + enable_external_traffic = var.enable_external_traffic } module "metrics_collector_autoscaling" { @@ -91,16 +91,19 @@ module "service_mesh" { service = var.service environment = var.environment service_port = var.kv_service_port - kv_server_address = local.kv_server_address + kv_server_address = var.service_mesh_address project_id = var.project_id instance_groups = flatten(module.autoscaling[*].kv_server_instance_groups) collector_forwarding_rule = module.metrics_collector.collector_forwarding_rule collector_tcp_proxy = module.metrics_collector.collector_tcp_proxy use_existing_service_mesh = var.use_existing_service_mesh existing_service_mesh = var.existing_service_mesh + enable_external_traffic = var.enable_external_traffic } module "external_load_balancing" { + count = var.enable_external_traffic ? 1 : 0 + source = "../../services/external_load_balancing" service = var.service environment = var.environment diff --git a/production/terraform/gcp/modules/kv_server/variables.tf b/production/terraform/gcp/modules/kv_server/variables.tf index 2746b1d5..bdcd0fc7 100644 --- a/production/terraform/gcp/modules/kv_server/variables.tf +++ b/production/terraform/gcp/modules/kv_server/variables.tf @@ -44,6 +44,16 @@ variable "regions" { type = set(string) } +variable "regions_cidr_blocks" { + description = "A set of CIDR ranges for all specified regions. The number of blocks here should correspond to the number of regions." + type = set(string) +} + +variable "regions_use_existing_nat" { + description = "Regions that use existing nat. No new nats will be created for regions specified here." + type = set(string) +} + variable "gcp_image_repo" { description = "A URL to a docker image repo containing the key-value service" type = string @@ -175,3 +185,15 @@ variable "server_domain_ssl_certificate_id" { description = "Ssl certificate id of the Kv-server domain." type = string } + +variable "service_mesh_address" { + description = "Service mesh address of the KV server." + default = "xds:///kv-service-host" + type = string +} + +variable "enable_external_traffic" { + description = "Whether to serve external traffic. If disabled, only internal traffic via service mesh will be served." + default = true + type = bool +} diff --git a/production/terraform/gcp/services/autoscaling/main.tf b/production/terraform/gcp/services/autoscaling/main.tf index 4d039298..3b10e1bc 100644 --- a/production/terraform/gcp/services/autoscaling/main.tf +++ b/production/terraform/gcp/services/autoscaling/main.tf @@ -24,7 +24,7 @@ resource "google_compute_instance_template" "kv_server" { for_each = var.subnets region = each.value.region - name = "${var.service}-${var.environment}-${var.shard_num}-instance-lt" + name_prefix = "${var.service}-${var.environment}-${var.shard_num}-instance-lt-" machine_type = var.machine_type tags = ["allow-hc", "allow-ssh", "allow-backend-ingress", "allow-all-egress"] @@ -81,7 +81,6 @@ resource "google_compute_instance_template" "kv_server" { lifecycle { create_before_destroy = true - ignore_changes = [name] replace_triggered_by = [null_resource.kv_parameters] } @@ -90,6 +89,8 @@ resource "google_compute_instance_template" "kv_server" { } resource "google_compute_region_instance_group_manager" "kv_server" { + provider = google-beta + for_each = google_compute_instance_template.kv_server name = "${var.service}-${var.environment}-${each.value.region}-${var.shard_num}-mig" region = each.value.region @@ -103,9 +104,12 @@ resource "google_compute_region_instance_group_manager" "kv_server" { port = var.service_port } - named_port { - name = "envoy" - port = var.envoy_port + dynamic "named_port" { + for_each = var.enable_external_traffic ? toset([1]) : toset([]) + content { + name = "envoy" + port = var.envoy_port + } } base_instance_name = "${var.service}-${var.environment}" diff --git a/production/terraform/gcp/services/autoscaling/variables.tf b/production/terraform/gcp/services/autoscaling/variables.tf index d070aa23..8b25de98 100644 --- a/production/terraform/gcp/services/autoscaling/variables.tf +++ b/production/terraform/gcp/services/autoscaling/variables.tf @@ -110,3 +110,9 @@ variable "shard_num" { description = "Shard number." type = string } + +variable "enable_external_traffic" { + description = "Whether to serve external traffic. If disabled, only internal traffic via service mesh will be served." + default = true + type = bool +} diff --git a/production/terraform/gcp/services/dashboards/main.tf b/production/terraform/gcp/services/dashboards/main.tf index f5cd84d1..88f7f0bf 100644 --- a/production/terraform/gcp/services/dashboards/main.tf +++ b/production/terraform/gcp/services/dashboards/main.tf @@ -20,9 +20,1273 @@ resource "google_monitoring_dashboard" "environment_dashboard" { "columns": 48, "tiles": [ { - "height": 19, + "height": 20, "widget": { - "title": "system.cpu.total_cores [MEAN]", + "title": "request.count [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/request.count\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 0 + }, + { + "height": 20, + "widget": { + "title": "Secure lookup request count [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/SecureLookupRequestCount\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 0 + }, + { + "height": 20, + "widget": { + "title": "request.failed_count_by_status [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/request.failed_count_by_status\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 20 + }, + { + "height": 20, + "widget": { + "title": "request.duration_ms [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/request.duration_ms\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 20 + }, + { + "height": 20, + "widget": { + "title": "request.size_bytes [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/request.size_bytes\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 40 + }, + { + "height": 20, + "widget": { + "title": "response.size_bytes [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/response.size_bytes\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 40 + }, + { + "height": 20, + "widget": { + "title": "Request Errors [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/KVUdfRequestError\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"error_code\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 60 + }, + { + "height": 20, + "widget": { + "title": "Internal Request Errors [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/InternalLookupRequestError\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"error_code\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 60 + }, + { + "height": 20, + "widget": { + "title": "Server Non-Request Errors [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/KVServerError\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"error_code\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 80 + }, + { + "height": 20, + "widget": { + "title": "Sharded Lookup Key Count By Shard [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/ShardedLookupKeyCountByShard\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 80 + }, + { + "height": 20, + "widget": { + "title": "Sharded Lookup GetKeyValues Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/ShardedLookupGetKeyValuesLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 100 + }, + { + "height": 20, + "widget": { + "title": "Sharded Lookup GetKeyValueSet Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/ShardedLookupGetKeyValueSetLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 100 + }, + { + "height": 20, + "widget": { + "title": "Sharded Lookup RunQuery Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/ShardedLookupRunQueryLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 120 + }, + { + "height": 20, + "widget": { + "title": "Internal GetKeyValues Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/InternalGetKeyValuesLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 120 + }, + { + "height": 20, + "widget": { + "title": "Internal GetKeyValueSet Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/InternalGetKeyValueSetLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 140 + }, + { + "height": 20, + "widget": { + "title": "Internal RunQuery Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/InternalRunQueryLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 140 + }, + { + "height": 20, + "widget": { + "title": "Cache GetKeyValuePairs Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/GetValuePairsLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 160 + }, + { + "height": 20, + "widget": { + "title": "Cache GetKeyValueSet Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/GetKeyValueSetLatencyInMicros\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 160 + }, + { + "height": 20, + "widget": { + "title": "Cache Access Event Count [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/CacheAccessEventCount\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"cache_access\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 180 + }, + { + "height": 20, + "widget": { + "title": "Server Retryable Operation Status Count [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/GetParameterStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/CompleteLifecycleStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/CreateDataOrchestratorStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/StartDataOrchestratorStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/LoadNewFilesStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/GetShardManagerStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/DescribeInstanceGroupInstancesStatus\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"status\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 180 + }, + { + "height": 20, + "widget": { + "title": "File Update Stats [MEAN]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/TotalRowsUpdatedInDataLoading\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\" metric.label.\"data_source\"!=\"realtime\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"data_source\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/TotalRowsDeletedInDataLoading\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\" metric.label.\"data_source\"!=\"realtime\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"data_source\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/TotalRowsDroppedInDataLoading\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\" metric.label.\"data_source\"!=\"realtime\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"data_source\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 200 + }, + { + "height": 20, + "widget": { + "title": "Data Reader Latency [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/ConcurrentStreamRecordReaderReadShardRecordsLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/ConcurrentStreamRecordReaderReadStreamRecordsLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 200 + }, + { + "height": 20, + "widget": { + "title": "Cache UpdateKeyValue Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/UpdateKeyValueLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 220 + }, + { + "height": 20, + "widget": { + "title": "Cache UpdateKeyValueSet Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/UpdateKeyValueSetLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 220 + }, + { + "height": 20, + "widget": { + "title": "Cache DeleteKey Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/DeleteKeyLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 240 + }, + { + "height": 20, + "widget": { + "title": "Cache DeleteValuesInSet Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/DeleteValuesInSetLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 240 + }, + { + "height": 20, + "widget": { + "title": "Cache RemoveDeletedKey Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/RemoveDeletedKeyLatency\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 0, + "yPos": 260 + }, + { + "height": 20, + "widget": { + "title": "Realtime Update Stats [MEAN]", "xyChart": { "chartOptions": {}, "dataSets": [ @@ -33,18 +1297,105 @@ resource "google_monitoring_dashboard" "environment_dashboard" { "timeSeriesQuery": { "timeSeriesFilter": { "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/TotalRowsUpdatedInDataLoading\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\" metric.label.\"data_source\"=\"realtime\"", + "secondaryAggregation": { "alignmentPeriod": "60s", "crossSeriesReducer": "REDUCE_MEAN", "groupByFields": [ - "metric.label.\"service_name\"", - "metric.label.\"deployment_environment\"", + "metric.label.\"Noise\"", "metric.label.\"shard_number\"", - "resource.label.\"task_id\"", - "metric.label.\"service_version\"" + "metric.label.\"service_instance_id\"" ], "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" }, - "filter": "metric.type=\"workload.googleapis.com/system.cpu.percent\" resource.type=\"generic_task\" metric.label.\"label\"=\"total cpu cores\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + "filter": "metric.type=\"workload.googleapis.com/TotalRowsDeletedInDataLoading\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\" metric.label.\"data_source\"=\"realtime\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"workload.googleapis.com/TotalRowsDroppedInDataLoading\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\" metric.label.\"data_source\"=\"realtime\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + } + } + } + } + ], + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 24, + "xPos": 24, + "yPos": 260 + }, + { + "height": 20, + "widget": { + "title": "Realtime Message Processing Latency Microseconds [95TH PERCENTILE]", + "xyChart": { + "chartOptions": {}, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_PERCENTILE_95", + "groupByFields": [ + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_DELTA" + }, + "filter": "metric.type=\"workload.googleapis.com/ReceivedLowLatencyNotifications\" resource.type=\"generic_task\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" } } } @@ -56,10 +1407,10 @@ resource "google_monitoring_dashboard" "environment_dashboard" { }, "width": 24, "xPos": 0, - "yPos": 0 + "yPos": 280 }, { - "height": 19, + "height": 20, "widget": { "title": "system.cpu.percent [MEAN]", "xyChart": { @@ -75,12 +1426,10 @@ resource "google_monitoring_dashboard" "environment_dashboard" { "alignmentPeriod": "60s", "crossSeriesReducer": "REDUCE_MEAN", "groupByFields": [ - "metric.label.\"service_name\"", - "metric.label.\"deployment_environment\"", "metric.label.\"label\"", + "metric.label.\"Noise\"", "metric.label.\"shard_number\"", - "resource.label.\"task_id\"", - "metric.label.\"service_version\"" + "metric.label.\"service_instance_id\"" ], "perSeriesAligner": "ALIGN_MEAN" }, @@ -96,12 +1445,12 @@ resource "google_monitoring_dashboard" "environment_dashboard" { }, "width": 24, "xPos": 24, - "yPos": 0 + "yPos": 280 }, { - "height": 19, + "height": 20, "widget": { - "title": "system.memory.usage_kb for main process [MEAN]", + "title": "system.memory.usage_kb [MEAN]", "xyChart": { "chartOptions": {}, "dataSets": [ @@ -115,17 +1464,37 @@ resource "google_monitoring_dashboard" "environment_dashboard" { "alignmentPeriod": "60s", "crossSeriesReducer": "REDUCE_MEAN", "groupByFields": [ - "metric.label.\"service_name\"", - "metric.label.\"deployment_environment\"", + "metric.label.\"label\"", + "metric.label.\"Noise\"", "metric.label.\"shard_number\"", - "resource.label.\"task_id\"", - "metric.label.\"service_version\"" + "metric.label.\"service_instance_id\"" ], "perSeriesAligner": "ALIGN_MEAN" }, "filter": "metric.type=\"workload.googleapis.com/system.memory.usage_kb\" resource.type=\"generic_task\" metric.label.\"label\"=\"main process\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" } } + }, + { + "minAlignmentPeriod": "60s", + "plotType": "LINE", + "targetAxis": "Y1", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_MEAN", + "groupByFields": [ + "metric.label.\"label\"", + "metric.label.\"Noise\"", + "metric.label.\"shard_number\"", + "metric.label.\"service_instance_id\"" + ], + "perSeriesAligner": "ALIGN_MEAN" + }, + "filter": "metric.type=\"workload.googleapis.com/system.memory.usage_kb\" resource.type=\"generic_task\" metric.label.\"label\"=\"MemTotal:\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + } + } } ], "yAxis": { @@ -135,12 +1504,12 @@ resource "google_monitoring_dashboard" "environment_dashboard" { }, "width": 24, "xPos": 0, - "yPos": 19 + "yPos": 300 }, { - "height": 19, + "height": 20, "widget": { - "title": "system.memory.usage_kb for MemAvailable: [MEAN]", + "title": "system.cpu.total_cores [MEAN]", "xyChart": { "chartOptions": {}, "dataSets": [ @@ -154,16 +1523,13 @@ resource "google_monitoring_dashboard" "environment_dashboard" { "alignmentPeriod": "60s", "crossSeriesReducer": "REDUCE_MEAN", "groupByFields": [ - "metric.label.\"service_name\"", - "metric.label.\"deployment_environment\"", - "metric.label.\"label\"", + "metric.label.\"Noise\"", "metric.label.\"shard_number\"", - "resource.label.\"task_id\"", - "metric.label.\"service_version\"" + "metric.label.\"service_instance_id\"" ], "perSeriesAligner": "ALIGN_MEAN" }, - "filter": "metric.type=\"workload.googleapis.com/system.memory.usage_kb\" resource.type=\"generic_task\" metric.label.\"label\"=\"MemAvailable:\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" + "filter": "metric.type=\"workload.googleapis.com/system.cpu.percent\" resource.type=\"generic_task\" metric.label.\"label\"=\"total cpu cores\" metric.label.\"deployment_environment\"=\"${var.environment}\" metric.label.\"service_name\"=\"kv-server\"" } } } @@ -175,7 +1541,7 @@ resource "google_monitoring_dashboard" "environment_dashboard" { }, "width": 24, "xPos": 24, - "yPos": 19 + "yPos": 300 } ] } diff --git a/production/terraform/gcp/services/networking/main.tf b/production/terraform/gcp/services/networking/main.tf index bd749005..87c8fb07 100644 --- a/production/terraform/gcp/services/networking/main.tf +++ b/production/terraform/gcp/services/networking/main.tf @@ -27,7 +27,7 @@ resource "google_compute_subnetwork" "kv_server" { network = var.use_existing_vpc ? var.existing_vpc_id : google_compute_network.kv_server[0].id purpose = "PRIVATE" region = each.value - ip_cidr_range = "10.${each.key}.3.0/24" + ip_cidr_range = tolist(var.regions_cidr_blocks)[each.key] } resource "google_compute_router" "kv_server" { @@ -39,7 +39,10 @@ resource "google_compute_router" "kv_server" { } resource "google_compute_router_nat" "kv_server" { - for_each = google_compute_router.kv_server + for_each = { + for key, value in google_compute_router.kv_server : key => value + if !contains(var.regions_use_existing_nat, value.region) + } name = "${var.service}-${var.environment}-${each.value.region}-nat" router = each.value.name @@ -59,6 +62,7 @@ resource "google_compute_global_address" "collector" { } resource "google_compute_global_address" "kv_server" { + count = var.enable_external_traffic ? 1 : 0 name = "${var.service}-${var.environment}-xlb-ip" ip_version = "IPV4" } diff --git a/production/terraform/gcp/services/networking/outputs.tf b/production/terraform/gcp/services/networking/outputs.tf index feb20b94..3bb29175 100644 --- a/production/terraform/gcp/services/networking/outputs.tf +++ b/production/terraform/gcp/services/networking/outputs.tf @@ -28,5 +28,5 @@ output "collector_ip_address" { } output "server_ip_address" { - value = google_compute_global_address.kv_server.address + value = one(google_compute_global_address.kv_server[*].address) } diff --git a/production/terraform/gcp/services/networking/variables.tf b/production/terraform/gcp/services/networking/variables.tf index 95d04e5d..00bf42ec 100644 --- a/production/terraform/gcp/services/networking/variables.tf +++ b/production/terraform/gcp/services/networking/variables.tf @@ -29,6 +29,16 @@ variable "regions" { type = set(string) } +variable "regions_cidr_blocks" { + description = "A set of CIDR ranges for all specified regions. The number of blocks here should correspond to the number of regions." + type = set(string) +} + +variable "regions_use_existing_nat" { + description = "Regions that use existing nat. No new nats will be created for regions specified here." + type = set(string) +} + variable "collector_service_name" { description = "Assigned name of metrics collection service" type = string @@ -43,3 +53,9 @@ variable "existing_vpc_id" { description = "Existing vpc id. This would only be used if use_existing_vpc is true." type = string } + +variable "enable_external_traffic" { + description = "Whether to serve external traffic. If disabled, only internal traffic via service mesh will be served." + default = true + type = bool +} diff --git a/production/terraform/gcp/services/service_mesh/variables.tf b/production/terraform/gcp/services/service_mesh/variables.tf index 96acce7c..8d35f935 100644 --- a/production/terraform/gcp/services/service_mesh/variables.tf +++ b/production/terraform/gcp/services/service_mesh/variables.tf @@ -63,3 +63,9 @@ variable "existing_service_mesh" { description = "Existing service mesh. This would only be used if use_existing_service_mesh is true." type = string } + +variable "enable_external_traffic" { + description = "Whether to serve external traffic. If disabled, only internal traffic via service mesh will be served." + default = true + type = bool +} diff --git a/public/applications/pa/BUILD.bazel b/public/applications/pa/BUILD.bazel index 19c877cc..ff1bdd0b 100644 --- a/public/applications/pa/BUILD.bazel +++ b/public/applications/pa/BUILD.bazel @@ -37,7 +37,7 @@ cc_library( deps = [ "api_overlay_cc_proto", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) diff --git a/public/applications/pa/response_utils.cc b/public/applications/pa/response_utils.cc index c4e5ed82..bbc44965 100644 --- a/public/applications/pa/response_utils.cc +++ b/public/applications/pa/response_utils.cc @@ -15,7 +15,7 @@ #include "public/applications/pa/response_utils.h" #include "google/protobuf/util/json_util.h" -#include "src/cpp/util/status_macro/status_macros.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server::application_pa { diff --git a/public/applications/pas/retrieval_request_builder.cc b/public/applications/pas/retrieval_request_builder.cc index 612a390f..45f2d420 100644 --- a/public/applications/pas/retrieval_request_builder.cc +++ b/public/applications/pas/retrieval_request_builder.cc @@ -16,14 +16,20 @@ namespace kv_server::application_pas { +v2::GetValuesRequest GetRequest() { + static const std::string* kClient = new std::string("Retrieval.20231018"); + v2::GetValuesRequest req; + req.set_client_version(*kClient); + (*(req.mutable_metadata()->mutable_fields()))["is_pas"].set_string_value( + "true"); + return req; +} + v2::GetValuesRequest BuildRetrievalRequest( std::string protected_signals, absl::flat_hash_map device_metadata, std::string contextual_signals, std::vector optional_ad_ids) { - static const std::string* kClient = new std::string("Retrieval.20231018"); - - v2::GetValuesRequest req; - req.set_client_version(*kClient); + v2::GetValuesRequest req = GetRequest(); v2::RequestPartition* partition = req.add_partitions(); { auto* protected_signals_arg = partition->add_arguments(); @@ -56,4 +62,17 @@ v2::GetValuesRequest BuildRetrievalRequest( return req; } +v2::GetValuesRequest BuildLookupRequest(std::vector ad_ids) { + v2::GetValuesRequest req = GetRequest(); + v2::RequestPartition* partition = req.add_partitions(); + auto* ad_id_arg = partition->add_arguments(); + for (auto&& item : std::move(ad_ids)) { + ad_id_arg->mutable_data() + ->mutable_list_value() + ->add_values() + ->set_string_value(std::move(item)); + } + return req; +} + } // namespace kv_server::application_pas diff --git a/public/applications/pas/retrieval_request_builder.h b/public/applications/pas/retrieval_request_builder.h index 7dd64298..99e2f7ce 100644 --- a/public/applications/pas/retrieval_request_builder.h +++ b/public/applications/pas/retrieval_request_builder.h @@ -35,6 +35,9 @@ v2::GetValuesRequest BuildRetrievalRequest( std::string contextual_signals, std::vector optional_ad_ids = {}); +// Builds a GetValuesRequest. Stores the input arguments into the request. +v2::GetValuesRequest BuildLookupRequest(std::vector ad_ids); + } // namespace kv_server::application_pas #endif // PUBLIC_APPLICATIONS_PAS_RETRIEVAL_REQUEST_BUILDER_H_ diff --git a/public/constants.cc b/public/constants.cc index f2105691..6fbeac95 100644 --- a/public/constants.cc +++ b/public/constants.cc @@ -44,4 +44,11 @@ const std::regex& LogicalShardingConfigFileFormatRegex() { return *regex; } +const std::regex& FileGroupFilenameFormatRegex() { + static const std::regex* const regex = new std::regex(absl::StrFormat( + R"((DELTA|SNAPSHOT)_\d{%d}_\d{%d}_OF_\d{%d})", kLogicalTimeDigits, + kFileGroupFileIndexDigits, kFileGroupSizeDigits)); + return *regex; +} + } // namespace kv_server diff --git a/public/constants.h b/public/constants.h index 10356049..ea2a346a 100644 --- a/public/constants.h +++ b/public/constants.h @@ -38,6 +38,12 @@ constexpr std::string_view kFileComponentDelimiter = "_"; // Number of digits in logical time in file basename. constexpr int kLogicalTimeDigits = 16; +// Number of digits used to represent the index of a file in a file group. +constexpr int kFileGroupFileIndexDigits = 5; + +// Number of digits used to represent the size of a file group. +constexpr int kFileGroupSizeDigits = 6; + // "DELTA_\d{16}" // The first component represents the file type. // @@ -65,10 +71,26 @@ constexpr char kQueryArgDelimiter = ','; // indicates a more recent snapshot. const std::regex& SnapshotFileFormatRegex(); +// Returns a compiled file group file name regex defined as follows: +// +// Compiled regex = "(DELTA|SNAPSHOT)_\d{16}_\d{5}_OF_\d{6}". Regex parts +// correspond to the following parts: +// NAME REGEX: +// ___OF_ +// WHERE: +// (1) FILE_TYPE - type of file. Valid values: [DELTA, SNAPSHOT]. +// (2) LOGICAL_TIMESTAMP - strictly increasing 16 digit number that represents +// logical time, a larger value means a more recent file. +// (3) PART_FILE_INDEX - zero-based 5 digit index of file in the file group. +// Valid range: [0..NUM_PART_FILES). +// (4) NUM_PART_FILES = a 6 digit number representing the total number of +// files in a file group. Valid range: [1..100,000] +const std::regex& FileGroupFilenameFormatRegex(); + // X25519 public key used to test/debug/demo the ObliviousGetValues query API. // ObliviousGetValues requests encrypted with this key can be processed by the // server. -// For cross code base consistency, matches the test key we use in the commong +// For cross code base consistency, matches the test key we use in the common // repo, see ../encryption/key_fetcher/src/fake_key_fetcher_manager.h constexpr std::string_view kTestPublicKey = "f3b7b2f1764f5c077effecad2afd86154596e63f7375ea522761b881e6c3c323"; @@ -76,12 +98,12 @@ constexpr std::string_view kTestPublicKey = // Parameters used to configure Oblivious HTTP according to // https://github.com/WICG/turtledove/blob/main/FLEDGE_Key_Value_Server_API.md#encryption // -// KEM: DHKEM(X25519, HKDF-SHA256) 0x0020 +// KEM: DHKEM(X25519, HKDF-SHA256) const uint16_t kKEMParameter = 0x0020; -// KDF: HKDF-SHA256 0x0001 +// KDF: HKDF-SHA256 const uint16_t kKDFParameter = 0x0001; -// AEAD: AES-128-GCM 0X0001 -const uint16_t kAEADParameter = 0x0001; +// AEAD: AES-256-GCM +const uint16_t kAEADParameter = 0x0002; constexpr std::string_view kServiceName = "kv-server"; diff --git a/public/data_loading/BUILD.bazel b/public/data_loading/BUILD.bazel index 8d0caf97..3286748e 100644 --- a/public/data_loading/BUILD.bazel +++ b/public/data_loading/BUILD.bazel @@ -56,7 +56,7 @@ cc_library( deps = [ ":data_loading_fbs", ":record_utils", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", ], @@ -68,7 +68,7 @@ cc_library( hdrs = ["record_utils.h"], deps = [ ":data_loading_fbs", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -112,8 +112,9 @@ cc_library( srcs = ["filename_utils.cc"], hdrs = ["filename_utils.h"], deps = [ + "//public:base_types_cc_proto", "//public:constants", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", diff --git a/public/data_loading/aggregation/BUILD.bazel b/public/data_loading/aggregation/BUILD.bazel index 673b48da..48af8024 100644 --- a/public/data_loading/aggregation/BUILD.bazel +++ b/public/data_loading/aggregation/BUILD.bazel @@ -25,8 +25,8 @@ cc_library( "//public/data_loading:records_utils", "//public/data_loading/writers:delta_record_stream_writer", "//public/data_loading/writers:delta_record_writer", - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -51,6 +51,7 @@ cc_test( cc_binary( name = "record_aggregator_benchmarks", srcs = ["record_aggregator_benchmarks.cc"], + malloc = "@com_google_tcmalloc//tcmalloc", deps = [ ":record_aggregator", "//public/data_loading:records_utils", diff --git a/public/data_loading/aggregation/record_aggregator.cc b/public/data_loading/aggregation/record_aggregator.cc index 893cf36b..4a2acee3 100644 --- a/public/data_loading/aggregation/record_aggregator.cc +++ b/public/data_loading/aggregation/record_aggregator.cc @@ -21,11 +21,11 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" #include "absl/memory/memory.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" -#include "glog/logging.h" #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/writers/delta_record_stream_writer.h" #include "public/data_loading/writers/delta_record_writer.h" diff --git a/public/data_loading/csv/BUILD.bazel b/public/data_loading/csv/BUILD.bazel index b36e5a7b..396907f0 100644 --- a/public/data_loading/csv/BUILD.bazel +++ b/public/data_loading/csv/BUILD.bazel @@ -67,7 +67,7 @@ cc_library( "@com_google_riegeli//riegeli/bytes:istream_reader", "@com_google_riegeli//riegeli/csv:csv_reader", "@com_google_riegeli//riegeli/csv:csv_record", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) @@ -75,11 +75,10 @@ cc_test( name = "csv_delta_record_stream_reader_test", size = "small", srcs = ["csv_delta_record_stream_reader_test.cc"], - env = {"GLOG_v": "10"}, deps = [ ":csv_delta_record_stream_reader", ":csv_delta_record_stream_writer", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_googletest//:gtest_main", ], ) diff --git a/public/data_loading/csv/csv_delta_record_stream_reader.cc b/public/data_loading/csv/csv_delta_record_stream_reader.cc index 6662c912..433c34ff 100644 --- a/public/data_loading/csv/csv_delta_record_stream_reader.cc +++ b/public/data_loading/csv/csv_delta_record_stream_reader.cc @@ -18,11 +18,11 @@ #include +#include "absl/log/log.h" #include "absl/strings/ascii.h" #include "absl/strings/escaping.h" #include "absl/strings/match.h" #include "absl/strings/str_split.h" -#include "glog/logging.h" #include "public/data_loading/record_utils.h" namespace kv_server { diff --git a/public/data_loading/csv/csv_delta_record_stream_reader.h b/public/data_loading/csv/csv_delta_record_stream_reader.h index 1dc17028..31ca6d1b 100644 --- a/public/data_loading/csv/csv_delta_record_stream_reader.h +++ b/public/data_loading/csv/csv_delta_record_stream_reader.h @@ -21,7 +21,7 @@ #include #include -#include "glog/logging.h" +#include "absl/log/log.h" #include "public/data_loading/csv/constants.h" #include "public/data_loading/readers/delta_record_reader.h" #include "public/data_loading/record_utils.h" diff --git a/public/data_loading/csv/csv_delta_record_stream_reader_test.cc b/public/data_loading/csv/csv_delta_record_stream_reader_test.cc index 0cab98cb..5ec95cb0 100644 --- a/public/data_loading/csv/csv_delta_record_stream_reader_test.cc +++ b/public/data_loading/csv/csv_delta_record_stream_reader_test.cc @@ -18,7 +18,7 @@ #include -#include "glog/logging.h" +#include "absl/log/log.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "public/data_loading/csv/csv_delta_record_stream_writer.h" diff --git a/public/data_loading/csv/csv_delta_record_stream_writer.cc b/public/data_loading/csv/csv_delta_record_stream_writer.cc index f884a389..1fc1d970 100644 --- a/public/data_loading/csv/csv_delta_record_stream_writer.cc +++ b/public/data_loading/csv/csv_delta_record_stream_writer.cc @@ -16,10 +16,10 @@ #include "public/data_loading/csv/csv_delta_record_stream_writer.h" +#include "absl/log/log.h" #include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" -#include "glog/logging.h" #include "public/data_loading/data_loading_generated.h" namespace kv_server { diff --git a/public/data_loading/filename_utils.cc b/public/data_loading/filename_utils.cc index c1b1fff0..291a0a16 100644 --- a/public/data_loading/filename_utils.cc +++ b/public/data_loading/filename_utils.cc @@ -17,9 +17,9 @@ #include #include // NOLINT +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/strings/str_format.h" -#include "glog/logging.h" #include "public/constants.h" namespace kv_server { @@ -84,4 +84,31 @@ absl::StatusOr ToLogicalShardingConfigFilename( return result; } +bool IsFileGroupFileName(std::string_view filename) { + return std::regex_match(filename.begin(), filename.end(), + FileGroupFilenameFormatRegex()); +} + +absl::StatusOr ToFileGroupFileName(FileType::Enum file_type, + uint64_t logical_commit_time, + uint64_t file_index, + uint64_t file_group_size) { + if (file_type != FileType::DELTA && file_type != FileType::SNAPSHOT) { + return absl::InvalidArgumentError( + absl::StrCat("File groups are not supported for file type: ", + FileType_Enum_Name(file_type))); + } + if (file_index >= file_group_size) { + return absl::InvalidArgumentError( + absl::StrCat("file index: ", file_index, + " must be less than file group size: ", file_group_size)); + } + return absl::StrFormat( + "%s%s%0*d%s%0*d%sOF%s%0*d", FileType::Enum_Name(file_type), + kFileComponentDelimiter, kLogicalTimeDigits, logical_commit_time, + kFileComponentDelimiter, kFileGroupFileIndexDigits, file_index, + kFileComponentDelimiter, kFileComponentDelimiter, kFileGroupSizeDigits, + file_group_size); +} + } // namespace kv_server diff --git a/public/data_loading/filename_utils.h b/public/data_loading/filename_utils.h index a331f7ba..3405ce63 100644 --- a/public/data_loading/filename_utils.h +++ b/public/data_loading/filename_utils.h @@ -21,6 +21,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "public/base_types.pb.h" namespace kv_server { @@ -55,6 +56,19 @@ bool IsLogicalShardingConfigFilename(std::string_view basename); absl::StatusOr ToLogicalShardingConfigFilename( uint64_t logical_commit_time); +// Returns true if `filename` is a valid file group filename. +// +// Valid file group filenames conform to the regex return by +// `FileGroupFilenameFormatRegex()` in constants.h +bool IsFileGroupFileName(std::string_view filename); + +// Attempts to construct a valid file group file name from the inputs. +// Returns a descriptive `absl::InvalidArgumentError` on error. +absl::StatusOr ToFileGroupFileName(FileType::Enum file_type, + uint64_t logical_commit_time, + uint64_t file_index, + uint64_t file_group_size); + } // namespace kv_server #endif // PUBLIC_DATA_LOADING_FILENAME_UTILS_H_ diff --git a/public/data_loading/filename_utils_test.cc b/public/data_loading/filename_utils_test.cc index d9e78ea7..e8882ecc 100644 --- a/public/data_loading/filename_utils_test.cc +++ b/public/data_loading/filename_utils_test.cc @@ -95,5 +95,41 @@ TEST(SnapshotFilename, ToLogicalShardingConfigFilename) { ("LOGICAL_SHARDING_CONFIG_1234512345123451")); } +TEST(FileGroupFilename, IsFileGroupFileName) { + EXPECT_FALSE(IsFileGroupFileName("")); + EXPECT_FALSE(IsFileGroupFileName("DELTA")); + EXPECT_FALSE(IsFileGroupFileName("SNAPSHOT")); + EXPECT_FALSE(IsFileGroupFileName("DELTA_1234512345123451")); + EXPECT_FALSE(IsFileGroupFileName("SNAPSHOT_1234512345123451")); + EXPECT_FALSE(IsFileGroupFileName("SNAPSHOT_1234512345123451_00000")); + EXPECT_TRUE(IsFileGroupFileName("DELTA_1234512345123451_00000_OF_000100")); + EXPECT_TRUE(IsFileGroupFileName("SNAPSHOT_1234512345123451_00000_OF_000100")); +} + +TEST(FileGroupFilename, ToFileGroupFileNameInvalidInputs) { + auto filename = + ToFileGroupFileName(FileType::DELTA, /*logical_commit_time=*/10, + /*file_index=*/11, /*file_group_size=*/10); + EXPECT_FALSE(filename.ok()) << filename.status(); + EXPECT_THAT(filename.status().code(), absl::StatusCode::kInvalidArgument); + filename = ToFileGroupFileName(FileType::FILE_TYPE_UNSPECIFIED, + /*logical_commit_time=*/10, + /*file_index=*/1, /*file_group_size=*/10); + EXPECT_FALSE(filename.ok()) << filename.status(); + EXPECT_THAT(filename.status().code(), absl::StatusCode::kInvalidArgument); +} + +TEST(FileGroupFilename, ToFileGroupFileNameValidInputs) { + auto filename = + ToFileGroupFileName(FileType::DELTA, /*logical_commit_time=*/10, + /*file_index=*/1, /*file_group_size=*/10); + ASSERT_TRUE(filename.ok()) << filename.status(); + EXPECT_THAT(*filename, "DELTA_0000000000000010_00001_OF_000010"); + filename = ToFileGroupFileName(FileType::SNAPSHOT, /*logical_commit_time=*/10, + /*file_index=*/0, /*file_group_size=*/10); + ASSERT_TRUE(filename.ok()) << filename.status(); + EXPECT_THAT(*filename, "SNAPSHOT_0000000000000010_00000_OF_000010"); +} + } // namespace } // namespace kv_server diff --git a/public/data_loading/readers/BUILD.bazel b/public/data_loading/readers/BUILD.bazel index 2a680fdc..6cb118e1 100644 --- a/public/data_loading/readers/BUILD.bazel +++ b/public/data_loading/readers/BUILD.bazel @@ -33,15 +33,16 @@ cc_library( ":stream_record_reader", "//components/telemetry:server_definition", "//public/data_loading:riegeli_metadata_cc_proto", - "@com_github_google_glog//:glog", "@com_google_absl//absl/base", "@com_google_absl//absl/cleanup", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_riegeli//riegeli/bytes:istream_reader", "@com_google_riegeli//riegeli/records:record_reader", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) @@ -60,13 +61,14 @@ cc_library( "//components/telemetry:server_definition", "//public/data_loading:riegeli_metadata_cc_proto", "@avro//:avrocpp", - "@com_github_google_glog//:glog", "@com_google_absl//absl/base", "@com_google_absl//absl/cleanup", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) @@ -75,7 +77,7 @@ cc_library( hdrs = ["stream_record_reader_factory.h"], deps = [ ":stream_record_reader", - "@google_privacysandbox_servers_common//src/cpp/telemetry:telemetry_provider", + "@google_privacysandbox_servers_common//src/telemetry:telemetry_provider", ], ) @@ -124,7 +126,6 @@ cc_test( "@com_google_riegeli//riegeli/bytes:ostream_writer", "@com_google_riegeli//riegeli/bytes:string_writer", "@com_google_riegeli//riegeli/records:record_writer", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -145,7 +146,6 @@ cc_test( "//public/test_util:mocks", "//public/test_util:proto_matcher", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -174,7 +174,7 @@ cc_library( ":delta_record_reader", "//public/data_loading:records_utils", "@avro//:avrocpp", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", ], diff --git a/public/data_loading/readers/avro_stream_io.cc b/public/data_loading/readers/avro_stream_io.cc index cc5a7426..cc648df0 100644 --- a/public/data_loading/readers/avro_stream_io.cc +++ b/public/data_loading/readers/avro_stream_io.cc @@ -14,6 +14,7 @@ #include "public/data_loading/readers/avro_stream_io.h" +#include "absl/log/check.h" #include "third_party/avro/api/DataFile.hh" #include "third_party/avro/api/Schema.hh" #include "third_party/avro/api/Stream.hh" @@ -25,15 +26,19 @@ AvroStreamReader::AvroStreamReader(std::istream& data_input) absl::Status AvroStreamReader::ReadStreamRecords( const std::function& callback) { - avro::InputStreamPtr input_stream = avro::istreamInputStream(data_input_); - avro::DataFileReader reader(std::move(input_stream)); + try { + avro::InputStreamPtr input_stream = avro::istreamInputStream(data_input_); + avro::DataFileReader reader(std::move(input_stream)); - std::string record; - absl::Status overall_status; - while (reader.read(record)) { - overall_status.Update(callback(record)); + std::string record; + absl::Status overall_status; + while (reader.read(record)) { + overall_status.Update(callback(record)); + } + return overall_status; + } catch (const std::exception& e) { + return absl::InternalError(e.what()); } - return overall_status; } AvroConcurrentStreamRecordReader::AvroConcurrentStreamRecordReader( @@ -95,7 +100,10 @@ AvroConcurrentStreamRecordReader::BuildByteRanges() { // stream are read. absl::Status AvroConcurrentStreamRecordReader::ReadStreamRecords( const std::function& callback) { - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder< + ServerSafeMetricsContext, + kConcurrentStreamRecordReaderReadStreamRecordsLatency> + latency_recorder(KVServerContextMap()->SafeMetric()); auto byte_ranges = BuildByteRanges(); if (!byte_ranges.ok() || byte_ranges->empty()) { return byte_ranges.status(); @@ -122,14 +130,9 @@ absl::Status AvroConcurrentStreamRecordReader::ReadStreamRecords( } total_records_read += curr_byte_range_result->num_records_read; } - auto duration = absl::Now() - start_time; VLOG(2) << "Done reading " << total_records_read << " records in " - << absl::ToDoubleMilliseconds(duration) << " ms."; - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); + << absl::ToDoubleMilliseconds(latency_recorder.GetLatency()) + << " ms."; return absl::OkStatus(); } @@ -153,7 +156,9 @@ AvroConcurrentStreamRecordReader::ReadByteRange( VLOG(2) << "Reading byte_range: " << "[" << byte_range.begin_offset << "," << byte_range.end_offset << "]"; - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder + latency_recorder(KVServerContextMap()->SafeMetric()); auto record_stream = stream_factory_(); VLOG(9) << "creating input stream"; avro::InputStreamPtr input_stream = @@ -182,15 +187,10 @@ AvroConcurrentStreamRecordReader::ReadByteRange( << overall_status; return overall_status; } - auto duration = absl::Now() - start_time; VLOG(2) << "Done reading " << num_records_read << " records in byte_range: [" << byte_range.begin_offset << "," << byte_range.end_offset << "] in " - << absl::ToDoubleMilliseconds(duration) << " ms."; - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); + << absl::ToDoubleMilliseconds(latency_recorder.GetLatency()) + << " ms."; ByteRangeResult result; result.num_records_read = num_records_read; return result; diff --git a/public/data_loading/readers/avro_stream_io.h b/public/data_loading/readers/avro_stream_io.h index 38ae8c30..de7b8c35 100644 --- a/public/data_loading/readers/avro_stream_io.h +++ b/public/data_loading/readers/avro_stream_io.h @@ -27,14 +27,14 @@ #include "absl/base/optimization.h" #include "absl/cleanup/cleanup.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "components/telemetry/server_definition.h" -#include "glog/logging.h" #include "public/data_loading/readers/stream_record_reader.h" #include "public/data_loading/riegeli_metadata.pb.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { diff --git a/public/data_loading/readers/avro_stream_io_test.cc b/public/data_loading/readers/avro_stream_io_test.cc index 181abfdb..59e4e5b0 100644 --- a/public/data_loading/readers/avro_stream_io_test.cc +++ b/public/data_loading/readers/avro_stream_io_test.cc @@ -19,7 +19,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" #include "third_party/avro/api/DataFile.hh" #include "third_party/avro/api/Schema.hh" #include "third_party/avro/api/ValidSchema.hh" @@ -30,6 +29,11 @@ namespace { constexpr std::string_view kTestRecord = "testrecord"; constexpr int64_t kIterations = 1024 * 1024 * 9; +void WriteInvalidFile(const std::vector& records, + std::ostream& dest_stream) { + dest_stream << "invalid"; +} + void WriteAvroToFile(const std::vector& records, std::ostream& dest_stream) { avro::OutputStreamPtr avro_output_stream = @@ -122,6 +126,44 @@ TEST(AvroStreamIO, SequentialReading) { record_reader.ReadStreamRecords(record_callback.AsStdFunction()); EXPECT_TRUE(status.ok()) << status; } +TEST(AvroStreamIO, ConcurrentReadingInvalidFile) { + kv_server::InitMetricsContextMap(); + constexpr std::string_view kFileName = "ConcurrentReading.invalid"; + const std::filesystem::path path = + std::filesystem::path(::testing::TempDir()) / kFileName; + std::ofstream output_stream(path); + WriteInvalidFile({kTestRecord}, output_stream); + output_stream.close(); + + AvroConcurrentStreamRecordReader::Options options; + AvroConcurrentStreamRecordReader record_reader( + [&path] { return std::make_unique(path); }, options); + + testing::MockFunction record_callback; + EXPECT_CALL(record_callback, Call).Times(0); + auto status = + record_reader.ReadStreamRecords(record_callback.AsStdFunction()); + EXPECT_FALSE(status.ok()); +} + +TEST(AvroStreamIO, SequentialReadingInvalidFile) { + kv_server::InitMetricsContextMap(); + constexpr std::string_view kFileName = "SequentialReading.invalid"; + const std::filesystem::path path = + std::filesystem::path(::testing::TempDir()) / kFileName; + std::ofstream output_stream(path); + WriteInvalidFile({kTestRecord}, output_stream); + output_stream.close(); + + std::ifstream is(path); + AvroStreamReader record_reader(is); + + testing::MockFunction record_callback; + EXPECT_CALL(record_callback, Call).Times(0); + auto status = + record_reader.ReadStreamRecords(record_callback.AsStdFunction()); + EXPECT_FALSE(status.ok()); +} } // namespace } // namespace kv_server diff --git a/public/data_loading/readers/riegeli_stream_io.h b/public/data_loading/readers/riegeli_stream_io.h index d863f4e1..b15ea78f 100644 --- a/public/data_loading/readers/riegeli_stream_io.h +++ b/public/data_loading/readers/riegeli_stream_io.h @@ -27,16 +27,17 @@ #include "absl/base/optimization.h" #include "absl/cleanup/cleanup.h" +#include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" #include "components/telemetry/server_definition.h" -#include "glog/logging.h" #include "public/data_loading/readers/stream_record_reader.h" #include "public/data_loading/riegeli_metadata.pb.h" #include "riegeli/bytes/istream_reader.h" #include "riegeli/records/record_reader.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { @@ -57,7 +58,8 @@ class RiegeliStreamReader : public StreamRecordReader { riegeli::RecordsMetadata metadata; if (!reader_.ReadMetadata(metadata)) { if (reader_.ok()) { - return absl::UnavailableError("Metadata not found"); + return absl::UnavailableError( + "Metadata not found. Please ensure metadata is set properly."); } return reader_.status(); } @@ -74,7 +76,9 @@ class RiegeliStreamReader : public StreamRecordReader { RecordT record; absl::Status overall_status; while (reader_.ReadRecord(record)) { - overall_status.Update(callback(record)); + const auto callback_status = callback(record); + LOG_IF(WARNING, !callback_status.ok()); + overall_status.Update(callback_status); } if (!overall_status.ok()) { LOG(ERROR) << overall_status; @@ -247,7 +251,10 @@ ConcurrentStreamRecordReader::BuildShards() { template absl::Status ConcurrentStreamRecordReader::ReadStreamRecords( const std::function& callback) { - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder< + ServerSafeMetricsContext, + kConcurrentStreamRecordReaderReadStreamRecordsLatency> + latency_recorder(KVServerContextMap()->SafeMetric()); auto shards = BuildShards(); if (!shards.ok() || shards->empty()) { return shards.status(); @@ -285,14 +292,9 @@ absl::Status ConcurrentStreamRecordReader::ReadStreamRecords( total_records_read += curr_shard_result->num_records_read; prev_shard_result = curr_shard_result; } - auto duration = absl::Now() - start_time; VLOG(2) << "Done reading " << total_records_read << " records in " - << absl::ToDoubleMilliseconds(duration) << " ms."; - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); + << absl::ToDoubleMilliseconds(latency_recorder.GetLatency()) + << " ms."; return absl::OkStatus(); } @@ -303,7 +305,10 @@ ConcurrentStreamRecordReader::ReadShardRecords( const std::function& record_callback) { VLOG(2) << "Reading shard: " << "[" << shard.start_pos << "," << shard.end_pos << "]"; - auto start_time = absl::Now(); + ScopeLatencyMetricsRecorder< + ServerSafeMetricsContext, + kConcurrentStreamRecordReaderReadShardRecordsLatency> + latency_recorder(KVServerContextMap()->SafeMetric()); auto record_stream = stream_factory_(); riegeli::RecordReader> record_reader( riegeli::IStreamReader(&record_stream->Stream()), @@ -334,15 +339,10 @@ ConcurrentStreamRecordReader::ReadShardRecords( } shard_result.next_shard_first_record_pos = next_record_pos; shard_result.num_records_read = num_records_read; - auto duration = absl::Now() - start_time; VLOG(2) << "Done reading " << num_records_read << " records in shard: [" << shard.start_pos << "," << shard.end_pos << "] in " - << absl::ToDoubleMilliseconds(duration) << " ms."; - LogIfError( - KVServerContextMap() - ->SafeMetric() - .LogHistogram( - absl::ToDoubleMicroseconds(duration))); + << absl::ToDoubleMilliseconds(latency_recorder.GetLatency()) + << " ms."; return shard_result; } diff --git a/public/data_loading/readers/riegeli_stream_io_test.cc b/public/data_loading/readers/riegeli_stream_io_test.cc index e231f034..7bb4998a 100644 --- a/public/data_loading/readers/riegeli_stream_io_test.cc +++ b/public/data_loading/readers/riegeli_stream_io_test.cc @@ -20,6 +20,7 @@ #include #include +#include "absl/log/check.h" #include "absl/status/status.h" #include "gmock/gmock.h" #include "google/protobuf/text_format.h" @@ -30,7 +31,6 @@ #include "riegeli/bytes/ostream_writer.h" #include "riegeli/bytes/string_writer.h" #include "riegeli/records/record_writer.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { @@ -268,6 +268,7 @@ class NonSeekingStringBlobStream : public RecordStream { }; TEST(ConcurrentStreamRecordReaderTest, FailsToReadNonSeekingStream) { + kv_server::InitMetricsContextMap(); std::string content; auto writer = riegeli::RecordWriter(riegeli::StringWriter(&content), riegeli::RecordWriterBase::Options()); diff --git a/public/data_loading/readers/stream_record_reader_factory.h b/public/data_loading/readers/stream_record_reader_factory.h index 5821666f..3910d749 100644 --- a/public/data_loading/readers/stream_record_reader_factory.h +++ b/public/data_loading/readers/stream_record_reader_factory.h @@ -20,7 +20,7 @@ #include #include "public/data_loading/readers/stream_record_reader.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { diff --git a/public/data_loading/record_utils.cc b/public/data_loading/record_utils.cc index 8f30922f..75d1ee4c 100644 --- a/public/data_loading/record_utils.cc +++ b/public/data_loading/record_utils.cc @@ -14,9 +14,9 @@ #include "public/data_loading/record_utils.h" +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/public/data_loading/records_utils.cc b/public/data_loading/records_utils.cc index eeb14eb6..73c42b99 100644 --- a/public/data_loading/records_utils.cc +++ b/public/data_loading/records_utils.cc @@ -16,8 +16,8 @@ #include +#include "absl/log/log.h" #include "absl/status/statusor.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/public/data_loading/writers/BUILD.bazel b/public/data_loading/writers/BUILD.bazel index 1a1e0f8f..3c3189bb 100644 --- a/public/data_loading/writers/BUILD.bazel +++ b/public/data_loading/writers/BUILD.bazel @@ -33,7 +33,7 @@ cc_library( deps = [ ":delta_record_writer", "//public/data_loading:records_utils", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_riegeli//riegeli/bytes:ostream_writer", @@ -48,7 +48,7 @@ cc_library( ":delta_record_writer", "//public/data_loading:records_utils", "@avro//:avrocpp", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", ], @@ -64,7 +64,6 @@ cc_test( "//public/data_loading/readers:riegeli_stream_record_reader_factory", "@com_google_absl//absl/status:statusor", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) @@ -102,7 +101,7 @@ cc_library( deps = [ "//public/data_loading:records_utils", "//public/sharding:sharding_function", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", @@ -127,7 +126,7 @@ cc_library( hdrs = ["delta_record_limiting_file_writer.h"], deps = [ ":delta_record_writer", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_riegeli//riegeli/bytes:fd_writer", diff --git a/public/data_loading/writers/avro_delta_record_stream_writer.h b/public/data_loading/writers/avro_delta_record_stream_writer.h index 7e0b663e..c816162f 100644 --- a/public/data_loading/writers/avro_delta_record_stream_writer.h +++ b/public/data_loading/writers/avro_delta_record_stream_writer.h @@ -22,9 +22,9 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "glog/logging.h" #include "public/data_loading/records_utils.h" #include "public/data_loading/writers/delta_record_writer.h" #include "third_party/avro/api/DataFile.hh" diff --git a/public/data_loading/writers/delta_record_limiting_file_writer.cc b/public/data_loading/writers/delta_record_limiting_file_writer.cc index 415ca5fc..0db8e406 100644 --- a/public/data_loading/writers/delta_record_limiting_file_writer.cc +++ b/public/data_loading/writers/delta_record_limiting_file_writer.cc @@ -14,7 +14,7 @@ #include "public/data_loading/writers/delta_record_limiting_file_writer.h" -#include "glog/logging.h" +#include "absl/log/log.h" namespace kv_server { diff --git a/public/data_loading/writers/delta_record_stream_writer.h b/public/data_loading/writers/delta_record_stream_writer.h index df08766e..4567f559 100644 --- a/public/data_loading/writers/delta_record_stream_writer.h +++ b/public/data_loading/writers/delta_record_stream_writer.h @@ -21,9 +21,9 @@ #include #include +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "glog/logging.h" #include "public/data_loading/records_utils.h" #include "public/data_loading/writers/delta_record_writer.h" #include "riegeli/bytes/ostream_writer.h" diff --git a/public/data_loading/writers/delta_record_stream_writer_test.cc b/public/data_loading/writers/delta_record_stream_writer_test.cc index 771f37c1..08c71eaf 100644 --- a/public/data_loading/writers/delta_record_stream_writer_test.cc +++ b/public/data_loading/writers/delta_record_stream_writer_test.cc @@ -24,7 +24,6 @@ #include "public/data_loading/readers/riegeli_stream_io.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" #include "public/data_loading/records_utils.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/public/query/cpp/BUILD.bazel b/public/query/cpp/BUILD.bazel index 0ca5e2d7..d0298d21 100644 --- a/public/query/cpp/BUILD.bazel +++ b/public/query/cpp/BUILD.bazel @@ -23,7 +23,7 @@ cc_library( deps = [ "//public/query/v2:get_values_v2_cc_proto", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) diff --git a/public/query/cpp/client_utils.cc b/public/query/cpp/client_utils.cc index 267a22a8..84eb0168 100644 --- a/public/query/cpp/client_utils.cc +++ b/public/query/cpp/client_utils.cc @@ -15,7 +15,7 @@ #include "public/query/cpp/client_utils.h" #include "google/protobuf/util/json_util.h" -#include "src/cpp/util/status_macro/status_macros.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server { diff --git a/public/query/v2/BUILD.bazel b/public/query/v2/BUILD.bazel index 0f11e3e9..ce6623c7 100644 --- a/public/query/v2/BUILD.bazel +++ b/public/query/v2/BUILD.bazel @@ -37,6 +37,7 @@ proto_library( "@com_google_googleapis//google/api:httpbody_proto", "@com_google_googleapis//google/rpc:status_proto", "@com_google_protobuf//:struct_proto", + "@google_privacysandbox_servers_common//src/logger:logger_proto", ], ) diff --git a/public/query/v2/get_values_v2.proto b/public/query/v2/get_values_v2.proto index ca13819e..f161711d 100644 --- a/public/query/v2/get_values_v2.proto +++ b/public/query/v2/get_values_v2.proto @@ -21,6 +21,7 @@ import "google/api/httpbody.proto"; import "google/protobuf/struct.proto"; import "google/rpc/status.proto"; import "public/api_schema.proto"; +import "src/logger/logger.proto"; // Key Value Service API V2. // Spec: @@ -107,6 +108,10 @@ message GetValuesRequest { // Metadata that is useful for all partitions in a request. google.protobuf.Struct metadata = 2; repeated RequestPartition partitions = 3; + // Context useful for logging and tracing requests + privacy_sandbox.server_common.LogContext log_context = 4; + // Consented debugging configuration + privacy_sandbox.server_common.ConsentedDebugConfiguration consented_debug_config = 5; } message ResponsePartition { diff --git a/public/test_util/BUILD.bazel b/public/test_util/BUILD.bazel index 2444921e..f5ee9f3f 100644 --- a/public/test_util/BUILD.bazel +++ b/public/test_util/BUILD.bazel @@ -34,6 +34,5 @@ cc_library( "//public/data_loading/readers:riegeli_stream_io", "//public/data_loading/readers:stream_record_reader", "@com_google_googletest//:gtest", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) diff --git a/public/test_util/mocks.h b/public/test_util/mocks.h index 1a9e192d..936a409c 100644 --- a/public/test_util/mocks.h +++ b/public/test_util/mocks.h @@ -23,7 +23,6 @@ #include "public/data_loading/readers/riegeli_stream_io.h" #include "public/data_loading/readers/stream_record_reader.h" #include "public/data_loading/riegeli_metadata.pb.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { diff --git a/public/udf/BUILD.bazel b/public/udf/BUILD.bazel index 70a056e5..51ed56ca 100644 --- a/public/udf/BUILD.bazel +++ b/public/udf/BUILD.bazel @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_proto_library") +load("@google_privacysandbox_servers_common//src/roma/tools/api_plugin:roma_api.bzl", "declare_roma_api", "js_proto_library") load("@rules_cc//cc:defs.bzl", "cc_library", "cc_proto_library") load("@rules_proto//proto:defs.bzl", "proto_library") @@ -30,12 +30,16 @@ cc_proto_library( deps = [":binary_get_values_proto"], ) -# Library that can be added as a dep from closure_js_libary or *_js_binary rules -closure_js_proto_library( +kv_api = declare_roma_api( + cc_protos = [":binary_get_values_cc_proto"], + proto_basename = "binary_get_values", + protos = [":binary_get_values_proto"], +) + +# Library that can be added as a dep from closure_js_library or *_js_binary rules +js_proto_library( name = "binary_get_values_js_proto", - srcs = ["binary_get_values.proto"], - import_style = "IMPORT_COMMONJS", - protocbin = "@com_google_protobuf_for_closure//:protoc", + roma_api = kv_api, ) cc_library( diff --git a/public/udf/constants.h b/public/udf/constants.h index 8cf6e6dc..8a7215dc 100644 --- a/public/udf/constants.h +++ b/public/udf/constants.h @@ -50,10 +50,35 @@ function getKeyGroupOutputs(udf_arguments) { return keyGroupOutputs; } +function handlePas(udf_arguments) { + if (udf_arguments.length != 1) { + const error_message = + 'For PAS default UDF exactly one argument should be provided, but was provided ' + udf_arguments.length; + console.error(error_message); + throw new Error(error_message); + } + const kv_result = JSON.parse(getValues(udf_arguments[0])); + if (kv_result.hasOwnProperty("kvPairs")) { + return kv_result.kvPairs; + } + const error_message = "Error executing handle PAS:" + + JSON.stringify(kv_result); + console.error(error_message); + throw new Error(error_message); +} -function HandleRequest(executionMetadata, ...udf_arguments) { +function handlePA(udf_arguments) { const keyGroupOutputs = getKeyGroupOutputs(udf_arguments); - return {keyGroupOutputs, udfOutputApiVersion: 1}; + return { keyGroupOutputs, udfOutputApiVersion: 1 }; +} + +function HandleRequest(executionMetadata, ...udf_arguments) { + if(executionMetadata.requestMetadata && + executionMetadata.requestMetadata.is_pas) { + console.log('Executing PAS branch'); + return handlePas(udf_arguments); + } + return handlePA(udf_arguments); } )"; diff --git a/third_party_deps/latency_benchmark_requirements.txt b/third_party_deps/latency_benchmark_requirements.txt new file mode 100644 index 00000000..aa1d793b --- /dev/null +++ b/third_party_deps/latency_benchmark_requirements.txt @@ -0,0 +1,6 @@ +numpy==1.25.1 +python-dateutil==2.8.2 +pytz==2020.1 +pandas==2.2.0 +tzdata==2022.7 +six diff --git a/third_party_deps/python_deps.bzl b/third_party_deps/python_deps.bzl index 6573d202..130255d3 100644 --- a/third_party_deps/python_deps.bzl +++ b/third_party_deps/python_deps.bzl @@ -19,3 +19,8 @@ def python_repositories(): name = "word2vec", requirements_lock = "//third_party_deps:word2vec_requirements.txt", ) + + pip_parse( + name = "latency_benchmark", + requirements_lock = "//third_party_deps:latency_benchmark_requirements.txt", + ) diff --git a/third_party_deps/rules_closure_repositories.bzl b/third_party_deps/rules_closure_repositories.bzl deleted file mode 100644 index 8c26eab1..00000000 --- a/third_party_deps/rules_closure_repositories.bzl +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -def rules_closure_repositories(): - # This is currently needed for closure_js_proto_library, - # since the server is using protobuf 3.23*., which no longer has javascript support. - # rules_closure uses this version of protoc. - # To make `closure_js_proto_library` work, we need to pass - # closure_js_proto_library( - # ... - # protocbin = "@com_google_protobuf_for_closure//:protoc" - # ) - http_archive( - name = "com_google_protobuf_for_closure", - strip_prefix = "protobuf-3.19.1", - sha256 = "87407cd28e7a9c95d9f61a098a53cf031109d451a7763e7dd1253abf8b4df422", - urls = [ - "https://github.com/protocolbuffers/protobuf/archive/v3.19.1.tar.gz", - ], - ) diff --git a/tools/bidding_auction_data_generator/BUILD.bazel b/tools/bidding_auction_data_generator/BUILD.bazel index 806916fe..41a29b1a 100644 --- a/tools/bidding_auction_data_generator/BUILD.bazel +++ b/tools/bidding_auction_data_generator/BUILD.bazel @@ -63,8 +63,8 @@ cc_library( hdrs = ["value_fetch_util.h"], deps = [ "//public/query:get_values_cc_grpc", - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_protobuf//:protobuf", "@curl", @@ -76,7 +76,7 @@ cc_library( srcs = ["http_url_fetch_client.cc"], hdrs = ["http_url_fetch_client.h"], deps = [ - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@curl", @@ -91,9 +91,9 @@ cc_library( ":http_url_fetch_client", ":value_fetch_util", "//public/query:get_values_cc_grpc", - "@com_github_google_glog//:glog", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_protobuf//:protobuf", @@ -124,7 +124,6 @@ cc_test( "//public/data_loading/readers:riegeli_stream_record_reader_factory", "//public/data_loading/writers:delta_record_stream_writer", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", ], ) diff --git a/tools/bidding_auction_data_generator/bidding_auction_data_cli.cc b/tools/bidding_auction_data_generator/bidding_auction_data_cli.cc index 420f9eba..eb582a3f 100644 --- a/tools/bidding_auction_data_generator/bidding_auction_data_cli.cc +++ b/tools/bidding_auction_data_generator/bidding_auction_data_cli.cc @@ -20,7 +20,7 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" -#include "glog/logging.h" +#include "absl/log/log.h" #include "public/data_loading/filename_utils.h" #include "custom_audience_data_parser.h" diff --git a/tools/bidding_auction_data_generator/delta_key_value_writer_test.cc b/tools/bidding_auction_data_generator/delta_key_value_writer_test.cc index e38ba313..0183218d 100644 --- a/tools/bidding_auction_data_generator/delta_key_value_writer_test.cc +++ b/tools/bidding_auction_data_generator/delta_key_value_writer_test.cc @@ -21,7 +21,6 @@ #include "gtest/gtest.h" #include "public/data_loading/readers/delta_record_stream_reader.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" -#include "src/cpp/telemetry/mocks.h" namespace kv_server { namespace { diff --git a/tools/bidding_auction_data_generator/http_url_fetch_client.cc b/tools/bidding_auction_data_generator/http_url_fetch_client.cc index 6e88aa05..1c09e6a4 100644 --- a/tools/bidding_auction_data_generator/http_url_fetch_client.cc +++ b/tools/bidding_auction_data_generator/http_url_fetch_client.cc @@ -19,9 +19,9 @@ #include #include +#include "absl/log/log.h" #include "absl/status/statusor.h" #include "curl/multi.h" -#include "glog/logging.h" namespace kv_server { namespace { diff --git a/tools/bidding_auction_data_generator/http_value_retriever.cc b/tools/bidding_auction_data_generator/http_value_retriever.cc index 3c4c3c2f..6607d62b 100644 --- a/tools/bidding_auction_data_generator/http_value_retriever.cc +++ b/tools/bidding_auction_data_generator/http_value_retriever.cc @@ -24,9 +24,9 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "glog/logging.h" #include "google/protobuf/util/json_util.h" #include "public/query/get_values.pb.h" #include "tools/bidding_auction_data_generator/value_fetch_util.h" diff --git a/tools/data_cli/BUILD.bazel b/tools/data_cli/BUILD.bazel index 2b68fcb2..8c5bdd40 100644 --- a/tools/data_cli/BUILD.bazel +++ b/tools/data_cli/BUILD.bazel @@ -15,6 +15,7 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") package(default_visibility = [ + "//docs/protected_app_signals:__subpackages__", "//getting_started:__subpackages__", "//production/packaging/tools:__subpackages__", "//testing:__subpackages__", @@ -33,5 +34,7 @@ cc_binary( "//tools/data_cli/commands:generate_snapshot_command", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", ], ) diff --git a/tools/data_cli/commands/BUILD.bazel b/tools/data_cli/commands/BUILD.bazel index 09d1490a..774bb3f0 100644 --- a/tools/data_cli/commands/BUILD.bazel +++ b/tools/data_cli/commands/BUILD.bazel @@ -45,12 +45,12 @@ cc_library( "//public/data_loading/writers:delta_record_stream_writer", "//public/data_loading/writers:delta_record_writer", "//public/sharding:sharding_function", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/memory", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) diff --git a/tools/data_cli/commands/format_data_command.cc b/tools/data_cli/commands/format_data_command.cc index 47b05de9..6ea65fe4 100644 --- a/tools/data_cli/commands/format_data_command.cc +++ b/tools/data_cli/commands/format_data_command.cc @@ -31,7 +31,7 @@ #include "public/data_loading/writers/avro_delta_record_stream_writer.h" #include "public/data_loading/writers/delta_record_stream_writer.h" #include "public/sharding/sharding_function.h" -#include "src/cpp/util/status_macro/status_macros.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server { namespace { diff --git a/tools/data_cli/commands/generate_snapshot_command.cc b/tools/data_cli/commands/generate_snapshot_command.cc index ae4a4bfa..7072a9a4 100644 --- a/tools/data_cli/commands/generate_snapshot_command.cc +++ b/tools/data_cli/commands/generate_snapshot_command.cc @@ -36,7 +36,7 @@ #include "public/data_loading/readers/delta_record_stream_reader.h" #include "public/data_loading/riegeli_metadata.pb.h" #include "public/sharding/sharding_function.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/telemetry_provider.h" namespace kv_server { namespace { diff --git a/tools/data_cli/data_cli.cc b/tools/data_cli/data_cli.cc index f6c79fcc..f7ec1c91 100644 --- a/tools/data_cli/data_cli.cc +++ b/tools/data_cli/data_cli.cc @@ -19,8 +19,10 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "components/util/platform_initializer.h" -#include "glog/logging.h" #include "tools/data_cli/commands/command.h" #include "tools/data_cli/commands/format_data_command.h" #include "tools/data_cli/commands/generate_snapshot_command.h" @@ -134,18 +136,19 @@ bool IsSupportedCommand(std::string_view command) { // Sample run using bazel: // -// GLOG_logtostderr=1 GLOG_v=3 bazel run \ +// bazel run \ // //tools/data_cli:data_cli \ // --//:instance=local --//:platform=local -- \ // format_data \ // --input_file=/data/DELTA_1689344645643610 \ // --input_format=DELTA \ // --output_format=CSV \ -// --output_file=/data/DELTA_1689344645643610.csv +// --output_file=/data/DELTA_1689344645643610.csv \ +// --v=3 --stderrthreshold=0 int main(int argc, char** argv) { kv_server::PlatformInitializer initializer; - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); absl::SetProgramUsageMessage(kUsageMessage); const std::vector commands = absl::ParseCommandLine(argc, argv); if (commands.size() < 2) { diff --git a/tools/latency_benchmarking/BUILD.bazel b/tools/latency_benchmarking/BUILD.bazel new file mode 100644 index 00000000..f3025200 --- /dev/null +++ b/tools/latency_benchmarking/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_python//python:defs.bzl", "py_binary") + +py_binary( + name = "generate_requests", + srcs = ["generate_requests.py"], + tags = ["manual"], +) + +py_binary( + name = "create_csv_summary", + srcs = ["create_csv_summary.py"], + deps = [ + "@latency_benchmark_pandas//:pkg", + ], +) + +py_binary( + name = "merge_csvs", + srcs = ["merge_csvs.py"], + deps = [ + "@latency_benchmark_pandas//:pkg", + ], +) diff --git a/tools/latency_benchmarking/README.md b/tools/latency_benchmarking/README.md new file mode 100644 index 00000000..6372288d --- /dev/null +++ b/tools/latency_benchmarking/README.md @@ -0,0 +1,405 @@ +# Latency Benchmarking Tool + +## Requirements + +- Install ghz + +The `run_benchmarks` script uses [ghz](https://ghz.sh/docs/intro). + +Follow instructions to [install ghz](https://ghz.sh/docs/install) and make sure it is installed +correctly: + +```sh +ghz -v +``` + +- Follow deployment guides + +To use the tool you will need to have a complete deployment setup. + +Please read through the deployment guide for the relevant cloud provider +([AWS](/docs/deployment/deploying_on_aws.md) or [GCP](/docs/deployment/deploying_on_gcp.md)). + +At the minimum, you will need to go through the steps up until the `terraform init` command. +Ideally, follow the entire guide and make sure your deployment setup works. + +## Tools + +Both the `run_benchmarks` and `deploy_and_benchmark` scripts are meant as a convenience to run +benchmarks against the server with different parameters/configurations. Instead of running multiple +iterations of benchmarks manually, the scripts take certain input parameters, run benchmarks, and +summarize the result. + +> Note: The script does not inspect the server's response. Should the server time out or not +> respond, the script currently does not attempt to rerun the benchmarks, it will simply continue. +> To find out which benchmarks need to be rerun, inspect the summary csvs. + +### run_benchmarks + +This script assumes that a server has already been deployed. + +It creates one or multiple requests, runs ghz for each request, and outputs the summary in a CSV +file. + +The request body is designed to work with the server's [default UDF](/public/udf/constants.h). To +ensure the script works, any custom UDF will need to be able to handle the same inputs (outputs are +not affected). + +```js +{"metadata": {...}, "partitions": [{"id": 0, "compressionGroupId": 0, "arguments": [{"tags": ["custom", "keys"], "data": }]}]} +``` + +The tool takes a directory of snapshot files or delta files with key value mutations of type +`Update`. + +For each snapshot file in `snapshot-dir`, the script reads its keys and may generate multiple +requests, one for each element in `number-of-lookup-keys-list`. The number of lookup keys element +indicates how many keys from the snapshot file should be included in a request. + +For example, given a `snapshot-dir` with 2 snapshot files and a `number-of-lookup-keys-list` with 3 +elements, the script will generate 6 requests. + +Each benchmark iteration will about 40 seconds to execute. In total, the script would take about 4-5 +minutes to complete the benchmark + 1-2 minutes for pre- and post processing. + +#### Reference + +Usage: + +```sh +./tools/latency_benchmarking/run_benchmarks +``` + +Flags: + +- `--server-address` + + Required. gRPC host and port. + + Example: `--server-address my-server:8443` + +- `--snapshot-dir` + + Required if `--snapshot-csv-dir` is not provided. Full path to a directory of snapshot files. + These snapshot files are converted to CSVs using the + [`data cli`](/docs/data_loading/loading_data.md). The keys from the snapshot file are used to + create requests. + +- `--snapshot-csv-dir` + + Required if `--snapshot-dir` is not provided. Full path to a directory of only snapshot CSV + files (i.e. converted using the [`data cli`](/docs/data_loading/loading_data.md)). This avoids + doing the conversion in the tool, which saves some time for multiple runs on the same snapshot + files. + +- `--number-of-lookup-keys-list` (Optional) + + A list of number of keys (in quotes, as a string) to include in a request. The tool will iterate + through snapshot csvs and the `--number-of-lookup-keys-list` to construct a request for each + combination. Default is `"1 5 10"`. + + Example: `--number-of-lookup-keys-list "1 10"` + +- `--benchmark-duration` (Optional) + + How long each benchmark iteration should run for. Default is `"5s"`. + +- `--ghz-tags` (Optional) + + Tags to include for all ghz runs. The csv summary will include this tag for all iterations. + + Example: `-- ghz-tags '{"my_tag":"my_value"}'` + +- `--request-metadata-json` (Optional) + + The request metadata json object to use for all requests. Default is empty `{}`. + + Example: `--request-metadata-json '{"metadata_key":"metadata_value"}'` + +- `--filter-snapshot-by-sets` (Optional) + + Whether to filter snapshot csvs using `value_type=string_set` only. This will create requests + that only include keys of sets. Default is false. + +#### Example + +Start from the workspace root. + +```sh +SNAPSHOT_DIR=/path/to/snapshot/dir +NUMBER_OF_LOOKUP_KEYS_LIST="1 10 100" +SERVER_ADDRESS="demo.kv-server.your-domain.example:8443" +./tools/latency_benchmarking/run_benchmarks \ +--server-address ${SERVER_ADDRESS} \ +--snapshot-dir ${SNAPSHOT_DIR} \ +--number-of-lookup-keys-list "${NUMBER_OF_LOOKUP_KEYS_LIST}" +``` + +The summary can be found in +`dist/tools/latency_benchmarking/output//summary.csv`. + +### deploy_and_benchmark + +This script will deploy a terraform configuration and call the `run_benchmarks` script. + +The tool will _not_ upload key value delta/snapshot files. Please ensure those are already loaded +into the blob storage bucket that the server will be reading from. + +It includes almost all of the `run_benchmarks` flags in addition to more deployment parameters to +iterate through. + +- If provided with a terraform overrides file with sets of terraform variable overrides, the + script will deploy once per set of variables and run benchmarks. +- If given a directory with UDF deltas, it will upload each UDF to the data bucket, and run + benchmarks against it. +- If given a json lines file with request metadata, it will iterate through each request metadata + and run benchmarks against it. + +> The number of iterations can increase dramatically with each added parameter. For example, given 3 +> sets of terraform overrides, 3 UDF deltas, and 3 request metadata jsons, `run_benchmarks` will +> execute 27 times. If in addition to that, the `number-of-lookup-keys` has 3 elements and +> `snapshot-dir` has 3 files, the script will be running 243 iterations of ghz, once for each +> combination of parameters. + +#### Reference + +Usage: + +```sh +./tools/latency_benchmarking/deploy_and_benchmark +``` + +Flags: + +- `--cloud-provider` + + Required. Cloud provider. Options: "aws" or "gcp" + +- `--tf-var-file` + + Required. Full path to `tfvars.json` file. + +- `--tf-backend-config` + + Required. Full path to `backend.conf` file. + +- `--server-url` + + Required. URL to deployed server. + + Example: "" + +- `--snapshot-dir` + + Required. Full path to a directory of snapshot files. The keys from the snapshot file are used + to create requests. + +- `--csv-output` (Optional) + + Full path to csv output. Contains a summary of all ghz runs. + +- `--tf-overrides` (Optional) + + File with terraform variable overrides. Each line in the file counts as a set of variable + overrides. If provided, the tool will deploy once per line. The expected format for each line is + a comma-separated list of terraform variable overrides, e.g. + `instance_type=c5.4xlarge,enclave_cpu_count=12` + + Example: `/tools/latency_benchmarking/example/aws_tf_overrides.txt` + +- `--udf-delta-dir` (Optional) + + Full path to directory of udf delta files to use for benchmarking. The tool will upload each + file in the directory to the given `data-bucket` and run benchmarks after. + + Note that all udf deltas are uploaded once per deployment and removed by the end of each + deployment. Since the server does not restart in between udf uploads, it will only pick up udf + deltas if the logical_commit_times and versions are set correctly. + + See the [udf delta documentation](/docs/generating_udf_files.md) for more info. + +- `--data-bucket` (Optional) + + The data bucket to upload the udf files too. This should be the same data bucket that the server + is reading delta files from. + +- `--request-metadata-json-file` (Optional) + + Path to JSON lines file to iterate through to generate requests. + + Example: `/tools/latency_benchmarking/example/request_metadata.jsonl` + +- `--number-of-lookup-keys-list` (Optional) + + A list of number of keys (in quotes, as a string) to include in a request. The tool will iterate + through snapshot csvs and the `--number-of-lookup-keys-list` to construct a request for each + combination. Default is `"1 10 50 100"`. + + Example: `--number-of-lookup-keys-list "1 10"` + +- `--minimum-server-wait-secs` (Optional) + + The amount of time that the tool should wait after the `terraform apply` command before checking + whether the server is healthy. + + If this is too low, the tool may still be pinging the previous deployment's server, since it may + take a while to tear down the instance. + + Default is `90`. + +- `--extra-server-wait-timeout` (Optional) + + The amount of time that the tool should continue checking server health on top of the + `minimum-server-wait-secs`. + + The tool will continue pinging the server until the timeout is reached or the server is healthy. + + If the delta files + + Default is `5m`. + + Examples: `5m`, `600s` + +- `--benchmark-duration` (Optional) + + How long each benchmark iteration should run for. Default is `"5s"`. + +- `--ghz-tags` (Optional) + + Tags to include for all ghz runs. The csv summary will include this tag for all iterations. + + Example: `-- ghz-tags '{"my_tag":"my_value"}'` + +- `--cleanup-deployment` (Optional) + + Whether to call terraform destroy once the tool exits. Default false. + +- `--filter-snapshot-by-sets` (Optional) + + Whether to filter snapshot csvs using `value_type=string_set` only. This will create requests + that only include keys of sets. Default is false. + +#### Example + +Start from the workspace root. + +1. Generate SNAPSHOT/DELTA files to upload to data storage: + + ```sh + ./tools/serving_data_generator/generate_test_riegeli_data + GENERATED_DELTA=/path/to/delta/dir/GENERATED_DELTA_FILE + ``` + + For AWS: + + ```sh + aws s3 cp $GENERATED_DELTA s3://bucket_name + ``` + + For GCP: + + ```sh + gcloud storage cp $GENERATED_DELTA gs://bucket_name + ``` + +1. Set your cloud provider + + AWS: + + ```sh + CLOUD_PROVIDER="aws" + ``` + + GCP: + + ```sh + CLOUD_PROVIDER="gcp" + ``` + +1. Provide a directory of SNAPSHOT (or DELTA) files with keys that should be sent in the request to + the server. This SNAPSHOT (or DELTA) file should only have `UPDATE` mutations. The tool will + iterate through each SNAPSHOT file, select keys from that file, and run benchmarks with those + keys. + + For this example, we'll use the generated DELTA files from step 1 + + ```sh + SNAPSHOT_DIR=/path/to/delta/dir/ + ``` + +1. Set up your terraform config files + + ```sh + TF_VAR_FILE=/path/to/my.tfvars.json + TF_BACKEND_CONFIG=/path/to/my.backend.conf + ``` + +1. Set the server url + + ```sh + SERVER_URL="https://demo.kv-server.your-domain.example/" + ``` + +1. (optional) Provide a file with sets of terraform variables to be overriden. For each set of + terraform variables, the script will `terraform apply` once with the given variables and run + benchmarks against the deployed server. The variable override file should have the following + format: + + - Each line should be in the form + + ```txt + variable_name1=variable_valueA,variable_name2=variable_valueB + ``` + + - Each line is considered a set of variables to be overriden in one `terraform apply` command + + - For an example, see `/latency_benchmarking/example/aws_tf_overrides.txt`. + + ```sh + TF_OVERRIDES=/path/to/tf_variable_overrides.txt + ``` + +1. (optional) Write UDFs If given a directory of UDF delta files, the tool iterates through each + one, uploads it to the given data bucket, runs benchmarks, then removes the UDF delta from the + data bucket. + + ```sh + UDF_DELTA_DIR=/path/to/udf_deltas/ + DATA_BUCKET=s3://bucket_name + ``` + +1. Run the script and wait for the result + + ```sh + ./tools/latency_benchmarking/deploy_and_benchmark \ + --cloud-provider ${CLOUD_PROVIDER} \ + --server-url ${SERVER_URL} \ + --snapshot-dir ${SNAPSHOT_DIR} \ + --tf-var-file ${TF_VAR_FILE} \ + --tf-backend-config ${TF_BACKEND_CONFIG} \ + --tf-overrides ${TF_OVERRIDES} \ + --csv-output ${PWD}/my_summary.csv + ``` + + The result will be in `my_summary.csv`. + + ```sh + ls my_summary.csv + ``` + +## Appendix + +### Things of note when deploying + +- Make sure the DELTA/SNAPSHOT files follow the expected logic for time stamps and naming, + otherwise the data may not load properly. See the + [data format specification](/docs/data_loading/data_format_specification.md) and + [udf delta doc](/docs/generating_udf_files.md) for more info. + +- Make sure the chosen instance sizes have enough memory to hold your data. + +- On AWS, some larger instances have multiple NUMA clusters. However, an enclave can only run on + one NUMA cluster. You may want to consider [sharding](/docs/sharding/sharding.md). + +- Depending on the number of iterations the script runs, it may take a long time to finish. Each + benchmark (ghz) iteration takes 40-60s + a few minutes of overhead per deployment. diff --git a/tools/latency_benchmarking/create_csv_summary.py b/tools/latency_benchmarking/create_csv_summary.py new file mode 100644 index 00000000..b5522bc0 --- /dev/null +++ b/tools/latency_benchmarking/create_csv_summary.py @@ -0,0 +1,78 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import argparse +import json +import pandas as pd + +""" +Iterates through ghz_output.json files and collects relevant data in a CSV file. +""" + + +def _ExtractGhzInfo(ghz_output_df: pd.DataFrame) -> pd.DataFrame: + keys = ["date", "count", "total", "average", "fastest", "slowest", "rps"] + prefixes = ("tags", "statusCodeDistribution") + return ghz_output_df.loc[ + :, + ghz_output_df.columns.str.startswith(prefixes) + | ghz_output_df.columns.isin(keys), + ] + + +def JsonToDataFrame(ghz_result_dir: str) -> pd.DataFrame: + """Reads a ghz_output.json files and outputs a dataframe with relevant columns. + + The dataframe will contain: + - metadata: date, user tags added to the ghz command + - overall stats: # requests, total time spent, rps, fastest, slowest + - status code distributions + + Example: + date count total average fastest slowest rps statusCodeDistribution.OK statusCodeDistribution.Unavailable + 0 2024-01-30T21:27:06Z 13130 5000416406 37281592 6313913 100341707 2625.781322 13021 109 + 0 2024-01-30T21:27:06Z 13130 5000416406 37281592 6313913 100341707 2625.781322 13021 109 + 0 2024-01-30T21:27:06Z 13130 5000416406 37281592 6313913 100341707 2625.781322 13021 109 + """ + output_dfs = [] + for root, dirs, files in os.walk(ghz_result_dir): + for name in files: + if name == "ghz_output.json": + fp = os.path.join(root, name) + try: + with open(fp, "r") as f: + json_data = json.loads(f.read()) + ghz_output_df = pd.json_normalize(json_data) + output_dfs.append(_ExtractGhzInfo(ghz_output_df)) + except FileNotFoundError: + print(f"File not found: {fp}") + return pd.concat(output_dfs) + + +def Main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--ghz-result-dir", + dest="ghz_result_dir", + type=str, + help="Directory containing subdirectories with ghz.json output files.", + ) + args = parser.parse_args() + output_df = JsonToDataFrame(args.ghz_result_dir) + output_df.to_csv(os.path.join(args.ghz_result_dir, "summary.csv"), index=False) + + +if __name__ == "__main__": + Main() diff --git a/tools/latency_benchmarking/deploy_and_benchmark b/tools/latency_benchmarking/deploy_and_benchmark new file mode 100755 index 00000000..730eacf8 --- /dev/null +++ b/tools/latency_benchmarking/deploy_and_benchmark @@ -0,0 +1,393 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o pipefail +set -o errexit + +# shellcheck disable=SC1090 +source ./builders/tools/builder.sh + +START=$(date +%s) +WORKSPACE="$(git rev-parse --show-toplevel)" + +# Script input params +EXTRA_SERVER_WAIT_TIMEOUT="5m" +MINIMUM_SERVER_WAIT_SECS=90 +NUMBER_OF_LOOKUP_KEYS_LIST="1 10 50 100" +GHZ_TAGS="{}" +FILTER_BY_SETS=0 + +# Additional GHZ tags per deployment/udf +DEPLOYMENT_GHZ_TAGS="{}" +UDF_GHZ_TAGS="{}" + +# Input to run_benchmark script +declare RUN_BENCHMARK_ARGS +BENCHMARK_DURATION="5s" + +# Deployment vars +declare SERVER_ADDRESS +declare SERVER_ENDPOINT + +# CSV I/O +declare -a BENCHMARK_CSVS +CSV_OUTPUT="${WORKSPACE}/dist/tools/latency_benchmarking/output/output.csv" +declare -r DOCKER_OUTPUT_CSV="/tmp/latency_benchmarking/output/deploy_and_benchmark/output.csv" +declare -r SNAPSHOT_CSV_DIR="${WORKSPACE}/dist/tools/latency_benchmarking/deploy_and_benchmark/snapshot_csvs/${START}" +declare -r DOCKER_SNAPSHOT_CSV_DIR="/tmp/latency_benchmarking/deploy_and_benchmark/snapshot_csvs/" +declare -r DOCKER_SNAPSHOT_DIR="/tmp/latency_benchmarking/deploy_and_benchmark/snapshots/" + +# run_benchmarks writes output to this directory +CSV_SUMMARY_INPUT_DIR="${WORKSPACE}/dist/tools/latency_benchmarking/output" +DOCKER_INPUT_DIR="/tmp/latency_benchmarking/output" +readonly DOCKER_INPUT_DIR + +DESTROY_INSTANCES=0 +TF_DEPLOY_SUCCESS=0 + +trap _destroy EXIT +function _destroy() { + if [[ ${DESTROY_INSTANCES} == 1 && ${TF_DEPLOY_SUCCESS} == 1 ]]; then + printf "Running terraform destroy\n" + builders/tools/terraform \ + -chdir="${WORKSPACE}"/production/terraform/"${CLOUD_PROVIDER}"/environments \ + destroy --var-file="${TF_VAR_FILE}" --auto-approve >/dev/null + fi +} + +trap _trap ERR +function _trap() { + local -r -i STATUS=$? + FAILED_COMMAND="${BASH_COMMAND}" + printf "Failed command: %s\n" "${FAILED_COMMAND}" + exit ${STATUS} +} + +function usage() { + local -r -i exitval=${1-1} + cat &>/dev/stderr < + [--cloud-provider] (Required) Cloud provider. Options: "aws" or "gcp" + [--tf-var-file] (Required) Full path to tfvars.json file. + [--tf-backend-config] (Required) Full path to tf backend.conf file. + [--server-url] (Required) URL of deployed server. + [--snapshot-dir] (Required) Full path to a directory of snapshot files. + [--csv-output] (Optional) Path to output file for summary of benchmarks. + [--tf-overrides] (Optional) Path to file with terraform variable overrides. + [--udf-delta-dir] (Optional) Full path to directory of udf delta files. + [--data-bucket] (Optional) Data bucket to upload the udf files too. + [--request-metadata-json-file] (Optional) Path to JSON lines file with request metadata. + [--number-of-lookup-keys-list] (Optional) List of number of keys to include in a request. + [--minimum-server-wait-secs] (Optional) Amount of time to wait before checking server health. + [--extra-server-wait-timeout] (Optional) Timeout for waiting for server to become healthy. + [--benchmark-duration] (Optional) Duration of each benchmark. Default "5s". + [--ghz-tags] (Optional) Tags to include in the ghz run. + [--cleanup-deployment] (Optional) Whether to call terraform destroy once the tool exits. + [--filter-snapshot-by-sets] (Optional) Whether to filter snapshots by sets when creating requests. +USAGE + # shellcheck disable=SC2086 + exit ${exitval} +} + +function convert_snapshots_to_csvs() { + mkdir -p "${SNAPSHOT_CSV_DIR}" + # Iterate through snapshot files and convert them to CSV + for SNAPSHOT_FILE in "${SNAPSHOT_DIR}"/*; do + SNAPSHOT_FILENAME=$(basename "${SNAPSHOT_FILE}") + EXTRA_DOCKER_RUN_ARGS+=" --volume ${SNAPSHOT_CSV_DIR}:${DOCKER_SNAPSHOT_CSV_DIR} --volume ${SNAPSHOT_DIR}:${DOCKER_SNAPSHOT_DIR} " \ + builders/tools/bazel-debian run //tools/data_cli:data_cli format_data \ + -- \ + --input_file "${DOCKER_SNAPSHOT_DIR}/${SNAPSHOT_FILENAME}" \ + --input_format DELTA \ + --output_file "${DOCKER_SNAPSHOT_CSV_DIR}/${SNAPSHOT_FILENAME}.csv" \ + --output_format CSV + done +} + +function set_benchmark_args() { + local -a RUN_BENCHMARK_GHZ_TAGS + RUN_BENCHMARK_GHZ_TAGS=$(jq -s -c 'add' <(echo "${GHZ_TAGS}") \ + <(echo "${DEPLOYMENT_GHZ_TAGS}") \ + <(echo "${UDF_GHZ_TAGS}")) + RUN_BENCHMARK_ARGS=( + --number-of-lookup-keys-list "${NUMBER_OF_LOOKUP_KEYS_LIST[@]}" + --server-address "${SERVER_ADDRESS}" + --ghz-tags "${RUN_BENCHMARK_GHZ_TAGS}" + --snapshot-csv-dir "${SNAPSHOT_CSV_DIR}" + --benchmark-duration "${BENCHMARK_DURATION}" + ) + if [[ -v "${REQUEST_METADATA_JSON}" ]]; then + RUN_BENCHMARK_ARGS+=( + --request-metadata-json "${REQUEST_METADATA_JSON}" + ) + fi + if [[ "${FILTER_BY_SETS}" = 1 ]]; then + RUN_BENCHMARK_ARGS+=( + --filter-snapshot-by-sets + ) + fi +} + +function run_benchmarks() { + if [[ -z "${REQUEST_METADATA_JSON_FILE}" ]]; then + set_benchmark_args + printf "BENCHMARK ARGS: %s\n" "${RUN_BENCHMARK_ARGS[*]}" + + local BENCHMARK_OUTPUT + BENCHMARK_OUTPUT=$(./tools/latency_benchmarking/run_benchmarks "${RUN_BENCHMARK_ARGS[@]}") + BENCHMARK_CSVS+=( + "$(echo "${BENCHMARK_OUTPUT}" | tail -n 1 2>&1 | tee /dev/tty)" + ) + else + while IFS= read -r REQUEST_METADATA_JSON; do + set_benchmark_args + printf "BENCHMARK ARGS: %s\n" "${RUN_BENCHMARK_ARGS[*]}" + + local BENCHMARK_OUTPUT + BENCHMARK_OUTPUT=$(./tools/latency_benchmarking/run_benchmarks "${RUN_BENCHMARK_ARGS[@]}") + BENCHMARK_CSVS+=( + "$(echo "${BENCHMARK_OUTPUT}" | tail -n 1 2>&1 | tee /dev/tty)" + ) + done < "${REQUEST_METADATA_JSON_FILE}" + fi +} + +function set_server_address() { + # Build HTTP server endpoint from tf output + local SERVER_HOSTNAME + SERVER_ENDPOINT="${SERVER_URL}/v1/getvalues?keys=hi" + # Build gRPC server address from tf output + SERVER_HOSTNAME=$([[ "${SERVER_URL}" =~ https://(.*) ]] && echo "${BASH_REMATCH[1]}") + SERVER_ADDRESS="${SERVER_HOSTNAME}:8443" +} + +function upload_file_to_bucket() { + if [[ "${CLOUD_PROVIDER}" == "aws" ]]; then + EXTRA_DOCKER_RUN_ARGS+=" --volume ${1}:/tmp/deltas/${1}" \ + builders/tools/aws-cli s3 cp "/tmp/deltas/${1}" "${2}" + elif [[ "${CLOUD_PROVIDER}" == "gcp" ]]; then + gcloud storage cp "${1}" "${2}" + else + echo "Cloud provider not supported" + exit 1 + fi +} + +function remove_file_from_bucket() { + if [[ "${CLOUD_PROVIDER}" == "aws" ]]; then + builders/tools/aws-cli s3 rm "${1}" + elif [[ "${CLOUD_PROVIDER}" == "gcp" ]]; then + gcloud storage rm "${1}" + else + echo "Cloud provider not supported" + exit 1 + fi +} + +function deploy_and_benchmark() { + printf "Running terraform init\n" + builders/tools/terraform \ + -chdir=production/terraform/"${CLOUD_PROVIDER}"/environments \ + init --backend-config="${TF_BACKEND_CONFIG}" \ + --var-file="${TF_VAR_FILE}" --reconfigure -input=false \ + >/dev/null + + printf "Running terraform apply with var file: %s\n" "${TF_VAR_FILE}" + printf "and var overrides: %s\n" "${VAR_OVERRIDES[*]}" + builders/tools/terraform \ + -chdir=production/terraform/"${CLOUD_PROVIDER}"/environments \ + apply --var-file="${TF_VAR_FILE}" "${VAR_OVERRIDES[@]}" \ + -auto-approve + TF_DEPLOY_SUCCESS=1 + printf "Done applying terraform, waiting for server to be ready\n" + + set_server_address + + # Wait for potential instance teardown before periodically checking if server is ready + sleep "${MINIMUM_SERVER_WAIT_SECS}" + + # Wait for server to be up + timeout --foreground "${EXTRA_SERVER_WAIT_TIMEOUT}" bash -c \ + "until curl --output /dev/null --silent --fail ${SERVER_ENDPOINT};do sleep 15; done" + printf "Server ready\n" + + # Iterate through each given UDF + # Upload delta file, run benchmarks, then remove it + if [[ -d "${UDF_DELTA_DIR}" ]]; then + for FILE in "${UDF_DELTA_DIR}"/*; do + local FILENAME + FILENAME=$(basename "${FILE}") + printf "Uploading UDF file %s to cloud storage %s\n" "${FILE}" "${DATA_BUCKET}" + upload_file_to_bucket "${FILE}" "${DATA_BUCKET}/${FILENAME}" + # Allow some time for server to pick up new UDF + sleep 60 + UDF_GHZ_TAGS="{ \"udf_delta_file\": \"${FILENAME}\" }" + run_benchmarks + printf "Removing file from cloud storage %s\n" "${DATA_BUCKET}/${FILENAME}" + remove_file_from_bucket "${DATA_BUCKET}/${FILENAME}" + done + else + run_benchmarks + fi +} + +function merge_benchmark_csvs() { + # Map csv summary files from run_benchmarks to docker volume paths + # so that we can access them from within builders/tools/bazel-debian + # i.e. csv_output_dir/summary.csv -> docker_mounted_dir/summary.csv + declare -a DOCKER_BENCHMARK_CSVS + for BENCHMARK_CSV in "${BENCHMARK_CSVS[@]}"; do + DOCKER_BENCHMARK_CSVS+=( + "${DOCKER_INPUT_DIR}${BENCHMARK_CSV#"${CSV_SUMMARY_INPUT_DIR}"}" + ) + done + touch "${CSV_OUTPUT}" + # Run merge_csvs python script + EXTRA_DOCKER_RUN_ARGS+=" --volume ${CSV_SUMMARY_INPUT_DIR}:${DOCKER_INPUT_DIR} --volume ${CSV_OUTPUT}:${DOCKER_OUTPUT_CSV} " \ + builders/tools/bazel-debian run //tools/latency_benchmarking:merge_csvs \ + -- \ + --csv-inputs "${DOCKER_BENCHMARK_CSVS[@]}" \ + --csv-output "${DOCKER_OUTPUT_CSV}" + + printf "Results in: %s\n" "${CSV_OUTPUT}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --tf-var-file) + TF_VAR_FILE="$2" + shift 2 || usage + ;; + --tf-backend-config) + TF_BACKEND_CONFIG="$2" + shift 2 || usage + ;; + --tf-overrides) + TF_OVERRIDES="$2" + shift 2 + ;; + --csv-output) + CSV_OUTPUT="$2" + shift 2 + ;; + --snapshot-dir) + SNAPSHOT_DIR="$2" + shift 2 + ;; + --udf-delta-dir) + UDF_DELTA_DIR="$2" + shift 2 + ;; + --data-bucket) + DATA_BUCKET="$2" + shift 2 + ;; + --extra-server-wait-timeout) + EXTRA_SERVER_WAIT_TIMEOUT="$2" + shift 2 + ;; + --number-of-lookup-keys-list) + NUMBER_OF_LOOKUP_KEYS_LIST="$2" + shift 2 + ;; + --benchmark-duration) + BENCHMARK_DURATION="$2" + shift 2 + ;; + --ghz-tags) + GHZ_TAGS="$2" + shift 2 + ;; + --minimum-server-wait-secs) + MINIMUM_SERVER_WAIT_SECS="$2" + shift 2 + ;; + --cloud-provider) + CLOUD_PROVIDER="$2" + shift 2 + ;; + --server-url) + SERVER_URL="$2" + shift 2 + ;; + --cleanup-deployment) + DESTROY_INSTANCES=1 + shift + ;; + --request-metadata-json-file) + REQUEST_METADATA_JSON_FILE="$2" + shift 2 + ;; + --filter-snapshot-by-sets) + FILTER_BY_SETS=1 + shift + ;; + -h | --help) usage 0 ;; + *) usage ;; + esac +done + +if [[ -z "${CLOUD_PROVIDER}" || ! ("${CLOUD_PROVIDER}" == "aws" || "${CLOUD_PROVIDER}" == "gcp") ]]; then + printf "cloud-provider must equal \"aws\" or \"gcp\"" + exit 1 +fi + +if [[ -z "${SERVER_URL}" ]]; then + printf "server-url must be provided" + exit 1 +fi + +# Check for SNAPSHOT_DIR. If not available, exit. +if [[ -z "${SNAPSHOT_DIR}" || ! -d "${SNAPSHOT_DIR}" ]]; then + printf "snapshot-dir not found:%s\n" "${SNAPSHOT_DIR}" + exit 1 +fi + +# Check that files, if provided, are readable +if [[ -v "${TF_OVERRIDES}" && ! -r "${TF_OVERRIDES}" ]]; then + printf "tf-overrides file not readable: %s\n" "${TF_OVERRIDES}" + exit 1 +fi + +if [[ -v "${REQUEST_METADATA_JSON_FILE}" && ! -r "${REQUEST_METADATA_JSON_FILE}" ]]; then + printf "request-metadata-json-file file not readable: %s\n" "${REQUEST_METADATA_JSON_FILE}" + exit 1 +fi + +convert_snapshots_to_csvs + +# No terraform variable overrides, deploy and benchmark without overrides +if [[ -z "${TF_OVERRIDES}" ]]; then + deploy_and_benchmark +else + # Terraform override file found: + # Each row defines a set of overrides for terraform variables. + # Pass the overrides to deploy_and_benchmark function and set + # them as tags in the ghz call. + while IFS=',' read -ra VARS; do + declare -a VAR_OVERRIDES=() + DEPLOYMENT_GHZ_TAGS="{}" + for VAR in "${VARS[@]}"; do + VAR_OVERRIDES+=(-var "${VAR}") + OVERRIDE_VAR_GHZ_TAG=$(echo "${VAR}" | jq -s -R 'split("\n") | .[0] | split("=") | {(.[0]): .[1]}') + DEPLOYMENT_GHZ_TAGS=$(echo "${DEPLOYMENT_GHZ_TAGS} ${OVERRIDE_VAR_GHZ_TAG}" | jq -s -c 'add') + done + deploy_and_benchmark + done <"${TF_OVERRIDES}" +fi + +# Benchmarks done, merge CSVs +merge_benchmark_csvs diff --git a/tools/latency_benchmarking/example/aws_tf_overrides.txt b/tools/latency_benchmarking/example/aws_tf_overrides.txt new file mode 100644 index 00000000..c37884e6 --- /dev/null +++ b/tools/latency_benchmarking/example/aws_tf_overrides.txt @@ -0,0 +1,6 @@ +instance_type=c5.4xlarge,enclave_cpu_count=12,enclave_memory_mib=24576,num_shards=1,udf_num_workers=12,instance_ami_id=ami-0c3ea8d5ff3ef70d2 +instance_type=c5.12xlarge,enclave_cpu_count=36,enclave_memory_mib=73728,num_shards=1,udf_num_workers=36,instance_ami_id=ami-0c3ea8d5ff3ef70d2 +instance_type=m5.4xlarge,enclave_cpu_count=12,enclave_memory_mib=49152,num_shards=1,udf_num_workers=12,instance_ami_id=ami-0c3ea8d5ff3ef70d2 +instance_type=m5.12xlarge,enclave_cpu_count=36,enclave_memory_mib=147456,num_shards=1,udf_num_workers=36,instance_ami_id=ami-0c3ea8d5ff3ef70d2 +instance_type=c5.4xlarge,enclave_cpu_count=12,enclave_memory_mib=24576,num_shards=2,udf_num_workers=12,instance_ami_id=ami-0c3ea8d5ff3ef70d2 +instance_type=m5.4xlarge,enclave_cpu_count=12,enclave_memory_mib=49152,num_shards=2,udf_num_workers=12,instance_ami_id=ami-0c3ea8d5ff3ef70d2 diff --git a/tools/latency_benchmarking/example/gcp_tf_overrides.txt b/tools/latency_benchmarking/example/gcp_tf_overrides.txt new file mode 100644 index 00000000..19c25231 --- /dev/null +++ b/tools/latency_benchmarking/example/gcp_tf_overrides.txt @@ -0,0 +1,2 @@ +machine_type=n2d-standard-4,num_shards=1,udf_num_workers=4 +machine_type=n2d-standard-8,num_shards=1,udf_num_workers=8 diff --git a/tools/latency_benchmarking/example/kv_data/BUILD.bazel b/tools/latency_benchmarking/example/kv_data/BUILD.bazel new file mode 100644 index 00000000..91bf4777 --- /dev/null +++ b/tools/latency_benchmarking/example/kv_data/BUILD.bazel @@ -0,0 +1,52 @@ +load("@bazel_skylib//rules:run_binary.bzl", "run_binary") + +package(default_visibility = [ + "//tools:__subpackages__", +]) + +num_kv_records = 100000 + +num_kv_set_records = 25000 + +num_values_in_set = 5 + +total_records = num_kv_records + num_kv_set_records + +# Rule to generate example KV delta that includes +# num_kv_records key value records and +# num_kv_set_records set records each of size num_values_in_set +[ + run_binary( + name = "generate_{}_delta".format(size), + outs = [ + "DELTA_100000000000000{}".format(num), + ], + args = [ + "--output_dir", + "/src/workspace", + "--output_filename", + "$(location DELTA_100000000000000{})".format(num), + "--key", + "value{}_".format(size), + "--num_records", + "{}".format(num_kv_records), + "--value_size", + "{}".format(size), + "--timestamp", + "{}".format(total_records * num), + "--generate_set_record", + "--set_value_key", + "setvalue{}_".format(size), + "--num_set_records", + "{}".format(num_kv_set_records), + "--num_values_in_set", + "{}".format(num_values_in_set), + ], + tags = ["manual"], + tool = "//tools/serving_data_generator:test_serving_data_generator", + ) + for num, size in enumerate( + ("10", "20", "50", "100", "500", "1000", "5000"), + 1, + ) +] diff --git a/tools/latency_benchmarking/example/request_metadata.jsonl b/tools/latency_benchmarking/example/request_metadata.jsonl new file mode 100644 index 00000000..ca9fcb1b --- /dev/null +++ b/tools/latency_benchmarking/example/request_metadata.jsonl @@ -0,0 +1,4 @@ +{"useGetValuesBinary": false} +{"useGetValuesBinary": false, "lookup_batch_size": 50} +{"useGetValuesBinary": true} +{"useGetValuesBinary": true, "lookup_batch_size": 50} diff --git a/tools/latency_benchmarking/example/request_metadata_run_query.jsonl b/tools/latency_benchmarking/example/request_metadata_run_query.jsonl new file mode 100644 index 00000000..b8966d1d --- /dev/null +++ b/tools/latency_benchmarking/example/request_metadata_run_query.jsonl @@ -0,0 +1,2 @@ +{"runQuery": true, "useGetValuesBinary": false} +{"runQuery": true, "useGetValuesBinary": true} diff --git a/tools/latency_benchmarking/example/udf_code/BUILD.bazel b/tools/latency_benchmarking/example/udf_code/BUILD.bazel new file mode 100644 index 00000000..1745c297 --- /dev/null +++ b/tools/latency_benchmarking/example/udf_code/BUILD.bazel @@ -0,0 +1,44 @@ +load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library") +load("//tools/udf/closure_js:closure_to_delta.bzl", "closure_to_delta") +load("//tools/udf/inline_wasm:wasm.bzl", "cc_inline_wasm_udf_delta") + +closure_js_library( + name = "benchmark_udf_js_lib", + srcs = [ + "benchmark_udf.js", # User-defined functions with handler/entrypoint + "externs.js", # Register APIs called from UDF as functions without definitions for closure. + ], + convention = "NONE", + suppress = [ + "missingSourcesWarnings", + ], + deps = [ + "//public/udf:binary_get_values_js_proto", + ], +) + +# Generates a UDF delta file using the given closure_js_lib target +# builders/tools/bazel-debian run \ +# //tools/latency_benchmarking/example/udf_code:benchmark_udf_js_delta +closure_to_delta( + name = "benchmark_udf_js_delta", + closure_js_library_target = ":benchmark_udf_js_lib", + logical_commit_time = "200000001", + output_file_name = "DELTA_2000000000000001", +) + +# builders/tools/bazel-debian run --config=emscripten \ +# //tools/latency_benchmarking/example/udf_code:benchmark_cpp_wasm_udf_delta +cc_inline_wasm_udf_delta( + name = "benchmark_cpp_wasm_udf_delta", + srcs = ["benchmark_cpp_wasm_udf.cc"], + custom_udf_js = "benchmark_cpp_wasm_udf.js", + logical_commit_time = "200000002", + output_file_name = "DELTA_2000000000000002", + tags = ["manual"], + deps = [ + "//public/udf:binary_get_values_cc_proto", + "@com_google_absl//absl/status:statusor", + "@nlohmann_json//:lib", + ], +) diff --git a/tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.cc b/tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.cc new file mode 100644 index 00000000..b2e278bc --- /dev/null +++ b/tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.cc @@ -0,0 +1,290 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include "absl/status/statusor.h" +#include "emscripten/bind.h" +#include "nlohmann/json.hpp" +#include "public/udf/binary_get_values.pb.h" + +namespace { + +constexpr int kTopK = 4; +constexpr int kLookupNKeysFromRunQuery = 100; + +// Calls getValues for a list of keys and processes the response. +absl::StatusOr> GetKvPairs( + const emscripten::val& get_values_cb, + const std::vector& lookup_data) { + std::vector kv_pairs_json; + for (const auto& keys : lookup_data) { + // `get_values_cb` takes an emscripten::val of type array and returns + // an emscripten::val of type string. + // We need to cast the emscripten::val string to a C++ string. + // However, getValues actually returns a serialized JSON object, + // so we need to parse the string to use it. + const std::string get_values_result = get_values_cb(keys).as(); + const nlohmann::json get_values_json = + nlohmann::json::parse(get_values_result, nullptr, + /*allow_exceptions=*/false, + /*ignore_comments=*/true); + if (get_values_json.is_discarded() || + !get_values_json.contains("kvPairs")) { + return absl::InvalidArgumentError("No kvPairs returned"); + } + kv_pairs_json.push_back(get_values_json); + } + return kv_pairs_json; +} + +absl::StatusOr GetKvPairsAsEmVal( + const emscripten::val& get_values_cb, + const std::vector& lookup_data) { + emscripten::val kv_pairs = emscripten::val::object(); + kv_pairs.set("udfApi", "getValues"); + + const auto kv_pairs_json_or_status = GetKvPairs(get_values_cb, lookup_data); + if (!kv_pairs_json_or_status.ok()) { + return kv_pairs_json_or_status.status(); + } + for (const auto& kv_pair_json : *kv_pairs_json_or_status) { + for (auto& [k, v] : kv_pair_json["kvPairs"].items()) { + if (v.contains("value")) { + emscripten::val value = emscripten::val::object(); + value.set("value", emscripten::val(v["value"].get())); + kv_pairs.set(k, value); + } + } + } + return kv_pairs; +} + +absl::StatusOr> GetKvPairsAsMap( + const emscripten::val& get_values_cb, + const std::vector& lookup_data) { + std::map kv_map; + kv_map["udfApi"] = "getValues"; + + const auto kv_pairs_json_or_status = GetKvPairs(get_values_cb, lookup_data); + if (!kv_pairs_json_or_status.ok()) { + return kv_pairs_json_or_status.status(); + } + for (const auto& kv_pair_json : *kv_pairs_json_or_status) { + for (auto& [k, v] : kv_pair_json["kvPairs"].items()) { + if (v.contains("value")) { + kv_map[k] = v["value"]; + } + } + } + return kv_map; +} + +// Calls getValuesBinary for a list of keys and processes the response. +absl::StatusOr> +GetKvPairsBinary(const emscripten::val& get_values_binary_cb, + const std::vector& lookup_data) { + std::vector responses; + for (const auto& keys : lookup_data) { + // `get_values_cb` takes an emscripten::val of type array and returns + // an emscripten::val of type Uint8Array that contains a serialized + // BinaryGetValuesResponse. + std::vector get_values_binary_result_array = + emscripten::convertJSArrayToNumberVector( + get_values_binary_cb(keys)); + kv_server::BinaryGetValuesResponse response; + if (!response.ParseFromArray(get_values_binary_result_array.data(), + get_values_binary_result_array.size())) { + return absl::InternalError( + absl::StrCat("Could not parse binary response to proto")); + } + responses.push_back(response); + } + return responses; +} + +absl::StatusOr GetKvPairsBinaryAsEmVal( + const emscripten::val& get_values_binary_cb, + const std::vector& lookup_data) { + emscripten::val kv_pairs = emscripten::val::object(); + kv_pairs.set("udfApi", "getValuesBinary"); + const auto responses_or_status = + GetKvPairsBinary(get_values_binary_cb, lookup_data); + if (!responses_or_status.ok()) { + return responses_or_status.status(); + } + for (const auto& response : *responses_or_status) { + for (auto& [k, v] : response.kv_pairs()) { + kv_pairs.set(k, v.data()); + } + } + return kv_pairs; +} + +absl::StatusOr> GetKvPairsBinaryAsMap( + const emscripten::val& get_values_binary_cb, + const std::vector& lookup_data) { + std::map kv_map; + kv_map["udfApi"] = "getValuesBinary"; + const auto responses_or_status = + GetKvPairsBinary(get_values_binary_cb, lookup_data); + if (!responses_or_status.ok()) { + return responses_or_status.status(); + } + for (const auto& response : *responses_or_status) { + for (auto& [k, v] : response.kv_pairs()) { + kv_map[k] = v.data(); + } + } + return kv_map; +} + +std::vector MaybeSplitDataByBatchSize( + const emscripten::val& request_metadata, const emscripten::val& data) { + if (!request_metadata.hasOwnProperty("lookup_batch_size")) { + return std::vector({data}); + } + const int batch_size = request_metadata["lookup_batch_size"].as(); + const int data_length = data["length"].as(); + if (batch_size >= data_length) { + return std::vector({data}); + } + std::vector batches; + for (int i = 0; i < data_length; i += batch_size) { + batches.emplace_back( + data.call("slice", i, i + batch_size)); + } + return batches; +} + +// I/O processing, similar to +// tools/latency_benchmarking/example/udf_code/benchmark_udf.js +emscripten::val GetKeyGroupOutputs(const emscripten::val& get_values_cb, + const emscripten::val& get_values_binary_cb, + const emscripten::val& request_metadata, + const emscripten::val& udf_arguments) { + emscripten::val key_group_outputs = emscripten::val::array(); + // Convert a JS array to a std::vector so we can iterate through it. + const std::vector key_groups = + emscripten::vecFromJSArray(udf_arguments); + for (const auto& key_group : key_groups) { + emscripten::val key_group_output = emscripten::val::object(); + const emscripten::val data = + key_group.hasOwnProperty("tags") ? key_group["data"] : key_group; + + std::vector lookup_data = + MaybeSplitDataByBatchSize(request_metadata, data); + absl::StatusOr kv_pairs; + kv_pairs = request_metadata.hasOwnProperty("useGetValuesBinary") && + request_metadata["useGetValuesBinary"].as() + ? GetKvPairsBinaryAsEmVal(get_values_binary_cb, lookup_data) + : GetKvPairsAsEmVal(get_values_cb, lookup_data); + if (kv_pairs.ok()) { + key_group_output.set("keyValues", *kv_pairs); + key_group_outputs.call("push", key_group_output); + } + } + return key_group_outputs; +} + +emscripten::val HandleGetValuesFlow(const emscripten::val& get_values_cb, + const emscripten::val& get_values_binary_cb, + const emscripten::val& request_metadata, + const emscripten::val& udf_arguments) { + emscripten::val result = emscripten::val::object(); + emscripten::val key_group_outputs = GetKeyGroupOutputs( + get_values_cb, get_values_binary_cb, request_metadata, udf_arguments); + result.set("keyGroupOutputs", key_group_outputs); + result.set("udfOutputApiVersion", emscripten::val(1)); + return result; +} + +// The run query flow performs the following steps: +// 1. Compute the set union of given arguments using `runQuery` API +// 2. Call `getValues`/`getValuesBinary` with first +// `lookup_n_keys_from_runquery` keys +// 3. Sort returned KVs +// 4. Return top 5 KV pairs +emscripten::val HandleRunQueryFlow(const emscripten::val& get_values_cb, + const emscripten::val& get_values_binary_cb, + const emscripten::val& run_query_cb, + const emscripten::val& request_metadata, + const emscripten::val& udf_arguments) { + emscripten::val result = emscripten::val::object(); + result.set("udfOutputApiVersion", emscripten::val(1)); + if (udf_arguments["length"].as() <= 0) { + return result; + } + // Union of all sets in udf_arguments + emscripten::val set_keys = udf_arguments[0].hasOwnProperty("data") + ? udf_arguments[0]["data"] + : udf_arguments[0]; + emscripten::val query = + set_keys.call("join", emscripten::val("|")); + emscripten::val keys = run_query_cb(query); + int n = kLookupNKeysFromRunQuery; + if (request_metadata.hasOwnProperty("lookup_n_keys_from_runquery")) { + n = request_metadata["lookup_n_keys_from_runquery"].as(); + } + emscripten::val lookup_keys = keys.call("slice", 0, n); + std::vector lookup_data = + MaybeSplitDataByBatchSize(request_metadata, lookup_keys); + absl::StatusOr> kv_map = + request_metadata.hasOwnProperty("useGetValuesBinary") && + request_metadata["useGetValuesBinary"].as() + ? GetKvPairsBinaryAsMap(get_values_binary_cb, lookup_data) + : GetKvPairsAsMap(get_values_cb, lookup_data); + if (!kv_map.ok()) { + return result; + } + int i = 0; + emscripten::val key_group_outputs = emscripten::val::array(); + emscripten::val key_group_output = emscripten::val::object(); + emscripten::val kv_pairs = emscripten::val::object(); + // select only kTopK + for (const auto& [k, v] : *kv_map) { + emscripten::val value = emscripten::val::object(); + value.set("value", v); + kv_pairs.set(k, value); + if (++i > kTopK) { + break; + } + } + key_group_output.set("keyValues", kv_pairs); + key_group_outputs.call("push", key_group_output); + result.set("keyGroupOutputs", key_group_outputs); + return result; +} + +} // namespace + +emscripten::val HandleRequestCc(const emscripten::val& get_values_cb, + const emscripten::val& get_values_binary_cb, + const emscripten::val& run_query_cb, + const emscripten::val& request_metadata, + const emscripten::val& udf_arguments) { + if (request_metadata.hasOwnProperty("runQuery") && + request_metadata["runQuery"].as()) { + return HandleRunQueryFlow(get_values_cb, get_values_binary_cb, run_query_cb, + request_metadata, udf_arguments); + } + return HandleGetValuesFlow(get_values_cb, get_values_binary_cb, + request_metadata, udf_arguments); +} + +EMSCRIPTEN_BINDINGS(HandleRequestExample) { + emscripten::function("handleRequestCc", &HandleRequestCc); +} diff --git a/tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.js b/tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.js new file mode 100644 index 00000000..4efae9ff --- /dev/null +++ b/tools/latency_benchmarking/example/udf_code/benchmark_cpp_wasm_udf.js @@ -0,0 +1,27 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +async function HandleRequest(executionMetadata, ...udf_arguments) { + const module = await getModule(); + const result = module.handleRequestCc( + getValues, + getValuesBinary, + runQuery, + executionMetadata.requestMetadata, + udf_arguments + ); + return result; +} diff --git a/tools/latency_benchmarking/example/udf_code/benchmark_udf.js b/tools/latency_benchmarking/example/udf_code/benchmark_udf.js new file mode 100644 index 00000000..688f32a3 --- /dev/null +++ b/tools/latency_benchmarking/example/udf_code/benchmark_udf.js @@ -0,0 +1,236 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Import the generated JS code based on public/udf/binary_get_values.proto + */ +goog.require('proto.kv_server.BinaryGetValuesResponse'); + +/** + * @param {!Object} value + * @param {!Object} keyGroupOutputs + * @return {Object} + * @suppress {reportUnknownTypes} + */ +function addValueToOutput(value, keyGroupOutputs) { + let keyGroupOutput = value; + keyGroupOutputs.push(keyGroupOutput); + return null; +} + +/** + * @param {!Object} getValuesBinaryProto + * @param {!Object} keyValuesOutput + * @return {Object} + * @suppress {reportUnknownTypes} + */ +function processGetValuesBinary(getValuesBinaryProto, keyValuesOutput) { + var kvMap = getValuesBinaryProto.getKvPairsMap(); + const keys = kvMap.keys(); + for (const key of keys) { + const val = kvMap.get(key); + const hasData = val.hasData(); + if (hasData) { + const valUint8Arr = val.getData_asU8(); + const valString = String.fromCharCode.apply(null, valUint8Arr); + keyValuesOutput[key] = { value: valString }; + } + } + return keyValuesOutput; +} + +/** + * @param {!Array>} lookupData + * @return {Object} + */ +function callGetValuesBinary(lookupData) { + let keyValuesOutput = {}; + for (const data of lookupData) { + const getValuesUnparsed = getValuesBinary(data); + const getValuesBinaryProto = proto.kv_server.BinaryGetValuesResponse.deserializeBinary(getValuesUnparsed); + processGetValuesBinary(getValuesBinaryProto, keyValuesOutput); + } + keyValuesOutput['getValuesApi'] = 'getValuesBinary'; + return keyValuesOutput; +} + +/** + * @param {!Array>} lookupData + * @return {Object} + */ +function callGetValues(lookupData) { + const keyValuesOutput = {}; + for (const data of lookupData) { + const getValuesResult = JSON.parse(getValues(data)); + // getValuesResult returns "kvPairs" when successful and "code" on failure. + // Ignore failures and only add successful getValuesResult lookups to + // output. + if (getValuesResult.hasOwnProperty('kvPairs')) { + const kvPairs = getValuesResult['kvPairs']; + for (const key in kvPairs) { + if (kvPairs[key].hasOwnProperty('value')) { + keyValuesOutput[key] = { value: kvPairs[key]['value'] }; + } + } + } + } + keyValuesOutput['getValuesApi'] = 'getValues'; + return keyValuesOutput; +} + +/** + * @param {!Array} data + * @param {number} batchSize + * @return {!Array>} + */ + +function splitDataByBatchSize(data, batchSize) { + const batches = []; + for (let i = 0; i < data.length; i += batchSize) { + batches.push(data.slice(i, i + batchSize)); + } + return batches; +} + +/** + * @param {Object} requestMetadata + * @param {!Array} udf_arguments + * @return {Object} + */ +function handleGetValuesRequest(requestMetadata, udf_arguments) { + let keyGroupOutputs = []; + for (let argument of udf_arguments) { + let keyGroupOutput = {}; + let data = argument.hasOwnProperty('tags') ? argument['data'] : argument; + let lookupData = [data]; + if (requestMetadata.hasOwnProperty('lookup_batch_size')) { + batchSize = parseInt(requestMetadata['lookup_batch_size'], 10); + lookupData = splitDataByBatchSize(data, batchSize); + } + keyGroupOutput['keyValues'] = requestMetadataKeyIsTrue(requestMetadata, 'useGetValuesBinary') + ? callGetValuesBinary(lookupData) + : callGetValues(lookupData); + keyGroupOutputs.push(keyGroupOutput); + } + return keyGroupOutputs; +} + +function requestMetadataKeyIsTrue(requestMetadata, key) { + return requestMetadata.hasOwnProperty(key) && requestMetadata[key] == true; +} + +/** + * Handles the runQuery flow: + * 1. compute set union on arguments using `runQuery` + * 2. getValues/getValuesBinary on first `lookup_n_keys_from_runquery` keys + * 3. sort returned KVs by key length + * 4. return top 5 KVs + * + * @param {!Object} requestMetadata + * @param {!Array} udf_arguments + * @returns {Object} + */ +function handleRunQueryFlow(requestMetadata, udf_arguments) { + var result = {}; + result['udfOutputApiVersion'] = 1; + if (!udf_arguments.length) { + return result; + } + + // Union all the sets in the udf_arguments + const setKeys = udf_arguments[0].hasOwnProperty('data') ? udf_arguments[0]['data'] : udf_arguments[0]; + const keys = runQuery(setKeys.join('|')); + const n = parseInt(requestMetadata['lookup_n_keys_from_runquery'] || '100', 10); + + const keyValuesOutput = handleGetValuesRequest(requestMetadata, [keys.slice(0, n)]); + if (!keyValuesOutput.length || !keyValuesOutput[0].hasOwnProperty('keyValues')) { + return result; + } + let top5keyValuesArray = Object.entries(keyValuesOutput[0]['keyValues']) + .sort((a, b) => a[0].length - b[0].length) + .slice(0, 5); + result['keyGroupOutputs'] = Object.fromEntries(top5keyValuesArray); + return result; +} + +/** + * Handles the getValues flow (without runQuery&sorting). + * + * @param {!Object} requestMetadata + * @param {!Array} udf_arguments + * @returns {Object} + */ +function handleGetValuesFlow(requestMetadata, udf_arguments) { + const keyGroupOutputs = handleGetValuesRequest(requestMetadata, udf_arguments); + var result = {}; + result['keyGroupOutputs'] = keyGroupOutputs; + result['udfOutputApiVersion'] = 1; + return result; +} + +/** + * Entry point for code snippet execution. + * + * This is a sample UDF js that calls different UDF APIs depending + * on executionMetadata.requestMetadata fields. + * + * If `executionMetadata.requestMetadata.useGetValuesBinary` is set to true, + * the UDF will use the `getValuesBinary` API to retrieve key-values. + * Otherwise, the UDF will use the `getValues` API to retrieve key-values. + * + * If `executionMetadata.requestMetadata.runQuery` is set to true, + * the UDF will do the following steps: + * 1. Compute the set union of all elements in the first `udf_argument` + * 2. Call `getValues`/`getValuesBinary` on the returned keys from step 1. + * 3. Return the top 5 key values (sorted by key length) from step 2. + * Otherwise, the UDF will just act as a pass-through UDF that calls + * `getValues`/`getValuesBinary` on the `udf_arguments` and return the + * retrieved key-value pairs. + * + * If `requestMetadata` has a `lookup_batch_size`, it will batch + * getValues/getValuesBinary calls by the provided `lookup_batch_size`. + * For example, if the input has 800 keys and `lookup_batch_size` is 300, + * the UDF will make 3 getValues/getValuesBinary calls: + * 2 call with 300 keys + * 1 call with 200 keys + * + * The Closure Compiler will see the @export JSDoc annotation below and + * generate the following synthetic code in the global scope: + * + * goog.exportSymbol('HandleRequest', HandleRequest); + * + * That makes the minified version of this function still available in the + * global namespace. + * + * You can also use @export for property names on classes, to prevent them + * from being minified. + * + * @export + * @param {Object} executionMetadata + * @param {...!Object} udf_arguments + * @return {Object} + * @suppress {checkTypes} + */ +function HandleRequest(executionMetadata, ...udf_arguments) { + const requestMetadata = executionMetadata.hasOwnProperty('requestMetadata') + ? executionMetadata['requestMetadata'] + : {}; + + if (requestMetadataKeyIsTrue(requestMetadata, 'runQuery')) { + return handleRunQueryFlow(requestMetadata, udf_arguments); + } + return handleGetValuesFlow(requestMetadata, udf_arguments); +} diff --git a/tools/latency_benchmarking/example/udf_code/externs.js b/tools/latency_benchmarking/example/udf_code/externs.js new file mode 100644 index 00000000..3b982ca0 --- /dev/null +++ b/tools/latency_benchmarking/example/udf_code/externs.js @@ -0,0 +1,34 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * These functions are provided by the K/V server as a part of the + * UDF API and need to be marked as @externs for the Closure Compiler. + * + * @externs + */ +function getValues(keyList) {} // Note the empty body. +function runQuery(query) {} // Note the empty body. + +/** + * Returns a serialized BinaryGetValuesResponse for the keyList. + * + * This function is provided by the K/V server as a part of the + * UDF API and needs to be marked as @externs for the Closure Compiler. + * + * @externs + */ +function getValuesBinary(keyList) {} // Note the empty body. diff --git a/tools/latency_benchmarking/generate_requests.py b/tools/latency_benchmarking/generate_requests.py new file mode 100644 index 00000000..e55b5733 --- /dev/null +++ b/tools/latency_benchmarking/generate_requests.py @@ -0,0 +1,160 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import csv +import argparse +import json +import base64 + +from pathlib import Path +from typing import Any, Iterator + +""" +Generates JSON requests to be sent to the KV server V2 API. + +For each N in a number-of-keys-list, it will + * select N keys from the given snapshot.csv file + * generate a request body with the list of keys + * save request under //n=/request.json +""" + + +def _BuildRequest(data: list[str], metadata: dict[str, str]) -> dict[str, Any]: + """Build the HTTP body that contains the base64 encoded request body as data.""" + arguments = [] + argument = {"tags": ["custom", "keys"], "data": data} + arguments.append(argument) + + body = { + "metadata": metadata, + "partitions": [{"id": 0, "compressionGroupId": 0, "arguments": arguments}], + } + body_base64_string = base64.b64encode(json.dumps(body).encode()).decode() + http_body = {"raw_body": {"data": body_base64_string}} + return json.dumps(http_body) + + +def WriteRequests( + keys: list[str], + number_of_keys_list: list[int], + output_dir: str, + metadata: dict[str, str], +) -> None: + """Writes the requests to JSON files. + + Args: + keys: List of all keys. + number_of_keys_list: List of number of keys to use in request. One request will generated per item. + output_dir: Base output dir to write to. + metadata: Metadata to include in the request body, as per V2 API. + """ + for n in number_of_keys_list: + if n > len(keys): + print( + f"Warning: List of provided lookup keys ({len(keys)}) is smaller than number of keys to be included in request ({n}). Skipping...\n" + ) + continue + + request = _BuildRequest(keys[:n], metadata) + # Write to an output file at /n=/request.json + output_dir_n = os.path.join(output_dir, f"{n=}") + Path(output_dir_n).mkdir(parents=True, exist_ok=True) + with open(os.path.join(output_dir_n, "request.json"), "w") as f: + f.write(request) + + +def _ReadCsv(snapshot_csv_file: str) -> Iterator[str]: + with open(snapshot_csv_file, "r") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + yield row + + +def ReadKeys( + snapshot_csv_file: str, max_number_of_keys: int, filter_by_sets: bool +) -> list[str]: + """Read keys from CSV file. Only include update and string type mutations. + + Args: + snapshot_csv_file: Path to snapshot CSV file. + max_number_of_keys: Maximum number of keys to read. + filter_by_sets: Whether to only use rows with value_type "string_set" + + Returns: + List of unique set of keys. + """ + keys = set() + for row in _ReadCsv(snapshot_csv_file): + if filter_by_sets and row["value_type"].lower() != "string_set": + continue + if not filter_by_sets and row["value_type"].lower() != "string": + continue + if row["mutation_type"].lower() == "update": + keys.add(row["key"]) + if len(keys) >= max_number_of_keys: + break + return list(keys) + + +def Main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--number-of-keys-list", + dest="number_of_keys_list", + type=int, + nargs="+", + help="List of number of keys to include in ghz request", + ) + parser.add_argument( + "--output-dir", + dest="output_dir", + type=str, + default="/tmp/latency_benchmarking", + help="Output directory for benchmarks", + ) + parser.add_argument( + "--snapshot-csv-dir", + dest="snapshot_csv_dir", + default="snapshot.csv", + help="Directory with snapshot CSVs with KVMutation update entries.", + ) + parser.add_argument( + "--metadata", + dest="metadata", + type=str, + default="{}", + help="Request metadata as a JSON object.", + ) + parser.add_argument( + "--filter-by-sets", + dest="filter_by_sets", + action="store_true", + help="Whether to only use keys of sets from the input to build the requests", + ) + args = parser.parse_args() + metadata = json.loads(args.metadata) + if not isinstance(metadata, dict): + raise ValueError("metadata is not a JSON object") + for filename in os.listdir(args.snapshot_csv_dir): + snapshot_csv_file = os.path.join(args.snapshot_csv_dir, filename) + keys = ReadKeys( + snapshot_csv_file, max(args.number_of_keys_list), args.filter_by_sets + ) + output_dir_for_snapshot = os.path.join(args.output_dir, filename) + WriteRequests(keys, args.number_of_keys_list, output_dir_for_snapshot, metadata) + + +if __name__ == "__main__": + Main() diff --git a/tools/latency_benchmarking/merge_csvs.py b/tools/latency_benchmarking/merge_csvs.py new file mode 100644 index 00000000..fcccbf19 --- /dev/null +++ b/tools/latency_benchmarking/merge_csvs.py @@ -0,0 +1,41 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import pandas as pd + +""" +Merges list of CSV files into one +""" + + +def Main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--csv-inputs", + dest="csv_inputs", + type=str, + nargs="+", + help="List of csv input files.", + ) + parser.add_argument( + "--csv-output", dest="csv_output", type=str, help="Output CSV file to write to." + ) + args = parser.parse_args() + df = pd.concat([pd.read_csv(file) for file in args.csv_inputs], ignore_index=True) + df.to_csv(args.csv_output, index=False) + + +if __name__ == "__main__": + Main() diff --git a/tools/latency_benchmarking/run_benchmarks b/tools/latency_benchmarking/run_benchmarks new file mode 100755 index 00000000..c67d11fb --- /dev/null +++ b/tools/latency_benchmarking/run_benchmarks @@ -0,0 +1,197 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o pipefail +set -o errexit + +START=$(date +%s) +readonly START + +WORKSPACE="$(git rev-parse --show-toplevel)" + +BASE_OUTPUT_DIR="${WORKSPACE}/dist/tools/latency_benchmarking/output/${START}" + +NUMBER_OF_LOOKUP_KEYS="1 5 10" +REQUEST_METADATA="{}" +FILTER_BY_SETS=0 +FILTER_BY_SETS_JSON='{"filter_snapshot_by_sets": "false"}' +BENCHMARK_DURATION="5s" + +DOCKER_OUTPUT_DIR="/tmp/latency_benchmarking/output" +readonly DOCKER_OUTPUT_DIR + +DOCKER_SNAPSHOT_DIR="/tmp/latency_benchmarking/snapshots" +DOCKER_SNAPSHOT_CSV_DIR="/tmp/latency_benchmarking/snapshot_csvs" + +function usage() { + local -r -i exitval=${1-1} + cat &>/dev/stderr < + [--server-address] (Required) gRPC host and port. + [--snapshot-dir] or (Required) Full path to either snapshot-dir or snapshot-csv-dir + [--snapshot-csv-dir] is required. + [--number-of-lookup-keys-list] (Optional) List of number of keys to include in a request. + [--benchmark-duration] (Optional) Duration of each benchmark. Default "5s". + [--ghz-tags] (Optional) Tags to include in the ghz run. + [--request-metadata-json] (Optional) Request metadata to set for all requests + [--filter-snapshot-by-sets] (Optional) Whether to filter snapshots by sets when creating requests. +USAGE + # shellcheck disable=SC2086 + exit ${exitval} +} + +function generate_requests() { + # Create output dirs before calling bazel-debian + # If the dir is created in the docker container, we lose write permission + for SNAPSHOT_CSV in "${SNAPSHOT_CSV_DIR}"/*; do + SNAPSHOT_CSV_FILENAME=$(basename "${SNAPSHOT_CSV}") + for N in "${NUMBER_OF_LOOKUP_KEYS_LIST[@]}"; do + mkdir -p "${BASE_OUTPUT_DIR}/${SNAPSHOT_CSV_FILENAME}/n=${N}" + done + done + + local -a GENERATE_REQUESTS_ARGS + GENERATE_REQUESTS_ARGS+=(--output-dir "${DOCKER_OUTPUT_DIR}") + GENERATE_REQUESTS_ARGS+=(--number-of-keys-list "${NUMBER_OF_LOOKUP_KEYS_LIST[@]}") + GENERATE_REQUESTS_ARGS+=(--metadata "${REQUEST_METADATA}") + GENERATE_REQUESTS_ARGS+=(--snapshot-csv-dir "${DOCKER_SNAPSHOT_CSV_DIR}") + if [[ "${FILTER_BY_SETS}" = 1 ]]; then + GENERATE_REQUESTS_ARGS+=(--filter-by-sets) + FILTER_BY_SETS_JSON='{"filter_snapshot_by_sets": "true"}' + fi + + # Mount the output dir to docker and write requests to output dir for each item in + # `NUMBER_OF_LOOKUP_KEYS_LIST`. + # This will write a json request for each NUMBER_OF_LOOKUP_KEY=N to + # dist/tools/latency_benchmarking/output/${START}/${SNAPSHOT_FILENAME}/n=${N}/request.json + EXTRA_DOCKER_RUN_ARGS+=" --volume ${BASE_OUTPUT_DIR}:${DOCKER_OUTPUT_DIR} --volume ${SNAPSHOT_CSV_DIR}:${DOCKER_SNAPSHOT_CSV_DIR} " \ + builders/tools/bazel-debian run //tools/latency_benchmarking:generate_requests \ + -- "${GENERATE_REQUESTS_ARGS[@]}" +} + +function run_ghz_for_requests() { + # Iterate through the generated request.json files and call `ghz` to benchmark server at ${SERVER_ADDRESS} + for N in "${NUMBER_OF_LOOKUP_KEYS_LIST[@]}"; do + DIR="${OUTPUT_DIR}/n=${N}" + REQUEST_JSON="${DIR}"/request.json + if [[ ! -f "${REQUEST_JSON}" ]]; then + continue + fi + + printf "Running ghz for number of keys %s\n" "${N}" + BASE_GHZ_TAGS='{"number_of_lookup_keys": "'"${N}"'", "keys_from_file": "'"${SNAPSHOT_CSV_FILENAME}"'"}' + REQUEST_METADATA_TAGS=$(echo "${REQUEST_METADATA}" | jq 'with_entries(.key |= "request_metadata."+.) | with_entries( .value |= @json)') + TAGS=$(echo "${GHZ_TAGS} ${BASE_GHZ_TAGS} ${REQUEST_METADATA_TAGS} ${FILTER_BY_SETS_JSON}" | jq -s -c 'add') + GHZ_OUTPUT_JSON_FILE="${DIR}/ghz_output.json" + ghz --protoset "${WORKSPACE}/dist/query_api_descriptor_set.pb" \ + -D "${REQUEST_JSON}" \ + --duration="${BENCHMARK_DURATION}" \ + --skipFirst=100 \ + --concurrency=100 \ + --format=pretty \ + --tags "${TAGS}" \ + --output "${GHZ_OUTPUT_JSON_FILE}" \ + --call kv_server.v2.KeyValueService/GetValuesHttp \ + "${SERVER_ADDRESS}" + # In case the server hangs from previous requests, wait before sending next batch + sleep 30 + done +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --server-address) + SERVER_ADDRESS="$2" + shift 2 || usage + ;; + --number-of-lookup-keys-list) + NUMBER_OF_LOOKUP_KEYS="$2" + shift 2 + ;; + --benchmark-duration) + BENCHMARK_DURATION="$2" + shift 2 + ;; + --ghz-tags) + GHZ_TAGS="$2" + shift 2 + ;; + --snapshot-dir) + SNAPSHOT_DIR="$2" + shift 2 + ;; + --snapshot-csv-dir) + SNAPSHOT_CSV_DIR="$2" + shift 2 + ;; + --request-metadata-json) + REQUEST_METADATA="$2" + shift 2 + ;; + --filter-snapshot-by-sets) + FILTER_BY_SETS=1 + shift + ;; + -h | --help) usage 0 ;; + *) usage ;; + esac +done + +# Check for SNAPSHOT_DIR or SNAPSHOT_CSV_DIR. If not available, exit. +if [[ (-z "${SNAPSHOT_DIR}" || ! -d "${SNAPSHOT_DIR}") && (-z "${SNAPSHOT_CSV_DIR}" || ! -d "${SNAPSHOT_CSV_DIR}") ]]; then + printf "snapshot-dir not found:%s\n" "${SNAPSHOT_DIR}" + exit 1 +fi + +IFS=' ' read -ra NUMBER_OF_LOOKUP_KEYS_LIST <<<"${NUMBER_OF_LOOKUP_KEYS}" +if [[ -d "${SNAPSHOT_CSV_DIR}" ]]; then + generate_requests +else + # No snapshot csv given, iterate through snapshot dir to create csvs + SNAPSHOT_CSV_DIR="${BASE_OUTPUT_DIR}/snapshot_csvs/" + # Iterate through snapshot files and convert them to CSV + mkdir -p "${SNAPSHOT_CSV_DIR}" + for SNAPSHOT_FILE in "${SNAPSHOT_DIR}"/*; do + SNAPSHOT_FILENAME=$(basename "${SNAPSHOT_FILE}") + EXTRA_DOCKER_RUN_ARGS+=" --volume ${SNAPSHOT_CSV_DIR}:${DOCKER_SNAPSHOT_CSV_DIR} --volume ${SNAPSHOT_DIR}:${DOCKER_SNAPSHOT_DIR} " \ + builders/tools/bazel-debian run //tools/data_cli:data_cli format_data \ + -- \ + --input_file "${DOCKER_SNAPSHOT_DIR}/${SNAPSHOT_FILENAME}" \ + --input_format DELTA \ + --output_file "${DOCKER_SNAPSHOT_CSV_DIR}/${SNAPSHOT_FILENAME}.csv" \ + --output_format CSV + done + generate_requests +fi + +# Iterate through each snapshot file and +# 1. create requests under dist/tools/latency_benchmarking/output/${START}/${SNAPSHOT_FILENAME} +# 2. for each request, run benchmarks with ghz +for SNAPSHOT_CSV_FILE in "${SNAPSHOT_CSV_DIR}"/*; do + SNAPSHOT_CSV_FILENAME=$(basename "${SNAPSHOT_CSV_FILE}") + OUTPUT_DIR="${BASE_OUTPUT_DIR}/${SNAPSHOT_CSV_FILENAME}" + run_ghz_for_requests +done + +# Go through all the ghz results in dist/tools/latency_benchmarking/output/${START} +# and collect the summary in a csv +EXTRA_DOCKER_RUN_ARGS+=" --volume ${BASE_OUTPUT_DIR}:${DOCKER_OUTPUT_DIR} " \ + builders/tools/bazel-debian run //tools/latency_benchmarking:create_csv_summary \ + -- \ + --ghz-result-dir ${DOCKER_OUTPUT_DIR} + +echo "Result in:" +echo "${BASE_OUTPUT_DIR}/summary.csv" diff --git a/tools/request_simulation/BUILD.bazel b/tools/request_simulation/BUILD.bazel index 1a8f01c7..f09a4173 100644 --- a/tools/request_simulation/BUILD.bazel +++ b/tools/request_simulation/BUILD.bazel @@ -47,7 +47,7 @@ cc_library( "//components/util:sleepfor", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/util:duration", + "@google_privacysandbox_servers_common//src/util:duration", ], ) @@ -80,9 +80,9 @@ cc_library( deps = [ "//components/data/common:thread_manager", "//components/util:sleepfor", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//test/core/util:grpc_test_util_base", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", + "@com_google_absl//absl/log", + "@google_privacysandbox_servers_common//src/telemetry:metrics_recorder", ], ) @@ -95,9 +95,9 @@ cc_library( ":metrics_collector", ":rate_limiter", "//components/data/common:thread_manager", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", ], ) @@ -118,10 +118,10 @@ cc_library( "//public/data_loading/readers:riegeli_stream_io", "//public/data_loading/readers:stream_record_reader_factory", "@com_github_google_flatbuffers//:flatbuffers", - "@com_github_google_glog//:glog", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", - "@google_privacysandbox_servers_common//src/cpp/telemetry:metrics_recorder", - "@google_privacysandbox_servers_common//src/cpp/telemetry:tracing", + "@google_privacysandbox_servers_common//src/telemetry:metrics_recorder", + "@google_privacysandbox_servers_common//src/telemetry:tracing", ], ) @@ -135,8 +135,8 @@ cc_library( hdrs = ["request_simulation_parameter_fetcher.h"], deps = [ "//components/data/common:message_service", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", ], ) @@ -147,8 +147,10 @@ cc_library( deps = [ ":client_worker", ":delta_based_request_generator", + ":detla_based_realtime_updates_publisher", ":grpc_client", ":metrics_collector", + ":realtime_message_batcher", ":request_generation_util", ":request_simulation_parameter_fetcher", ":synthetic_request_generator", @@ -163,9 +165,9 @@ cc_library( "//public/data_loading/readers:riegeli_stream_record_reader_factory", "//public/query:get_values_cc_grpc", "//tools/request_simulation/request:raw_request_cc_proto", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", ], ) @@ -189,11 +191,13 @@ cc_binary( ], deps = [ ":request_simulation_system", - "@com_github_google_glog//:glog", "@com_google_absl//absl/debugging:failure_signal_handler", "@com_google_absl//absl/debugging:symbolize", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/strings", ], ) @@ -267,7 +271,7 @@ cc_test( "//public/testing:fake_key_value_service_impl", "//tools/request_simulation/request:raw_request_cc_proto", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/telemetry:mocks", ], ) @@ -279,7 +283,7 @@ cc_test( ":metrics_collector", "//components/util:sleepfor_mock", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/telemetry:mocks", ], ) @@ -302,7 +306,7 @@ cc_test( "//tools/request_simulation:mocks", "//tools/request_simulation/request:raw_request_cc_proto", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/telemetry:mocks", ], ) @@ -315,7 +319,7 @@ cc_test( "//components/data/common:mocks", "//public/test_util:mocks", "@com_google_googletest//:gtest_main", - "@google_privacysandbox_servers_common//src/cpp/telemetry:mocks", + "@google_privacysandbox_servers_common//src/telemetry:mocks", ], ) @@ -327,7 +331,8 @@ cc_library( "//components/tools:concurrent_publishing_engine", "//public/data_loading/writers:delta_record_limiting_file_writer", "//public/sharding:key_sharder", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@com_google_absl//absl/log:check", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) @@ -357,8 +362,8 @@ cc_library( "//components/data/realtime:realtime_notifier", "//public/data_loading:records_utils", "//public/data_loading/readers:riegeli_stream_record_reader_factory", - "@com_github_google_glog//:glog", "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/log", "@com_google_absl//absl/status:statusor", ], ) diff --git a/tools/request_simulation/client_worker.h b/tools/request_simulation/client_worker.h index cb679b48..d4c82ae4 100644 --- a/tools/request_simulation/client_worker.h +++ b/tools/request_simulation/client_worker.h @@ -22,8 +22,8 @@ #include #include "absl/functional/any_invocable.h" +#include "absl/log/log.h" #include "components/data/common/thread_manager.h" -#include "glog/logging.h" #include "grpcpp/grpcpp.h" #include "tools/request_simulation/grpc_client.h" #include "tools/request_simulation/message_queue.h" @@ -67,7 +67,7 @@ class ClientWorker { metrics_collector_(metrics_collector), request_converter_(std::move(request_converter)), thread_manager_( - TheadManager::Create(absl::StrCat("Client worker ", id))) { + ThreadManager::Create(absl::StrCat("Client worker ", id))) { grpc_client_ = std::make_unique>( channel, request_timeout, is_client_channel); } @@ -93,7 +93,7 @@ class ClientWorker { std::unique_ptr> grpc_client_; absl::AnyInvocable request_converter_; // Thread manager to start or stop the request sending thread. - std::unique_ptr thread_manager_; + std::unique_ptr thread_manager_; }; template diff --git a/tools/request_simulation/client_worker_test.cc b/tools/request_simulation/client_worker_test.cc index a787a225..a9c45ea1 100644 --- a/tools/request_simulation/client_worker_test.cc +++ b/tools/request_simulation/client_worker_test.cc @@ -25,8 +25,8 @@ #include "grpcpp/grpcpp.h" #include "gtest/gtest.h" #include "public/testing/fake_key_value_service_impl.h" -#include "src/cpp/telemetry/mocks.h" -#include "src/cpp/util/duration.h" +#include "src/telemetry/mocks.h" +#include "src/util/duration.h" #include "tools/request_simulation/mocks.h" #include "tools/request_simulation/request/raw_request.pb.h" #include "tools/request_simulation/request_generation_util.h" diff --git a/tools/request_simulation/delta_based_request_generator.cc b/tools/request_simulation/delta_based_request_generator.cc index eef90c2f..37ceaaad 100644 --- a/tools/request_simulation/delta_based_request_generator.cc +++ b/tools/request_simulation/delta_based_request_generator.cc @@ -20,7 +20,7 @@ #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/filename_utils.h" #include "public/data_loading/records_utils.h" -#include "src/cpp/telemetry/tracing.h" +#include "src/telemetry/tracing.h" using privacy_sandbox::server_common::MetricsRecorder; using privacy_sandbox::server_common::TraceWithStatusOr; @@ -42,7 +42,8 @@ class BlobRecordStream : public RecordStream { absl::Status DeltaBasedRequestGenerator::Start() { LOG(INFO) << "Start monitor and load delta files"; absl::Status status = options_.delta_notifier.Start( - options_.change_notifier, {.bucket = options_.data_bucket}, "", + options_.change_notifier, {.bucket = options_.data_bucket}, + {std::make_pair("", "")}, absl::bind_front(&DeltaBasedRequestGenerator::EnqueueNewFilesToProcess, this)); if (!status.ok()) { diff --git a/tools/request_simulation/delta_based_request_generator.h b/tools/request_simulation/delta_based_request_generator.h index f87ff4da..ada28ca2 100644 --- a/tools/request_simulation/delta_based_request_generator.h +++ b/tools/request_simulation/delta_based_request_generator.h @@ -30,7 +30,7 @@ #include "components/data/realtime/realtime_notifier.h" #include "public/data_loading/readers/riegeli_stream_io.h" #include "public/data_loading/readers/stream_record_reader_factory.h" -#include "src/cpp/telemetry/metrics_recorder.h" +#include "src/telemetry/metrics_recorder.h" #include "tools/request_simulation/message_queue.h" #include "tools/request_simulation/request_generation_util.h" @@ -57,7 +57,7 @@ class DeltaBasedRequestGenerator { privacy_sandbox::server_common::MetricsRecorder& metrics_recorder) : options_(std::move(options)), data_load_thread_manager_( - TheadManager::Create("Delta file loading thread")), + ThreadManager::Create("Delta file loading thread")), request_generation_fn_(std::move(request_generation_fn)), metrics_recorder_(metrics_recorder) {} ~DeltaBasedRequestGenerator() = default; @@ -92,7 +92,7 @@ class DeltaBasedRequestGenerator { absl::Mutex mu_; std::deque unprocessed_basenames_ ABSL_GUARDED_BY(mu_); bool stop_ ABSL_GUARDED_BY(mu_) = false; - std::unique_ptr data_load_thread_manager_; + std::unique_ptr data_load_thread_manager_; // Callback function to generate KV request from a given key absl::AnyInvocable request_generation_fn_; privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; diff --git a/tools/request_simulation/delta_based_request_generator_test.cc b/tools/request_simulation/delta_based_request_generator_test.cc index ee758088..ecc92de7 100644 --- a/tools/request_simulation/delta_based_request_generator_test.cc +++ b/tools/request_simulation/delta_based_request_generator_test.cc @@ -26,7 +26,7 @@ #include "public/data_loading/filename_utils.h" #include "public/data_loading/records_utils.h" #include "public/test_util/mocks.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/telemetry/mocks.h" #include "tools/request_simulation/request_generation_util.h" using kv_server::BlobStorageChangeNotifier; @@ -58,8 +58,10 @@ using testing::_; using testing::AllOf; using testing::ByMove; using testing::Field; +using testing::Pair; using testing::Return; using testing::ReturnRef; +using testing::UnorderedElementsAre; namespace { @@ -103,10 +105,11 @@ TEST_F(GenerateRequestsFromDeltaFilesTest, LoadingDataFromDeltaFiles) { DeltaBasedRequestGenerator request_generator( std::move(options_), std::move(GetRequestGenFn()), metrics_recorder_); const std::string last_basename = ""; - EXPECT_CALL(notifier_, Start(_, GetTestLocation(), last_basename, _)) - .WillOnce([](BlobStorageChangeNotifier& change_notifier, - BlobStorageClient::DataLocation location, - std::string start_after, + EXPECT_CALL(notifier_, + Start(_, GetTestLocation(), + UnorderedElementsAre(Pair("", last_basename)), _)) + .WillOnce([](BlobStorageChangeNotifier&, BlobStorageClient::DataLocation, + absl::flat_hash_map, std::function callback) { callback(ToDeltaFileName(1).value()); LOG(INFO) << "Notified 1 file"; diff --git a/tools/request_simulation/detla_based_realtime_updates_publisher.cc b/tools/request_simulation/detla_based_realtime_updates_publisher.cc index fad28107..0abfae97 100644 --- a/tools/request_simulation/detla_based_realtime_updates_publisher.cc +++ b/tools/request_simulation/detla_based_realtime_updates_publisher.cc @@ -16,6 +16,7 @@ #include "absl/functional/bind_front.h" #include "public/data_loading/filename_utils.h" +#include "src/util/status_macro/status_macros.h" using privacy_sandbox::server_common::TraceWithStatusOr; @@ -33,16 +34,20 @@ class BlobRecordStream : public RecordStream { }; } // namespace +// b/321746753 -- this class can potentially benefit from refactoring absl::Status DeltaBasedRealtimeUpdatesPublisher::Start() { LOG(INFO) << "Start monitor delta files and publish them as realtime updates"; - absl::Status status = options_.delta_notifier.Start( - options_.change_notifier, {.bucket = options_.data_bucket}, "", + PS_RETURN_IF_ERROR(options_.delta_notifier.Start( + options_.change_notifier, {.bucket = options_.data_bucket}, + {std::make_pair("", "")}, absl::bind_front( - &DeltaBasedRealtimeUpdatesPublisher::EnqueueNewFilesToProcess, this)); - if (!status.ok()) { - return status; - } - return data_load_thread_manager_->Start([this]() { ProcessNewFiles(); }); + &DeltaBasedRealtimeUpdatesPublisher::EnqueueNewFilesToProcess, + this))); + PS_RETURN_IF_ERROR( + data_load_thread_manager_->Start([this]() { ProcessNewFiles(); })); + + return rt_publisher_thread_manager_->Start( + [this]() { concurrent_publishing_engine_->Start(); }); } absl::Status DeltaBasedRealtimeUpdatesPublisher::Stop() { @@ -57,6 +62,15 @@ absl::Status DeltaBasedRealtimeUpdatesPublisher::Stop() { LOG(ERROR) << "Failed to stop notify: " << status; } } + + if (rt_publisher_thread_manager_->IsRunning()) { + concurrent_publishing_engine_->Stop(); + if (const auto status = rt_publisher_thread_manager_->Stop(); + !status.ok()) { + LOG(ERROR) << "Failed to stop realtime publisher thread manager: " + << status; + } + } LOG(INFO) << "Delta notifier stopped"; LOG(INFO) << "Stopping publishing realtime updates from"; return data_load_thread_manager_->Stop(); @@ -112,10 +126,6 @@ DeltaBasedRealtimeUpdatesPublisher::CreateRealtimeMessagesAndAddToQueue( return std::make_unique( blob_client.GetBlobReader(location)); }); - auto metadata = record_reader->GetKVFileMetadata(); - if (!metadata.ok()) { - return metadata.status(); - } DataLoadingStats data_loading_stats; const auto process_data_record_fn = [this, &data_loading_stats](const DataRecord& data_record) { @@ -136,13 +146,10 @@ DeltaBasedRealtimeUpdatesPublisher::CreateRealtimeMessagesAndAddToQueue( } } }; - auto status = record_reader->ReadStreamRecords( + PS_RETURN_IF_ERROR(record_reader->ReadStreamRecords( [&process_data_record_fn](std::string_view raw) { return DeserializeDataRecord(raw, process_data_record_fn); - }); - if (!status.ok()) { - return status; - } + })); return data_loading_stats; } bool DeltaBasedRealtimeUpdatesPublisher::HasNewEventToProcess() const { diff --git a/tools/request_simulation/detla_based_realtime_updates_publisher.h b/tools/request_simulation/detla_based_realtime_updates_publisher.h index 1812a82b..8059a27c 100644 --- a/tools/request_simulation/detla_based_realtime_updates_publisher.h +++ b/tools/request_simulation/detla_based_realtime_updates_publisher.h @@ -51,11 +51,15 @@ class DeltaBasedRealtimeUpdatesPublisher { }; DeltaBasedRealtimeUpdatesPublisher( std::unique_ptr realtime_message_batcher, + std::unique_ptr concurrent_publishing_engine, Options options) : realtime_message_batcher_(std::move(realtime_message_batcher)), + concurrent_publishing_engine_(std::move(concurrent_publishing_engine)), options_(std::move(options)), data_load_thread_manager_( - TheadManager::Create("Realtime sharded publisher thread")) {} + ThreadManager::Create("Realtime sharded publisher thread")), + rt_publisher_thread_manager_( + ThreadManager::Create("Publisher thread")) {} ~DeltaBasedRealtimeUpdatesPublisher() = default; // DeltaBasedRealtimeUpdatesPublisher is neither copyable nor movable. @@ -75,7 +79,8 @@ class DeltaBasedRealtimeUpdatesPublisher { private: std::unique_ptr realtime_message_batcher_; - // Checks if there is a new event such as new file or stop event to process + std::unique_ptr concurrent_publishing_engine_; + // Checks if there is new event such as new file or stop event to process bool HasNewEventToProcess() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); absl::Mutex mu_; Options options_; @@ -90,7 +95,8 @@ class DeltaBasedRealtimeUpdatesPublisher { BlobStorageClient::DataLocation location); std::deque unprocessed_basenames_ ABSL_GUARDED_BY(mu_); bool stop_ ABSL_GUARDED_BY(mu_) = false; - std::unique_ptr data_load_thread_manager_; + std::unique_ptr data_load_thread_manager_; + std::unique_ptr rt_publisher_thread_manager_; }; } // namespace kv_server diff --git a/tools/request_simulation/grpc_client.h b/tools/request_simulation/grpc_client.h index 50f49544..a29762ff 100644 --- a/tools/request_simulation/grpc_client.h +++ b/tools/request_simulation/grpc_client.h @@ -135,7 +135,7 @@ class GrpcClient { std::shared_ptr response) { if (is_client_channel_ && grpc_channel_->GetState(true) != GRPC_CHANNEL_READY) { - return absl::FailedPreconditionError("GRPC channel is disconnected"); + return absl::UnavailableError("GRPC channel is disconnected"); } std::shared_ptr notification = std::make_shared(); diff --git a/tools/request_simulation/main.cc b/tools/request_simulation/main.cc index 8e0aa5fd..0690a362 100644 --- a/tools/request_simulation/main.cc +++ b/tools/request_simulation/main.cc @@ -17,10 +17,12 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" -#include "glog/logging.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry_provider.h" +#include "src/telemetry/metrics_recorder.h" +#include "src/telemetry/telemetry_provider.h" #include "tools/request_simulation/grpc_client.h" #include "tools/request_simulation/request_simulation_system.h" @@ -34,7 +36,7 @@ int main(int argc, char** argv) { absl::FailureSignalHandlerOptions options; absl::InstallFailureSignalHandler(options); } - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); absl::SetProgramUsageMessage(absl::StrCat( "Key Value Server Request Simulation System. Sample usage:\n", argv[0])); kv_server::RequestSimulationSystem::InitializeTelemetry(); diff --git a/tools/request_simulation/metrics_collector.cc b/tools/request_simulation/metrics_collector.cc index efe882d5..fdfae0ae 100644 --- a/tools/request_simulation/metrics_collector.cc +++ b/tools/request_simulation/metrics_collector.cc @@ -16,7 +16,7 @@ #include -#include "glog/logging.h" +#include "absl/log/log.h" ABSL_FLAG(absl::Duration, metrics_report_interval, absl::Minutes(1), "The interval for reporting metrics"); @@ -41,7 +41,7 @@ MetricsCollector::MetricsCollector( requests_with_error_response_per_interval_(0), report_interval_(std::move(absl::GetFlag(FLAGS_metrics_report_interval))), report_thread_manager_( - TheadManager::Create("Metrics periodic report thread")), + ThreadManager::Create("Metrics periodic report thread")), metrics_recorder_(metrics_recorder), sleep_for_(std::move(sleep_for)) { histogram_per_interval_ = grpc_histogram_create(kDefaultHistogramResolution, diff --git a/tools/request_simulation/metrics_collector.h b/tools/request_simulation/metrics_collector.h index 77becc19..49721a15 100644 --- a/tools/request_simulation/metrics_collector.h +++ b/tools/request_simulation/metrics_collector.h @@ -22,7 +22,7 @@ #include "absl/flags/flag.h" #include "components/data/common/thread_manager.h" #include "components/util/sleepfor.h" -#include "src/cpp/telemetry/metrics_recorder.h" +#include "src/telemetry/metrics_recorder.h" #include "test/core/util/histogram.h" namespace kv_server { @@ -77,7 +77,7 @@ class MetricsCollector { mutable std::atomic requests_with_ok_response_per_interval_; mutable std::atomic requests_with_error_response_per_interval_; absl::Duration report_interval_; - std::unique_ptr report_thread_manager_; + std::unique_ptr report_thread_manager_; privacy_sandbox::server_common::MetricsRecorder& metrics_recorder_; std::unique_ptr sleep_for_; grpc_histogram* histogram_per_interval_ ABSL_GUARDED_BY(mutex_); diff --git a/tools/request_simulation/metrics_collector_test.cc b/tools/request_simulation/metrics_collector_test.cc index 0b88b22a..014e6708 100644 --- a/tools/request_simulation/metrics_collector_test.cc +++ b/tools/request_simulation/metrics_collector_test.cc @@ -21,7 +21,7 @@ #include "components/util/sleepfor_mock.h" #include "gtest/gtest.h" -#include "src/cpp/telemetry/mocks.h" +#include "src/telemetry/mocks.h" namespace kv_server { diff --git a/tools/request_simulation/rate_limiter.h b/tools/request_simulation/rate_limiter.h index 67420580..9d0fcfb3 100644 --- a/tools/request_simulation/rate_limiter.h +++ b/tools/request_simulation/rate_limiter.h @@ -25,7 +25,7 @@ #include "absl/status/statusor.h" #include "absl/synchronization/mutex.h" #include "components/util/sleepfor.h" -#include "src/cpp/util/duration.h" +#include "src/util/duration.h" namespace kv_server { // A simple permit-based rate limiter. The permits are refilled at given rate diff --git a/tools/request_simulation/realtime_message_batcher.cc b/tools/request_simulation/realtime_message_batcher.cc index 61ea61c0..af20231d 100644 --- a/tools/request_simulation/realtime_message_batcher.cc +++ b/tools/request_simulation/realtime_message_batcher.cc @@ -19,7 +19,9 @@ #include #include -#include "src/cpp/util/status_macro/status_macros.h" +#include "absl/log/check.h" +#include "absl/strings/substitute.h" +#include "src/util/status_macro/status_macros.h" namespace kv_server { namespace { @@ -89,8 +91,9 @@ RealtimeMessage RealtimeMessageBatcher::GetMessage(int shard_num) const { shard_num_opt = shard_num; } RealtimeMessage rm{ - .message = std::string(std::istreambuf_iterator(file_stream), - std::istreambuf_iterator()), + .message = absl::Base64Escape( + std::string(std::istreambuf_iterator(file_stream), + std::istreambuf_iterator())), .shard_num = shard_num_opt, }; return rm; diff --git a/tools/request_simulation/realtime_message_batcher_test.cc b/tools/request_simulation/realtime_message_batcher_test.cc index c553610a..c5a486de 100644 --- a/tools/request_simulation/realtime_message_batcher_test.cc +++ b/tools/request_simulation/realtime_message_batcher_test.cc @@ -18,6 +18,7 @@ #include "absl/strings/match.h" #include "absl/strings/str_cat.h" +#include "absl/strings/substitute.h" #include "gtest/gtest.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" @@ -27,7 +28,9 @@ namespace { absl::StatusOr> Convert( RealtimeMessage rt) { std::vector rows; - std::istringstream is(rt.message); + std::string string_decoded; + absl::Base64Unescape(rt.message, &string_decoded); + std::istringstream is(string_decoded); auto delta_stream_reader_factory = std::make_unique(); auto record_reader = delta_stream_reader_factory->CreateReader(is); diff --git a/tools/request_simulation/request_generation_util.cc b/tools/request_simulation/request_generation_util.cc index 16ec1388..8cd8f91a 100644 --- a/tools/request_simulation/request_generation_util.cc +++ b/tools/request_simulation/request_generation_util.cc @@ -29,8 +29,7 @@ constexpr std::string_view kKVV2KeyValueDSPRequestBodyFormat = R"json( std::vector GenerateRandomKeys(int number_of_keys, int key_size) { std::vector result; for (int i = 0; i < number_of_keys; ++i) { - result.push_back( - std::string(key_size, 'A' + (std::rand() % number_of_keys))); + result.push_back(std::string(key_size, 'A' + (std::rand() % 26))); } return result; } diff --git a/tools/request_simulation/request_simulation_parameter_fetcher.h b/tools/request_simulation/request_simulation_parameter_fetcher.h index bf7b118e..94475144 100644 --- a/tools/request_simulation/request_simulation_parameter_fetcher.h +++ b/tools/request_simulation/request_simulation_parameter_fetcher.h @@ -26,6 +26,7 @@ class RequestSimulationParameterFetcher { RequestSimulationParameterFetcher() = default; virtual ~RequestSimulationParameterFetcher() = default; virtual NotifierMetadata GetBlobStorageNotifierMetadata() const; + virtual NotifierMetadata GetRealtimeNotifierMetadata() const; }; } // namespace kv_server diff --git a/tools/request_simulation/request_simulation_parameter_fetcher_aws.cc b/tools/request_simulation/request_simulation_parameter_fetcher_aws.cc index 86f98e2f..16d71d5a 100644 --- a/tools/request_simulation/request_simulation_parameter_fetcher_aws.cc +++ b/tools/request_simulation/request_simulation_parameter_fetcher_aws.cc @@ -15,13 +15,17 @@ #include #include "absl/flags/flag.h" -#include "glog/logging.h" +#include "absl/log/log.h" #include "tools/request_simulation/request_simulation_parameter_fetcher.h" ABSL_FLAG(std::string, s3_bucket_sns_arn, "", "The Amazon Resource Name(ARN) for the SNS topic" "configured for the S3 bucket"); +ABSL_FLAG(std::string, realtime_sns_arn, "", + "The Amazon Resource Name(ARN) for the SNS topic" + "configured for the realtime updates"); + namespace kv_server { NotifierMetadata @@ -31,4 +35,11 @@ RequestSimulationParameterFetcher::GetBlobStorageNotifierMetadata() const { return AwsNotifierMetadata{"BlobNotifier_", std::move(bucket_sns_arn)}; } +NotifierMetadata +RequestSimulationParameterFetcher::GetRealtimeNotifierMetadata() const { + std::string realtime_sns_arn = absl::GetFlag(FLAGS_realtime_sns_arn); + LOG(INFO) << "The sns arn for s3 bucket is " << realtime_sns_arn; + return AwsNotifierMetadata{"QueueNotifier_", std::move(realtime_sns_arn)}; +} + } // namespace kv_server diff --git a/tools/request_simulation/request_simulation_parameter_fetcher_gcp.cc b/tools/request_simulation/request_simulation_parameter_fetcher_gcp.cc index 61623f2c..6581fcf2 100644 --- a/tools/request_simulation/request_simulation_parameter_fetcher_gcp.cc +++ b/tools/request_simulation/request_simulation_parameter_fetcher_gcp.cc @@ -18,10 +18,12 @@ #include #include "absl/flags/flag.h" -#include "glog/logging.h" +#include "absl/log/log.h" #include "tools/request_simulation/request_simulation_parameter_fetcher.h" ABSL_FLAG(std::string, delta_dir, "", "The local directory for delta files"); +ABSL_FLAG(std::string, realtime_delta_dir, "", + "The local directory for realtime delta files"); namespace kv_server { @@ -32,4 +34,11 @@ RequestSimulationParameterFetcher::GetBlobStorageNotifierMetadata() const { return LocalNotifierMetadata{.local_directory = std::move(directory)}; } +NotifierMetadata +RequestSimulationParameterFetcher::GetRealtimeNotifierMetadata() const { + std::string directory = absl::GetFlag(FLAGS_realtime_delta_dir); + LOG(INFO) << "The local realtim delta file directory is " << directory; + return LocalNotifierMetadata{.local_directory = std::move(directory)}; +} + } // namespace kv_server diff --git a/tools/request_simulation/request_simulation_parameter_fetcher_local.cc b/tools/request_simulation/request_simulation_parameter_fetcher_local.cc index 9fd87521..cbc6dcdb 100644 --- a/tools/request_simulation/request_simulation_parameter_fetcher_local.cc +++ b/tools/request_simulation/request_simulation_parameter_fetcher_local.cc @@ -15,10 +15,12 @@ #include #include "absl/flags/flag.h" -#include "glog/logging.h" +#include "absl/log/log.h" #include "tools/request_simulation/request_simulation_parameter_fetcher.h" ABSL_FLAG(std::string, delta_dir, "", "The local directory for delta files"); +ABSL_FLAG(std::string, realtime_delta_dir, "", + "The local directory for realtime delta files"); namespace kv_server { @@ -29,4 +31,11 @@ RequestSimulationParameterFetcher::GetBlobStorageNotifierMetadata() const { return LocalNotifierMetadata{.local_directory = std::move(directory)}; } +NotifierMetadata +RequestSimulationParameterFetcher::GetRealtimeNotifierMetadata() const { + std::string directory = absl::GetFlag(FLAGS_realtime_delta_dir); + LOG(INFO) << "The local realtim delta file directory is " << directory; + return LocalNotifierMetadata{.local_directory = std::move(directory)}; +} + } // namespace kv_server diff --git a/tools/request_simulation/request_simulation_system.cc b/tools/request_simulation/request_simulation_system.cc index 34a66a96..83a85095 100644 --- a/tools/request_simulation/request_simulation_system.cc +++ b/tools/request_simulation/request_simulation_system.cc @@ -15,16 +15,20 @@ #include "tools/request_simulation/request_simulation_system.h" #include +#include #include #include #include -#include "glog/logging.h" +#include "absl/log/log.h" +#include "components/tools/concurrent_publishing_engine.h" #include "grpcpp/grpcpp.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/resource/semantic_conventions.h" #include "public/data_loading/readers/riegeli_stream_record_reader_factory.h" #include "public/query/get_values.grpc.pb.h" +#include "src/util/status_macro/status_macros.h" +#include "tools/request_simulation/realtime_message_batcher.h" #include "tools/request_simulation/request/raw_request.pb.h" #include "tools/request_simulation/request_generation_util.h" #include "tools/request_simulation/request_simulation_parameter_fetcher.h" @@ -40,7 +44,7 @@ ABSL_FLAG(int, concurrency, 10, "Number of concurrent requests sent to the server," "this number will be limited by the maximum concurrent threads" "supported by state of the machine"); -ABSL_FLAG(absl::Duration, request_timeout, absl::Seconds(5), +ABSL_FLAG(absl::Duration, request_timeout, absl::Seconds(300), "The timeout duration for getting response for the request"); ABSL_FLAG(int64_t, synthetic_requests_fill_qps, 1000, "The per second rate of synthetic requests generated by the " @@ -81,6 +85,13 @@ ABSL_FLAG( ABSL_FLAG(int32_t, data_loading_num_threads, 1, "Number of parallel threads for reading and loading data files."); ABSL_FLAG(std::string, delta_file_bucket, "", "The name of delta file bucket"); +ABSL_FLAG(int32_t, num_shards, 1, "Number of shards on the kv-server"); +ABSL_FLAG(int32_t, realtime_message_size_kb, 10, + "Realtime message size threshold in kb"); +ABSL_FLAG(int32_t, realtime_publisher_insertion_num_threads, 1, + "Number of threads used to write to pubsub in parallel."); +ABSL_FLAG(int32_t, realtime_publisher_files_insertion_rate, 15, + "Number of messages sent per insertion thread to pubsub per second"); namespace kv_server { @@ -180,6 +191,7 @@ absl::Status RequestSimulationSystem::Init( } blob_storage_client_ = CreateBlobClient(); delta_file_notifier_ = CreateDeltaFileNotifier(); + realtime_delta_file_notifier_ = CreateDeltaFileNotifier(); delta_stream_reader_factory_ = CreateStreamRecordReaderFactory(); auto blob_notifier_meta_data = @@ -192,7 +204,7 @@ absl::Status RequestSimulationSystem::Init( SetQueueManager(blob_notifier_meta_data, message_service_blob_.get()); { auto status_or_notifier = - BlobStorageChangeNotifier::Create(std::move(blob_notifier_meta_data)); + BlobStorageChangeNotifier::Create(blob_notifier_meta_data); if (!status_or_notifier.ok()) { // The ChangeNotifier is required to read delta files, if it's not // available that's a critical error and so return immediately. @@ -202,7 +214,9 @@ absl::Status RequestSimulationSystem::Init( } blob_change_notifier_ = std::move(*status_or_notifier); } - + PS_ASSIGN_OR_RETURN( + realtime_blob_change_notifier_, + BlobStorageChangeNotifier::Create(std::move(blob_notifier_meta_data))); delta_based_request_generator_ = std::make_unique( DeltaBasedRequestGenerator::Options{ .data_bucket = absl::GetFlag(FLAGS_delta_file_bucket), @@ -212,6 +226,34 @@ absl::Status RequestSimulationSystem::Init( .change_notifier = *blob_change_notifier_, .delta_stream_reader_factory = *delta_stream_reader_factory_}, CreateRequestFromKeyFn(), metrics_recorder_); + PS_ASSIGN_OR_RETURN(realtime_message_batcher_, + RealtimeMessageBatcher::Create( + realtime_messages_, realtime_messages_mutex_, + absl::GetFlag(FLAGS_num_shards), + absl::GetFlag(FLAGS_realtime_message_size_kb))); + auto notifier_metadata = parameter_fetcher_->GetRealtimeNotifierMetadata(); + const int realtime_publisher_insertion_num_threads = + absl::GetFlag(FLAGS_realtime_publisher_insertion_num_threads); + const int realtime_publisher_files_insertion_rate = + absl::GetFlag(FLAGS_realtime_publisher_files_insertion_rate); + concurrent_publishing_engine_ = std::make_unique( + realtime_publisher_insertion_num_threads, std::move(notifier_metadata), + realtime_publisher_files_insertion_rate, realtime_messages_mutex_, + realtime_messages_); + delta_based_realtime_updates_publisher_ = + std::make_unique( + std::move(realtime_message_batcher_), + std::move(concurrent_publishing_engine_), + DeltaBasedRealtimeUpdatesPublisher::Options{ + .data_bucket = absl::GetFlag(FLAGS_delta_file_bucket), + .blob_client = *blob_storage_client_, + .delta_notifier = *realtime_delta_file_notifier_, + .change_notifier = *realtime_blob_change_notifier_, + .delta_stream_reader_factory = *delta_stream_reader_factory_, + .realtime_messages = realtime_messages_, + .realtime_messages_mutex = realtime_messages_mutex_, + }); + LOG(INFO) << "Request simulation system is initialized," "target server address is " << server_address_ << " and server method is " << server_method_; @@ -233,6 +275,17 @@ absl::Status RequestSimulationSystem::InitializeGrpcClientWorkers() { absl::GetFlag(FLAGS_server_auth_mode)); bool is_client_channel = absl::GetFlag(FLAGS_is_client_channel); + if (is_client_channel) { + RetryUntilOk( + [channel]() { + if (channel->GetState(true) != GRPC_CHANNEL_READY) { + return absl::UnavailableError("GRPC channel is disconnected"); + } + return absl::OkStatus(); + }, + "check grpc connection in start up", LogMetricsNoOpCallback()); + } + auto request_timeout = absl::GetFlag(FLAGS_request_timeout); for (int i = 0; i < num_of_workers; ++i) { auto request_converter = [](const std::string& request_body) { RawRequest request; @@ -241,7 +294,7 @@ absl::Status RequestSimulationSystem::InitializeGrpcClientWorkers() { }; auto worker = std::make_unique>( - i, channel, server_method_, absl::Seconds(1), request_converter, + i, channel, server_method_, request_timeout, request_converter, *message_queue_, *grpc_request_rate_limiter_, *metrics_collector_, is_client_channel); grpc_client_workers_.push_back(std::move(worker)); @@ -254,6 +307,8 @@ absl::Status RequestSimulationSystem::Start() { if (auto status = delta_based_request_generator_->Start(); !status.ok()) { return status; } + LOG(INFO) << "Starting delta based realtime updates publisher"; + PS_RETURN_IF_ERROR(delta_based_realtime_updates_publisher_->Start()); if (synthetic_requests_fill_qps_ > 0) { LOG(INFO) << "Starting synthetic request generator"; if (auto status = synthetic_request_generator_->Start(); !status.ok()) { @@ -276,6 +331,8 @@ absl::Status RequestSimulationSystem::Start() { return absl::OkStatus(); } absl::Status RequestSimulationSystem::Stop() { + LOG(INFO) << "Stopping delta based realtime updates publisher"; + PS_RETURN_IF_ERROR(delta_based_realtime_updates_publisher_->Stop()); LOG(INFO) << "Stopping delta based request generator"; if (auto status = delta_based_request_generator_->Stop(); !status.ok()) { return status; diff --git a/tools/request_simulation/request_simulation_system.h b/tools/request_simulation/request_simulation_system.h index 43d7c1f7..0d7579d6 100644 --- a/tools/request_simulation/request_simulation_system.h +++ b/tools/request_simulation/request_simulation_system.h @@ -18,6 +18,7 @@ #define TOOLS_REQUEST_SIMULATION_REQUEST_SIMULATION_SYSTEM_H_ #include +#include #include #include #include @@ -33,10 +34,11 @@ #include "grpcpp/grpcpp.h" #include "public/data_loading/readers/riegeli_stream_io.h" #include "public/query/get_values.grpc.pb.h" -#include "src/cpp/telemetry/metrics_recorder.h" +#include "src/telemetry/metrics_recorder.h" #include "test/core/util/histogram.h" #include "tools/request_simulation/client_worker.h" #include "tools/request_simulation/delta_based_request_generator.h" +#include "tools/request_simulation/detla_based_realtime_updates_publisher.h" #include "tools/request_simulation/message_queue.h" #include "tools/request_simulation/rate_limiter.h" #include "tools/request_simulation/request/raw_request.pb.h" @@ -73,6 +75,8 @@ namespace kv_server { // 4. Client workers that send requests to the target server. // The number of client workers is determined by the given concurrency // parameter. +// 5. A delta based request generator that reads keys from delta file and +// publishes realtime updates to the specified SNS/pubsub endpoint. // // Once the system successfully starts, the system will continuously generates // requests and send requests to the target server. @@ -147,16 +151,25 @@ class RequestSimulationSystem { std::unique_ptr blob_storage_client_; std::unique_ptr message_service_blob_; std::unique_ptr blob_change_notifier_; + std::unique_ptr realtime_blob_change_notifier_; std::unique_ptr delta_file_notifier_; + std::unique_ptr realtime_delta_file_notifier_; std::unique_ptr delta_stream_reader_factory_; std::unique_ptr message_queue_; std::unique_ptr synthetic_request_generator_rate_limiter_; std::unique_ptr grpc_request_rate_limiter_; std::unique_ptr synthetic_request_generator_; std::unique_ptr delta_based_request_generator_; + std::unique_ptr + delta_based_realtime_updates_publisher_; std::vector>> grpc_client_workers_; std::unique_ptr parameter_fetcher_; + std::queue realtime_messages_; + absl::Mutex realtime_messages_mutex_; + std::unique_ptr realtime_message_batcher_; + std::unique_ptr concurrent_publishing_engine_; + bool is_running; friend class RequestSimulationSystemTestPeer; }; diff --git a/tools/request_simulation/request_simulation_system_local_test.cc b/tools/request_simulation/request_simulation_system_local_test.cc index 62843058..30b3eaf7 100644 --- a/tools/request_simulation/request_simulation_system_local_test.cc +++ b/tools/request_simulation/request_simulation_system_local_test.cc @@ -23,8 +23,8 @@ #include "grpcpp/grpcpp.h" #include "gtest/gtest.h" #include "public/testing/fake_key_value_service_impl.h" -#include "src/cpp/telemetry/mocks.h" -#include "src/cpp/util/duration.h" +#include "src/telemetry/mocks.h" +#include "src/util/duration.h" #include "tools/request_simulation/mocks.h" #include "tools/request_simulation/request_simulation_system.h" @@ -54,6 +54,7 @@ class MockRequestSimulationParameterFetcher : public RequestSimulationParameterFetcher { public: MOCK_METHOD(NotifierMetadata, GetBlobStorageNotifierMetadata, (), (const)); + MOCK_METHOD(NotifierMetadata, GetRealtimeNotifierMetadata, (), (const)); }; namespace { @@ -164,6 +165,10 @@ TEST_F(SimulationSystemTest, TestSimulationSystemRunning) { GetBlobStorageNotifierMetadata()) .WillRepeatedly(Return( LocalNotifierMetadata{.local_directory = ::testing::TempDir()})); + EXPECT_CALL(*mock_request_simulation_parameter_fetcher_, + GetRealtimeNotifierMetadata()) + .WillRepeatedly(Return( + LocalNotifierMetadata{.local_directory = ::testing::TempDir()})); RequestSimulationSystem system( metrics_recorder_, sim_clock_, channel_creation_fn, std::move(mock_request_simulation_parameter_fetcher_)); diff --git a/tools/request_simulation/synthetic_request_generator.h b/tools/request_simulation/synthetic_request_generator.h index 97aae750..e4b9a2e0 100644 --- a/tools/request_simulation/synthetic_request_generator.h +++ b/tools/request_simulation/synthetic_request_generator.h @@ -44,7 +44,7 @@ class SyntheticRequestGenerator { MessageQueue& message_queue, RateLimiter& rate_limiter, std::unique_ptr sleep_for, int64_t requests_fill_qps, absl::AnyInvocable request_body_generation_fn) - : thread_manager_(TheadManager::Create("Synthetic request generator")), + : thread_manager_(ThreadManager::Create("Synthetic request generator")), message_queue_(message_queue), rate_limiter_(rate_limiter), sleep_for_(std::move(sleep_for)), @@ -65,7 +65,7 @@ class SyntheticRequestGenerator { private: // The actual function that generates requests void GenerateRequests(); - std::unique_ptr thread_manager_; + std::unique_ptr thread_manager_; kv_server::MessageQueue& message_queue_; RateLimiter& rate_limiter_; std::unique_ptr sleep_for_; diff --git a/tools/server_diagnostic/helloworld_server/BUILD.bazel b/tools/server_diagnostic/helloworld_server/BUILD.bazel index 81af4c4b..a78f154d 100644 --- a/tools/server_diagnostic/helloworld_server/BUILD.bazel +++ b/tools/server_diagnostic/helloworld_server/BUILD.bazel @@ -23,11 +23,11 @@ cc_binary( deps = [ "//public/query:get_values_cc_grpc", "//public/query/v2:get_values_v2_cc_grpc", - "@com_github_google_glog//:glog", "@com_github_grpc_grpc//:grpc++", "@com_github_grpc_grpc//:grpc++_reflection", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", ], ) diff --git a/tools/server_diagnostic/helloworld_server/helloworld.cc b/tools/server_diagnostic/helloworld_server/helloworld.cc index 9474576a..4a110cae 100644 --- a/tools/server_diagnostic/helloworld_server/helloworld.cc +++ b/tools/server_diagnostic/helloworld_server/helloworld.cc @@ -13,7 +13,7 @@ // limitations under the License. #include "absl/flags/flag.h" -#include "glog/logging.h" +#include "absl/log/log.h" #include "grpcpp/ext/proto_server_reflection_plugin.h" #include "grpcpp/grpcpp.h" #include "public/query/get_values.grpc.pb.h" diff --git a/tools/serving_data_generator/BUILD.bazel b/tools/serving_data_generator/BUILD.bazel index 245f8e75..99f7e36d 100644 --- a/tools/serving_data_generator/BUILD.bazel +++ b/tools/serving_data_generator/BUILD.bazel @@ -14,6 +14,10 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") +package(default_visibility = [ + "//tools:__subpackages__", +]) + cc_binary( name = "test_serving_data_generator", srcs = ["test_serving_data_generator.cc"], @@ -23,9 +27,11 @@ cc_binary( "//public/data_loading:records_utils", "//public/data_loading:riegeli_metadata_cc_proto", "//public/sharding:sharding_function", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:flags", + "@com_google_absl//absl/log:initialize", "@com_google_absl//absl/strings", "@com_google_riegeli//riegeli/bytes:ostream_writer", "@com_google_riegeli//riegeli/records:record_writer", diff --git a/tools/serving_data_generator/test_serving_data_generator.cc b/tools/serving_data_generator/test_serving_data_generator.cc index eb5aa9fb..aebae233 100644 --- a/tools/serving_data_generator/test_serving_data_generator.cc +++ b/tools/serving_data_generator/test_serving_data_generator.cc @@ -18,8 +18,10 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/flags.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" #include "absl/strings/substitute.h" -#include "glog/logging.h" #include "google/protobuf/text_format.h" #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/filename_utils.h" @@ -29,18 +31,22 @@ #include "riegeli/bytes/ostream_writer.h" #include "riegeli/records/record_writer.h" -ABSL_FLAG(std::string, key, "foo", "Specify the key for lookups"); +ABSL_FLAG(std::string, key, "foo", "Specify the key prefix for lookups"); ABSL_FLAG(int, value_size, 10, "Specify the size of value for the key"); ABSL_FLAG(std::string, output_dir, "", "Output file directory"); +ABSL_FLAG(std::string, output_filename, "", "Output file name"); ABSL_FLAG(int, num_records, 5, "Number of records to generate"); ABSL_FLAG(int, num_shards, 1, "Number of shards"); ABSL_FLAG(int, shard_number, 0, "Shard number"); ABSL_FLAG(int64_t, timestamp, absl::ToUnixMicros(absl::Now()), - "Record timestamp"); + "Record timestamp. Increases by 1 for each record."); ABSL_FLAG(bool, generate_set_record, false, "Whether to generate set record or not"); +ABSL_FLAG(std::string, set_value_key, "bar", + "Specify the set value key prefix for lookups"); ABSL_FLAG(int, num_values_in_set, 10, "Number of values in the set to generate"); +ABSL_FLAG(int, num_set_records, 5, "Number of records to generate"); using kv_server::DataRecordStruct; using kv_server::KeyValueMutationRecordStruct; @@ -51,16 +57,27 @@ using kv_server::ToDeltaFileName; using kv_server::ToFlatBufferBuilder; using kv_server::ToStringView; -void WriteKeyValueRecords(std::string_view key, int value_size, - riegeli::RecordWriterBase& writer) { - const int repetition = absl::GetFlag(FLAGS_num_records); - int64_t timestamp = absl::GetFlag(FLAGS_timestamp); +void WriteKeyValueRecord(std::string_view key, std::string_view value, + int64_t logical_commit_time, + riegeli::RecordWriterBase& writer) { + auto kv_record = KeyValueMutationRecordStruct{ + KeyValueMutationType::Update, logical_commit_time, key, value}; + writer.WriteRecord(ToStringView( + ToFlatBufferBuilder(DataRecordStruct{.record = std::move(kv_record)}))); +} + +std::vector WriteKeyValueRecords( + std::string_view key, int value_size, int64_t timestamp, + riegeli::RecordWriterBase& writer) { + const int num_records = absl::GetFlag(FLAGS_num_records); const int64_t num_shards = absl::GetFlag(FLAGS_num_shards); const int64_t current_shard_number = absl::GetFlag(FLAGS_shard_number); + std::vector keys; std::string query(" "); - for (int i = 0; i < repetition; ++i) { + for (int i = 0; i < num_records; ++i) { const std::string value(value_size, 'A' + (i % 50)); const std::string actual_key = absl::StrCat(key, i); + keys.emplace_back(actual_key); if (num_shards > 1) { kv_server::ShardingFunction sharding_func(""); auto shard_number = @@ -69,39 +86,39 @@ void WriteKeyValueRecords(std::string_view key, int value_size, continue; } } - auto kv_record = KeyValueMutationRecordStruct{ - KeyValueMutationType::Update, timestamp++, actual_key, value}; - writer.WriteRecord(ToStringView( - ToFlatBufferBuilder(DataRecordStruct{.record = std::move(kv_record)}))); + WriteKeyValueRecord(actual_key, value, timestamp++, writer); absl::StrAppend(&query, "\"", actual_key, "\"", ", "); } LOG(INFO) << "Print keys to query " << query; LOG(INFO) << "write done"; + return keys; } -void WriteKeyValueSetRecords(std::string_view key, int value_size, +void WriteKeyValueSetRecords(const std::vector& keys, + std::string_view set_value_key_prefix, + int64_t timestamp, riegeli::RecordWriterBase& writer) { - const int repetition = absl::GetFlag(FLAGS_num_records); - int64_t timestamp = absl::GetFlag(FLAGS_timestamp); + const int num_set_records = absl::GetFlag(FLAGS_num_set_records); const int num_values_in_set = absl::GetFlag(FLAGS_num_values_in_set); + const int keys_max_index = keys.size() - 1; std::string query(" "); - for (int i = 0; i < repetition; ++i) { + for (int i = 0; i < num_set_records; ++i) { std::vector set_copy; for (int j = 0; j < num_values_in_set; ++j) { - const std::string value(value_size, 'A' + (j % 50)); - set_copy.emplace_back( - absl::StrCat(value, std::to_string(std::rand() % num_values_in_set))); + // Add a random element from keys + set_copy.emplace_back(keys[std::rand() % keys_max_index]); } std::vector set; for (const auto& v : set_copy) { set.emplace_back(v); } - absl::StrAppend(&query, absl::StrCat(key, i), " | "); + std::string set_value_key = absl::StrCat(set_value_key_prefix, i); + absl::StrAppend(&query, set_value_key, " | "); KeyValueMutationRecordStruct record; record.value = set; record.mutation_type = KeyValueMutationType::Update; record.logical_commit_time = timestamp++; - record.key = absl::StrCat(key, i); + record.key = set_value_key; writer.WriteRecord(ToStringView( ToFlatBufferBuilder(DataRecordStruct{.record = std::move(record)}))); } @@ -123,12 +140,15 @@ KVFileMetadata GetKVFileMetadata() { int main(int argc, char** argv) { const std::vector commands = absl::ParseCommandLine(argc, argv); - google::InitGoogleLogging(argv[0]); + absl::InitializeLog(); const std::string output_dir = absl::GetFlag(FLAGS_output_dir); + std::string output_filename = absl::GetFlag(FLAGS_output_filename); auto write_records = [](std::ostream* os) { const std::string key = absl::GetFlag(FLAGS_key); const int value_size = absl::GetFlag(FLAGS_value_size); + const std::string set_value_key_prefix = absl::GetFlag(FLAGS_set_value_key); + int64_t timestamp = absl::GetFlag(FLAGS_timestamp); auto os_writer = riegeli::OStreamWriter(os); riegeli::RecordWriterBase::Options options; @@ -138,34 +158,39 @@ int main(int argc, char** argv) { GetKVFileMetadata(); options.set_metadata(std::move(metadata)); auto record_writer = riegeli::RecordWriter(std::move(os_writer), options); + const auto keys = + WriteKeyValueRecords(key, value_size, timestamp, record_writer); if (absl::GetFlag(FLAGS_generate_set_record)) { - WriteKeyValueSetRecords(key, value_size, record_writer); - } else { - WriteKeyValueRecords(key, value_size, record_writer); + timestamp += keys.size(); + WriteKeyValueSetRecords(keys, set_value_key_prefix, timestamp, + record_writer); } - record_writer.Close(); }; if (output_dir == "-") { LOG(INFO) << "Writing records to console"; - write_records(&std::cout); - } else { + return 0; + } + + if (output_filename.empty()) { absl::Time now = absl::Now(); if (const auto maybe_name = ToDeltaFileName(absl::ToUnixMicros(now)); !maybe_name.ok()) { LOG(ERROR) << "Unable to construct file name: " << maybe_name.status(); return -1; } else { - const std::string outfile = - absl::StrCat(output_dir, "/", maybe_name.value()); - LOG(INFO) << "Writing records to " << outfile; - - std::ofstream ofs(outfile); - write_records(&ofs); - ofs.close(); + output_filename = *maybe_name; } } + + const std::string outfile = absl::StrCat(output_dir, "/", output_filename); + LOG(INFO) << "Writing records to " << outfile; + + std::ofstream ofs(outfile); + write_records(&ofs); + ofs.close(); + return 0; } diff --git a/tools/udf/closure_js/closure_to_delta.bzl b/tools/udf/closure_js/closure_to_delta.bzl index 81f49975..5ebc1094 100644 --- a/tools/udf/closure_js/closure_to_delta.bzl +++ b/tools/udf/closure_js/closure_to_delta.bzl @@ -20,6 +20,7 @@ def closure_to_delta( closure_js_library_target, custom_udf_js_handler = "HandleRequest", output_file_name = "DELTA_0000000000000009", + logical_commit_time = None, # Not passing a logical_commit_time will default to now. udf_tool = "//tools/udf/udf_generator:udf_delta_file_generator", tags = ["manual"]): """Generate a JS UDF delta file from a given closure_js_library target and put it under dist/ @@ -30,6 +31,7 @@ def closure_to_delta( closure_js_library_target = ":my_js_lib", custom_udf_js_handler = "MyHandlerName", output_file_name = "DELTA_0000000000000009", + logical_commit_time="123123123", ) Args: @@ -39,6 +41,7 @@ def closure_to_delta( output_file_name: Name of UDF delta file output udf_tool: BUILD target for the udf_delta_file_generator. Defaults to `//tools/udf/udf_generator:udf_delta_file_generator` + logical_commit_time: Logical commit timestamp for UDF config. Optional, defaults to now. tags: tags to propagate to rules """ closure_js_binary( @@ -50,6 +53,8 @@ def closure_to_delta( tags = tags, ) + logical_commit_time_args = [] if logical_commit_time == None else ["--logical_commit_time", logical_commit_time] + run_binary( name = "{}_udf_delta".format(name), srcs = [ @@ -65,7 +70,7 @@ def closure_to_delta( "$(location {})".format(output_file_name), "--udf_handler_name", custom_udf_js_handler, - ], + ] + logical_commit_time_args, tool = udf_tool, visibility = ["//visibility:private"], tags = tags, @@ -78,8 +83,8 @@ def closure_to_delta( ], outs = ["{}_copy_to_dist.bin".format(name)], cmd_bash = """cat << EOF > '$@' -mkdir -p dist -cp $(location {}_udf_delta) dist +mkdir -p dist/deltas +cp $(location {}_udf_delta) dist/deltas builders/tools/normalize-dist EOF""".format(name), executable = True, diff --git a/tools/udf/closure_js/examples/get_values_binary/udf.js b/tools/udf/closure_js/examples/get_values_binary/udf.js index 8980f573..63c63ba3 100644 --- a/tools/udf/closure_js/examples/get_values_binary/udf.js +++ b/tools/udf/closure_js/examples/get_values_binary/udf.js @@ -56,6 +56,33 @@ function getKeyGroupOutputs(udf_arguments) { return keyGroupOutputs; } +/** + * @param {!Array} udf_arguments + * @suppress {reportUnknownTypes} + * @return {Object} + */ +function handlePas(udf_arguments) { + if (udf_arguments.length != 1) { + const error_message = + 'For PAS default UDF exactly one argument should be provided, but was provided ' + udf_arguments.length; + console.error(error_message); + throw new Error(error_message); + } + var serializedGetValuesBinary = /** @type {!Array} */ (getValuesBinary(udf_arguments[0])); + var getValuesBinaryProto = proto.kv_server.BinaryGetValuesResponse.deserializeBinary(serializedGetValuesBinary); + return getValuesBinaryProto.getKvPairsMap(); +} + +/** + * @param {!Array} udf_arguments + * @suppress {reportUnknownTypes} + * @return {Object} + */ +function handlePa(udf_arguments) { + const keyGroupOutputs = getKeyGroupOutputs(udf_arguments); + return { keyGroupOutputs, udfOutputApiVersion: 1 }; +} + /** * Entry point for code snippet execution. * @@ -71,11 +98,15 @@ function getKeyGroupOutputs(udf_arguments) { * from being minified. * * @export + * @suppress {reportUnknownTypes} * @param {!Object} executionMetadata * @param {...?} udf_arguments * @return {Object} */ function HandleRequest(executionMetadata, ...udf_arguments) { - const keyGroupOutputs = getKeyGroupOutputs(udf_arguments); - return { keyGroupOutputs: keyGroupOutputs, udfOutputApiVersion: 1 }; + if (executionMetadata.requestMetadata && executionMetadata.requestMetadata.is_pas) { + console.log('Executing PAS branch'); + return handlePas(udf_arguments); + } + return handlePa(udf_arguments); } diff --git a/tools/udf/inline_wasm/examples/get_values_binary_proto/my_udf.js b/tools/udf/inline_wasm/examples/get_values_binary_proto/my_udf.js index fbb998e6..285d4bbd 100644 --- a/tools/udf/inline_wasm/examples/get_values_binary_proto/my_udf.js +++ b/tools/udf/inline_wasm/examples/get_values_binary_proto/my_udf.js @@ -16,11 +16,11 @@ async function HandleRequest(executionMetadata, ...udf_arguments) { const module = await getModule(); - logMessage('Done loading WASM Module'); + console.log('Done loading WASM Module'); // Pass in the getValuesBinary function for the C++ code to call. // getValuesBinary returns a Uint8Array, which emscripten converts to std::string const result = module.handleRequestCc(getValuesBinary, udf_arguments); - logMessage('handleRequestCc result: ' + JSON.stringify(result)); + console.log('handleRequestCc result: ' + JSON.stringify(result)); return result; } diff --git a/tools/udf/inline_wasm/examples/hello_world/BUILD.bazel b/tools/udf/inline_wasm/examples/hello_world/BUILD.bazel index 60acdc5c..e1936042 100644 --- a/tools/udf/inline_wasm/examples/hello_world/BUILD.bazel +++ b/tools/udf/inline_wasm/examples/hello_world/BUILD.bazel @@ -36,7 +36,7 @@ inline_wasm_udf_delta( wasm_binary = ":hello.wasm", ) -# builders/tools/bazel-debian run \ +# builders/tools/bazel-debian run --config=emscripten \ # //tools/udf/inline_wasm/examples/hello_world:hello_delta_cc cc_inline_wasm_udf_delta( name = "hello_delta_cc", diff --git a/tools/udf/inline_wasm/examples/hello_world/my_udf.js b/tools/udf/inline_wasm/examples/hello_world/my_udf.js index 81a9a3c7..e14ef923 100644 --- a/tools/udf/inline_wasm/examples/hello_world/my_udf.js +++ b/tools/udf/inline_wasm/examples/hello_world/my_udf.js @@ -41,9 +41,9 @@ function getKeyGroupOutputs(udf_arguments, module) { } async function HandleRequest(executionMetadata, ...udf_arguments) { - logMessage('Handling request'); + console.log('Handling request'); const module = await getModule(); - logMessage('Done loading WASM Module'); + console.log('Done loading WASM Module'); const keyGroupOutputs = getKeyGroupOutputs(udf_arguments, module); return { keyGroupOutputs, udfOutputApiVersion: 1 }; } diff --git a/tools/udf/inline_wasm/examples/js_call/my_udf.js b/tools/udf/inline_wasm/examples/js_call/my_udf.js index 038a5503..a156d031 100644 --- a/tools/udf/inline_wasm/examples/js_call/my_udf.js +++ b/tools/udf/inline_wasm/examples/js_call/my_udf.js @@ -15,12 +15,12 @@ */ async function HandleRequest(executionMetadata, ...input) { - logMessage('Handling request'); + console.log('Handling request'); const module = await getModule(); - logMessage('Done loading WASM Module'); + console.log('Done loading WASM Module'); // Pass in the getValues function for the C++ code to call. const result = module.handleRequestCc(getValues, input); - logMessage('handleRequestCc result: ' + JSON.stringify(result)); + console.log('handleRequestCc result: ' + JSON.stringify(result)); return result; } diff --git a/tools/udf/inline_wasm/examples/protobuf/my_udf.js b/tools/udf/inline_wasm/examples/protobuf/my_udf.js index 89d97825..57a5145a 100644 --- a/tools/udf/inline_wasm/examples/protobuf/my_udf.js +++ b/tools/udf/inline_wasm/examples/protobuf/my_udf.js @@ -42,9 +42,9 @@ function getKeyGroupOutputs(udf_arguments, module) { } async function HandleRequest(executionMetadata, ...udf_arguments) { - logMessage('Handling request'); + console.log('Handling request'); const module = await getModule(); - logMessage('Done loading WASM Module'); + console.log('Done loading WASM Module'); const keyGroupOutputs = getKeyGroupOutputs(udf_arguments, module); return { keyGroupOutputs, udfOutputApiVersion: 1 }; } diff --git a/tools/udf/inline_wasm/wasm.bzl b/tools/udf/inline_wasm/wasm.bzl index 1deb4f3f..07d49b24 100644 --- a/tools/udf/inline_wasm/wasm.bzl +++ b/tools/udf/inline_wasm/wasm.bzl @@ -22,7 +22,7 @@ def inline_wasm_udf_delta( custom_udf_js, custom_udf_js_handler = "HandleRequest", output_file_name = "DELTA_0000000000000005", - logical_commit_time = "123123123", + logical_commit_time = None, udf_tool = "//tools/udf/udf_generator:udf_delta_file_generator", tags = ["manual"]): """Generate a JS + inline WASM UDF delta file and put it under dist/ directory @@ -52,8 +52,7 @@ def inline_wasm_udf_delta( output_file_name: Name of UDF delta file output. Recommended to follow DELTA file naming convention. Defaults to `DELTA_0000000000000005` - logical_commit_time: Logical commit timestamp for UDF config. - Defaults to `123123123`. + logical_commit_time: Logical commit timestamp for UDF config. Optional, defaults to now. udf_tool: build target for the udf_delta_file_generator. Defaults to `//tools/udf/udf_generator:udf_delta_file_generator` tags: tags to propagate to rules @@ -79,6 +78,8 @@ def inline_wasm_udf_delta( tags = tags, ) + logical_commit_time_args = [] if logical_commit_time == None else ["--logical_commit_time", logical_commit_time] + run_binary( name = "{}_udf_delta".format(name), srcs = [ @@ -92,11 +93,9 @@ def inline_wasm_udf_delta( "$(location {}_generated)".format(name), "--output_path", "$(location {})".format(output_file_name), - "--logical_commit_time", - logical_commit_time, "--udf_handler_name", custom_udf_js_handler, - ], + ] + logical_commit_time_args, tool = udf_tool, visibility = ["//visibility:private"], tags = tags, @@ -110,11 +109,12 @@ def inline_wasm_udf_delta( ], outs = ["{}_copy_to_dist.bin".format(name)], cmd_bash = """cat << EOF > '$@' -mkdir -p dist/debian -cp $(location {}_udf_delta) dist -cp $(location {}_generated) dist +mkdir -p dist/deltas +mkdir -p dist/udfs +cp $(location {name}_udf_delta) dist/deltas +cp $(location {name}_generated) dist/udfs builders/tools/normalize-dist -EOF""".format(name, name), +EOF""".format(name = name), executable = True, local = True, message = "Copying {} dist directory".format(output_file_name), @@ -127,7 +127,7 @@ def cc_inline_wasm_udf_delta( custom_udf_js, custom_udf_js_handler = "HandleRequest", output_file_name = "DELTA_0000000000000005", - logical_commit_time = "123123123", + logical_commit_time = None, udf_tool = "//tools/udf/udf_generator:udf_delta_file_generator", deps = [], tags = ["manual"], @@ -167,7 +167,6 @@ def cc_inline_wasm_udf_delta( Recommended to follow DELTA file naming convention. Defaults to `DELTA_0000000000000005` logical_commit_time: Logical commit timestamp for UDF config. - Defaults to `123123123`. udf_tool: build target for the udf_delta_file_generator. Defaults to `//tools/udf/udf_generator:udf_delta_file_generator` tags: tags to propagate to rules diff --git a/tools/udf/sample_udf/udf.js b/tools/udf/sample_udf/udf.js index d840e1d2..d7cbc7ef 100644 --- a/tools/udf/sample_udf/udf.js +++ b/tools/udf/sample_udf/udf.js @@ -38,7 +38,31 @@ function getKeyGroupOutputs(udf_arguments) { return keyGroupOutputs; } -function HandleRequest(executionMetadata, ...udf_arguments) { +function handlePas(udf_arguments) { + if (udf_arguments.length != 1) { + let error_message = + 'For PAS default UDF exactly one argument should be provided, but was provided ' + udf_arguments.length; + console.error(error_message); + return error_message; + } + const kv_result = JSON.parse(getValues(udf_arguments[0])); + if (kv_result.hasOwnProperty('kvPairs')) { + return kv_result.kvPairs; + } + let error_message_lookup = 'Failed looking up values'; + console.error(error_message_lookup); + return error_message_lookup; +} + +function handlePA(udf_arguments) { const keyGroupOutputs = getKeyGroupOutputs(udf_arguments); return { keyGroupOutputs, udfOutputApiVersion: 1 }; } + +function HandleRequest(executionMetadata, ...udf_arguments) { + if (executionMetadata.requestMetadata && executionMetadata.requestMetadata.is_pas) { + console.log('Executing PAS branch'); + return handlePas(udf_arguments); + } + return handlePA(udf_arguments); +} diff --git a/tools/udf/udf_generator/BUILD.bazel b/tools/udf/udf_generator/BUILD.bazel index 17b0296a..78e56bf3 100644 --- a/tools/udf/udf_generator/BUILD.bazel +++ b/tools/udf/udf_generator/BUILD.bazel @@ -15,6 +15,7 @@ load("@rules_cc//cc:defs.bzl", "cc_binary") package(default_visibility = [ + "//docs/protected_app_signals:__subpackages__", "//getting_started:__subpackages__", "//production/packaging/tools:__subpackages__", "//testing:__subpackages__", @@ -40,9 +41,9 @@ cc_binary( "//public/data_loading/writers:delta_record_stream_writer", "//public/data_loading/writers:delta_record_writer", "//public/udf:constants", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", "@com_google_riegeli//riegeli/bytes:ostream_writer", "@com_google_riegeli//riegeli/records:record_writer", diff --git a/tools/udf/udf_generator/udf_delta_file_generator.cc b/tools/udf/udf_generator/udf_delta_file_generator.cc index c37fed04..b4e0e559 100644 --- a/tools/udf/udf_generator/udf_delta_file_generator.cc +++ b/tools/udf/udf_generator/udf_delta_file_generator.cc @@ -20,8 +20,8 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/log.h" #include "absl/strings/substitute.h" -#include "glog/logging.h" #include "google/protobuf/text_format.h" #include "public/constants.h" #include "public/data_loading/data_loading_generated.h" @@ -40,8 +40,8 @@ ABSL_FLAG(std::string, output_path, "", "Output path. If specified, output_dir is ignored. If '-', output is " "written to " "console."); -ABSL_FLAG(int64_t, logical_commit_time, 123123123, - "Record logical_commit_time. Default is 123123123."); +ABSL_FLAG(int64_t, logical_commit_time, absl::ToUnixMicros(absl::Now()), + "Record logical_commit_time. Default is current timestamp."); ABSL_FLAG(int64_t, code_snippet_version, 2, "UDF version. Default is 2."); ABSL_FLAG(std::string, data_loading_file_format, std::string(kv_server::kFileFormats[static_cast( diff --git a/tools/udf/udf_tester/BUILD.bazel b/tools/udf/udf_tester/BUILD.bazel index 98d1b458..de2873b8 100644 --- a/tools/udf/udf_tester/BUILD.bazel +++ b/tools/udf/udf_tester/BUILD.bazel @@ -33,11 +33,11 @@ cc_binary( "//public/data_loading/readers:delta_record_stream_reader", "//public/query/v2:get_values_v2_cc_proto", "//public/udf:constants", - "@com_github_google_glog//:glog", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", "@com_google_absl//absl/strings", "@com_google_protobuf//:protobuf", - "@google_privacysandbox_servers_common//src/cpp/util/status_macro:status_macros", + "@google_privacysandbox_servers_common//src/util/status_macro:status_macros", ], ) diff --git a/tools/udf/udf_tester/README.md b/tools/udf/udf_tester/README.md index cb868663..3496d3ea 100644 --- a/tools/udf/udf_tester/README.md +++ b/tools/udf/udf_tester/README.md @@ -4,7 +4,8 @@ This binary directly invokes UDF which can access data input. It requires two delta files: -- Delta file with key-value pairs to be stored in memory ([docs](/docs/loading_data.md)) +- Delta file with key-value pairs to be stored in memory + ([docs](/docs/data_loading/loading_data.md)) - Delta file with the UDF configuration ([docs](/docs/generating_udf_files.md)). ## Flags: diff --git a/tools/udf/udf_tester/udf_delta_file_tester.cc b/tools/udf/udf_tester/udf_delta_file_tester.cc index ea498e95..d7e178fa 100644 --- a/tools/udf/udf_tester/udf_delta_file_tester.cc +++ b/tools/udf/udf_tester/udf_delta_file_tester.cc @@ -16,6 +16,7 @@ #include "absl/flags/flag.h" #include "absl/flags/parse.h" +#include "absl/log/log.h" #include "absl/strings/substitute.h" #include "components/data_server/cache/cache.h" #include "components/data_server/cache/key_value_cache.h" @@ -23,15 +24,13 @@ #include "components/udf/hooks/get_values_hook.h" #include "components/udf/udf_client.h" #include "components/udf/udf_config_builder.h" -#include "glog/logging.h" #include "google/protobuf/util/json_util.h" #include "public/data_loading/data_loading_generated.h" #include "public/data_loading/readers/delta_record_stream_reader.h" #include "public/query/v2/get_values_v2.pb.h" #include "public/udf/constants.h" -#include "src/cpp/telemetry/metrics_recorder.h" -#include "src/cpp/telemetry/telemetry_provider.h" -#include "src/cpp/util/status_macro/status_macros.h" +#include "src/telemetry/telemetry_provider.h" +#include "src/util/status_macro/status_macros.h" ABSL_FLAG(std::string, kv_delta_file_path, "", "Path to delta file with KV pairs."); @@ -138,9 +137,9 @@ void ShutdownUdf(UdfClient& udf_client) { absl::Status TestUdf(const std::string& kv_delta_file_path, const std::string& udf_delta_file_path, const std::string& input_arguments) { + InitMetricsContextMap(); LOG(INFO) << "Loading cache from delta file: " << kv_delta_file_path; - auto noop_metrics_recorder = MetricsRecorder::CreateNoop(); - std::unique_ptr cache = KeyValueCache::Create(*noop_metrics_recorder); + std::unique_ptr cache = KeyValueCache::Create(); PS_RETURN_IF_ERROR(LoadCacheFromFile(kv_delta_file_path, *cache)) << "Error loading cache from file"; @@ -154,20 +153,18 @@ absl::Status TestUdf(const std::string& kv_delta_file_path, UdfConfigBuilder config_builder; auto string_get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kString); - string_get_values_hook->FinishInit( - CreateLocalLookup(*cache, *noop_metrics_recorder)); + string_get_values_hook->FinishInit(CreateLocalLookup(*cache)); auto binary_get_values_hook = GetValuesHook::Create(GetValuesHook::OutputType::kBinary); - binary_get_values_hook->FinishInit( - CreateLocalLookup(*cache, *noop_metrics_recorder)); + binary_get_values_hook->FinishInit(CreateLocalLookup(*cache)); auto run_query_hook = RunQueryHook::Create(); - run_query_hook->FinishInit(CreateLocalLookup(*cache, *noop_metrics_recorder)); + run_query_hook->FinishInit(CreateLocalLookup(*cache)); absl::StatusOr> udf_client = UdfClient::Create(std::move( config_builder.RegisterStringGetValuesHook(*string_get_values_hook) .RegisterBinaryGetValuesHook(*binary_get_values_hook) .RegisterRunQueryHook(*run_query_hook) - .RegisterLoggingHook() + .RegisterLoggingFunction() .SetNumberOfWorkers(1) .Config())); PS_RETURN_IF_ERROR(udf_client.status()) @@ -188,8 +185,9 @@ absl::Status TestUdf(const std::string& kv_delta_file_path, JsonStringToMessage(req_partition_json, &req_partition); LOG(INFO) << "Calling UDF for partition: " << req_partition.DebugString(); - auto udf_result = - udf_client.value()->ExecuteCode({}, req_partition.arguments()); + auto metrics_context = std::make_unique(); + auto udf_result = udf_client.value()->ExecuteCode( + RequestContext(*metrics_context), {}, req_partition.arguments()); if (!udf_result.ok()) { LOG(ERROR) << "UDF execution failed: " << udf_result.status(); ShutdownUdf(*udf_client.value()); diff --git a/version.txt b/version.txt index 7092c7c4..d183d4ac 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.15.0 \ No newline at end of file +0.16.0 \ No newline at end of file