mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
Cleanup user and brewery generation exception logic
This commit is contained in:
@@ -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_
|
||||||
|
|||||||
@@ -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{};
|
||||||
|
|||||||
@@ -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
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>{
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user