Cleanup user and brewery generation exception logic

This commit is contained in:
Aaron Po
2026-06-21 23:09:29 -04:00
parent ad97b0ea6c
commit 3f978f4de4
9 changed files with 114 additions and 86 deletions

View File

@@ -110,9 +110,9 @@ class BiergartenPipelineOrchestrator {
void LogResults() const; void LogResults() const;
/// @brief Stores generated brewery data. /// @brief Stores generated brewery data.
std::vector<GeneratedBrewery> generated_breweries_; std::vector<BreweryRecord> generated_breweries_;
/// @brief Stores generated user data. /// @brief Stores generated user data.
std::vector<GeneratedUser> generated_users_; std::vector<UserRecord> generated_users_;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_BIERGARTEN_DATA_GENERATOR_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_BIERGARTEN_DATA_GENERATOR_H_

View File

@@ -74,7 +74,7 @@ struct EnrichedCity {
/** /**
* @brief Helper struct to store generated brewery data. * @brief Helper struct to store generated brewery data.
*/ */
struct GeneratedBrewery { struct BreweryRecord {
Location location; Location location;
BreweryResult brewery; BreweryResult brewery;
}; };
@@ -86,7 +86,7 @@ struct GeneratedBrewery {
* the orchestrator (never LLM-authored) so a downstream auth-account seeding * the orchestrator (never LLM-authored) so a downstream auth-account seeding
* consumer can register real accounts from the pipeline's SQLite export. * consumer can register real accounts from the pipeline's SQLite export.
*/ */
struct GeneratedUser { struct UserRecord {
Location location; Location location;
UserResult user; UserResult user;
std::string email{}; std::string email{};

View File

@@ -53,17 +53,6 @@ struct Location {
double longitude{}; double longitude{};
}; };
/**
* @brief Non-owning brewery location input.
*/
struct BreweryLocation {
/// @brief City name.
std::string_view city_name;
/// @brief Country name.
std::string_view country_name;
};
// ============================================================================ // ============================================================================
// Name / Persona Models // Name / Persona Models
// ============================================================================ // ============================================================================

View File

@@ -33,14 +33,14 @@ class IExportService {
* *
* @param brewery Generated brewery payload to store. * @param brewery Generated brewery payload to store.
*/ */
virtual uint64_t ProcessRecord(const GeneratedBrewery& brewery) = 0; virtual uint64_t ProcessRecord(const BreweryRecord& brewery) = 0;
/** /**
* @brief Persists one generated user record. * @brief Persists one generated user record.
* *
* @param user Generated user payload to store. * @param user Generated user payload to store.
*/ */
virtual uint64_t ProcessRecord(const GeneratedUser& user) = 0; virtual uint64_t ProcessRecord(const UserRecord& user) = 0;
/// @brief Finalizes the export destination. /// @brief Finalizes the export destination.
virtual void Finalize() = 0; virtual void Finalize() = 0;

View File

@@ -30,8 +30,8 @@ class SqliteExportService final : public IExportService {
SqliteExportService& operator=(SqliteExportService&&) = delete; SqliteExportService& operator=(SqliteExportService&&) = delete;
void Initialize() override; void Initialize() override;
uint64_t ProcessRecord(const GeneratedBrewery& brewery) override; uint64_t ProcessRecord(const BreweryRecord& brewery) override;
uint64_t ProcessRecord(const GeneratedUser& user) override; uint64_t ProcessRecord(const UserRecord& user) override;
void Finalize() override; void Finalize() override;
private: private:

View File

@@ -5,6 +5,7 @@
#include <chrono> #include <chrono>
#include <format> #include <format>
#include <optional>
#include "biergarten_pipeline_orchestrator.h" #include "biergarten_pipeline_orchestrator.h"
#include "services/logging/logger.h" #include "services/logging/logger.h"
@@ -19,28 +20,14 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
size_t skipped_count = 0; size_t skipped_count = 0;
size_t export_failed_count = 0; size_t export_failed_count = 0;
for (const auto& [location, region_context] : cities) { const auto generate_record =
[this, &skipped_count](
const Location& location,
const std::string& region_context) -> std::optional<BreweryRecord> {
try { try {
const BreweryResult brewery = const BreweryResult brewery =
generator_->GenerateBrewery(location, region_context); generator_->GenerateBrewery(location, region_context);
return BreweryRecord{.location = location, .brewery = brewery};
const GeneratedBrewery gen{.location = location, .brewery = brewery};
generated_breweries_.push_back(gen);
try {
exporter_->ProcessRecord(gen);
} catch (const std::exception& export_exception) {
++export_failed_count;
logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::BreweryAndBeerGeneration,
.message = std::format(
"[Pipeline] Generated brewery for '{}' ({}) "
"but SQLite export failed: {}",
location.city, location.country, export_exception.what())});
}
} catch (const std::exception& e) { } catch (const std::exception& e) {
++skipped_count; ++skipped_count;
@@ -50,7 +37,35 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
.message = std::format("[Pipeline] Skipping city '{}' ({}): brewery " .message = std::format("[Pipeline] Skipping city '{}' ({}): brewery "
"generation failed: {}", "generation failed: {}",
location.city, location.country, e.what())}); location.city, location.country, e.what())});
return std::nullopt;
} }
};
const auto export_record = [this, &export_failed_count](
const BreweryRecord& record) {
try {
exporter_->ProcessRecord(record);
} catch (const std::exception& export_exception) {
++export_failed_count;
logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::BreweryAndBeerGeneration,
.message = std::format("[Pipeline] Generated brewery for '{}' ({}) "
"but SQLite export failed: {}",
record.location.city, record.location.country,
export_exception.what())});
}
};
for (const auto& [location, region_context] : cities) {
const std::optional<BreweryRecord> record =
generate_record(location, region_context);
if (!record.has_value()) {
continue;
}
generated_breweries_.push_back(*record);
export_record(*record);
} }
if (skipped_count > 0) { if (skipped_count > 0) {

View File

@@ -37,9 +37,13 @@ std::string BuildEmail(const Name& name,
std::format("{}.{}", Sanitize(name.first_name), Sanitize(name.last_name)); std::format("{}.{}", Sanitize(name.first_name), Sanitize(name.last_name));
std::string local_part = base; std::string local_part = base;
for (int suffix = 1; used_local_parts.contains(local_part); ++suffix) { uint32_t suffix = 1;
while (used_local_parts.contains(local_part)) {
local_part = std::format("{}{}", base, suffix); local_part = std::format("{}{}", base, suffix);
++suffix;
} }
used_local_parts.insert(local_part); used_local_parts.insert(local_part);
return std::format("{}@thebiergarten.app", local_part); return std::format("{}@thebiergarten.app", local_part);
@@ -68,17 +72,17 @@ std::string GenerateDateOfBirth(std::mt19937& rng) {
} }
std::string GenerateRandomPassword(std::mt19937& rng) { std::string GenerateRandomPassword(std::mt19937& rng) {
constexpr size_t kPasswordLength = 32; constexpr size_t k_password_length = 32;
constexpr std::string_view kCharset = constexpr std::string_view k_charset =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&" "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&"
"*"; "*";
std::uniform_int_distribution<size_t> char_dist(0, kCharset.size() - 1); std::uniform_int_distribution<size_t> char_dist(0, k_charset.size() - 1);
std::string password; std::string password;
password.reserve(kPasswordLength); password.reserve(k_password_length);
for (size_t i = 0; i < kPasswordLength; ++i) { for (size_t i = 0; i < k_password_length; ++i) {
password.push_back(kCharset[char_dist(rng)]); password.push_back(k_charset[char_dist(rng)]);
} }
return password; return password;
} }
@@ -93,6 +97,7 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
const std::vector<UserPersona> personas = const std::vector<UserPersona> personas =
JsonLoader::LoadPersonas("personas.json", logger_); JsonLoader::LoadPersonas("personas.json", logger_);
if (personas.empty()) { if (personas.empty()) {
throw std::runtime_error( throw std::runtime_error(
"No personas available in personas.json for user generation"); "No personas available in personas.json for user generation");
@@ -109,6 +114,50 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
size_t skipped_count = 0; size_t skipped_count = 0;
size_t export_failed_count = 0; size_t export_failed_count = 0;
const auto generate_record =
[this, &rng, &used_email_local_parts, &skipped_count](
const EnrichedCity& city, const UserPersona& persona,
const Name& sampled_name) -> std::optional<UserRecord> {
try {
const UserResult user =
generator_->GenerateUser(city, persona, sampled_name);
return UserRecord{
.location = city.location,
.user = user,
.email = BuildEmail(sampled_name, used_email_local_parts),
.date_of_birth = GenerateDateOfBirth(rng),
.password = GenerateRandomPassword(rng),
};
} catch (const std::exception& e) {
++skipped_count;
logger_->Log({.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration,
.message = std::format(
"[Pipeline] Skipping city '{}' ({}): "
"user generation failed: {}",
city.location.city, city.location.country, e.what())});
return std::nullopt;
}
};
const auto export_record = [this,
&export_failed_count](const UserRecord& record) {
try {
exporter_->ProcessRecord(record);
} catch (const std::exception& export_exception) {
++export_failed_count;
logger_->Log(
{.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration,
.message = std::format("[Pipeline] Generated user for '{}' ({}) but "
"SQLite export failed: {}",
record.location.city, record.location.country,
export_exception.what())});
}
};
for (const auto& city : cities) { for (const auto& city : cities) {
const std::optional<Name> sampled_name = const std::optional<Name> sampled_name =
names_by_country.SampleName(city.location.iso3166_1, rng); names_by_country.SampleName(city.location.iso3166_1, rng);
@@ -119,8 +168,7 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
.phase = PipelinePhase::UserGeneration, .phase = PipelinePhase::UserGeneration,
.message = std::format( .message = std::format(
"[Pipeline] Skipping city '{}' ({}): no names " "[Pipeline] Skipping city '{}' ({}): no names "
"available for " "available for country '{}'",
"country '{}'",
city.location.city, city.location.country, city.location.city, city.location.country,
city.location.iso3166_1)}); city.location.iso3166_1)});
continue; continue;
@@ -128,42 +176,14 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
const UserPersona& persona = personas[persona_dist(rng)]; const UserPersona& persona = personas[persona_dist(rng)];
try { const std::optional<UserRecord> record =
const UserResult user = generate_record(city, persona, *sampled_name);
generator_->GenerateUser(city, persona, *sampled_name); if (!record.has_value()) {
continue;
const GeneratedUser generated_user{
.location = city.location,
.user = user,
.email = BuildEmail(*sampled_name, used_email_local_parts),
.date_of_birth = GenerateDateOfBirth(rng),
.password = GenerateRandomPassword(rng),
};
generated_users_.push_back(generated_user);
try {
exporter_->ProcessRecord(generated_user);
} catch (const std::exception& export_exception) {
++export_failed_count;
logger_->Log({.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration,
.message = std::format(
"[Pipeline] Generated user for '{}' ({}) but "
"SQLite export failed: {}",
city.location.city, city.location.country,
export_exception.what())});
}
} catch (const std::exception& e) {
++skipped_count;
logger_->Log({.level = LogLevel::Warn,
.phase = PipelinePhase::UserGeneration,
.message = std::format(
"[Pipeline] Skipping city '{}' ({}): "
"user generation failed: {}",
city.location.city, city.location.country, e.what())});
} }
generated_users_.push_back(*record);
export_record(*record);
} }
if (skipped_count > 0) { if (skipped_count > 0) {

View File

@@ -1,6 +1,6 @@
/** /**
* @file services/sqlite/process_record.cc * @file services/sqlite/process_record.cc
* @brief SqliteExportService::ProcessRecord(GeneratedBrewery) implementation * @brief SqliteExportService::ProcessRecord() implementation
* and the shared location-resolution helper. * and the shared location-resolution helper.
*/ */
@@ -102,7 +102,7 @@ sqlite3_int64 SqliteExportService::ResolveLocationId(const Location& location) {
return location_id; return location_id;
} }
uint64_t SqliteExportService::ProcessRecord(const GeneratedBrewery& brewery) { uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
if (db_handle_ == nullptr || !transaction_open_) { if (db_handle_ == nullptr || !transaction_open_) {
throw std::runtime_error("SQLite export service is not initialized"); throw std::runtime_error("SQLite export service is not initialized");
} }
@@ -115,12 +115,14 @@ uint64_t SqliteExportService::ProcessRecord(const GeneratedBrewery& brewery) {
.index = sqlite_export_service_internal::kBreweryLocationIdBindIndex, .index = sqlite_export_service_internal::kBreweryLocationIdBindIndex,
.value = location_id, .value = location_id,
.action = "Failed to bind SQLite brewery location id"}); .action = "Failed to bind SQLite brewery location id"});
sqlite_export_service_internal::Bind( sqlite_export_service_internal::Bind(
insert_brewery_stmt_, insert_brewery_stmt_,
sqlite_export_service_internal::BindParam<std::string_view>{ sqlite_export_service_internal::BindParam<std::string_view>{
.index = sqlite_export_service_internal::kBreweryEnglishNameBindIndex, .index = sqlite_export_service_internal::kBreweryEnglishNameBindIndex,
.value = brewery.brewery.name_en, .value = brewery.brewery.name_en,
.action = "Failed to bind SQLite brewery English name"}); .action = "Failed to bind SQLite brewery English name"});
sqlite_export_service_internal::Bind( sqlite_export_service_internal::Bind(
insert_brewery_stmt_, insert_brewery_stmt_,
sqlite_export_service_internal::BindParam<std::string_view>{ sqlite_export_service_internal::BindParam<std::string_view>{
@@ -128,12 +130,14 @@ uint64_t SqliteExportService::ProcessRecord(const GeneratedBrewery& brewery) {
kBreweryEnglishDescriptionBindIndex, kBreweryEnglishDescriptionBindIndex,
.value = brewery.brewery.description_en, .value = brewery.brewery.description_en,
.action = "Failed to bind SQLite brewery English description"}); .action = "Failed to bind SQLite brewery English description"});
sqlite_export_service_internal::Bind( sqlite_export_service_internal::Bind(
insert_brewery_stmt_, insert_brewery_stmt_,
sqlite_export_service_internal::BindParam<std::string_view>{ sqlite_export_service_internal::BindParam<std::string_view>{
.index = sqlite_export_service_internal::kBreweryLocalNameBindIndex, .index = sqlite_export_service_internal::kBreweryLocalNameBindIndex,
.value = brewery.brewery.name_local, .value = brewery.brewery.name_local,
.action = "Failed to bind SQLite brewery local name"}); .action = "Failed to bind SQLite brewery local name"});
sqlite_export_service_internal::Bind( sqlite_export_service_internal::Bind(
insert_brewery_stmt_, insert_brewery_stmt_,
sqlite_export_service_internal::BindParam<std::string_view>{ sqlite_export_service_internal::BindParam<std::string_view>{

View File

@@ -1,6 +1,6 @@
/** /**
* @file services/sqlite/process_user_record.cc * @file services/sqlite/process_user_record.cc
* @brief SqliteExportService::ProcessRecord(GeneratedUser) implementation. * @brief SqliteExportService::ProcessRecord(UserRecord) implementation.
*/ */
#include <stdexcept> #include <stdexcept>
@@ -8,7 +8,7 @@
#include "services/database/sqlite_export_service.h" #include "services/database/sqlite_export_service.h"
#include "services/database/sqlite_export_service_helpers.h" #include "services/database/sqlite_export_service_helpers.h"
uint64_t SqliteExportService::ProcessRecord(const GeneratedUser& user) { uint64_t SqliteExportService::ProcessRecord(const UserRecord& user) {
if (db_handle_ == nullptr || !transaction_open_) { if (db_handle_ == nullptr || !transaction_open_) {
throw std::runtime_error("SQLite export service is not initialized"); throw std::runtime_error("SQLite export service is not initialized");
} }