Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3085,6 +3085,10 @@ def _create_media_buy_impl(

# 3. Package/Product validation
product_ids = req.get_product_ids()
logger.info(f"DEBUG: Extracted product_ids: {product_ids}")
logger.info(
f"DEBUG: Request packages: {[{'package_id': p.package_id, 'product_id': p.product_id, 'products': p.products, 'buyer_ref': p.buyer_ref} for p in (req.packages or [])]}"
)
if not product_ids:
error_msg = "At least one product is required."
raise ValueError(error_msg)
Expand Down
9 changes: 9 additions & 0 deletions src/core/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ def initialize_application() -> None:
validate_configuration()
logger.info("✅ Configuration validation passed")

# Restart active delivery simulations (mock adapter only)
try:
from src.services.delivery_simulator import delivery_simulator

delivery_simulator.restart_active_simulations()
except Exception as e:
logger.warning(f"⚠️ Failed to restart delivery simulations: {e}")
# Don't fail startup if simulations can't restart

logger.info("🎉 Application initialization completed successfully")

except Exception as e:
Expand Down
95 changes: 95 additions & 0 deletions src/services/delivery_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,101 @@ def __init__(self):
# Register graceful shutdown
atexit.register(self._shutdown)

def restart_active_simulations(self):
"""Restart delivery simulations for active media buys.

Called on server startup to resume simulations that were running
before a server restart. Daemon threads don't survive restarts.
"""
try:

from sqlalchemy import select

from src.core.database.database_session import get_db_session
from src.core.database.models import MediaBuy, Product, PushNotificationConfig

logger.info("🔄 Checking for active media buys to restart simulations...")

with get_db_session() as session:
# Find active media buys with webhook configs
stmt = (
select(MediaBuy, PushNotificationConfig)
.join(PushNotificationConfig, MediaBuy.media_buy_id == PushNotificationConfig.media_buy_id)
.where(MediaBuy.status.in_(["active", "working"]))
.where(PushNotificationConfig.enabled == True) # noqa: E712
)

results = session.execute(stmt).all()

restarted_count = 0
for media_buy, _webhook_config in results:
# Check if simulation is already running
if media_buy.media_buy_id in self._active_simulations:
continue

# Extract product IDs from the media buy packages
raw_request = media_buy.raw_request or {}
packages = raw_request.get("packages", [])
if not packages:
continue

# Get the first product (for simulation config)
first_package = packages[0]
product_id = first_package.get("product_id") or (first_package.get("products") or [None])[0]
if not product_id:
continue

# Look up the product
product_stmt = select(Product).where(
Product.tenant_id == media_buy.tenant_id, Product.product_id == product_id
)
product = session.scalars(product_stmt).first()
if not product:
continue

# Only restart for mock adapter products
if product.adapter_type != "mock":
continue

# Get simulation config from product
impl_config = product.implementation_config or {}
delivery_sim_config = impl_config.get("delivery_simulation", {})

# Skip if simulation is disabled
if not delivery_sim_config.get("enabled", True):
continue

logger.info(
f"🚀 Restarting simulation for {media_buy.media_buy_id} " f"(product: {product.product_id})"
)

try:
self.start_simulation(
media_buy_id=media_buy.media_buy_id,
tenant_id=media_buy.tenant_id,
principal_id=media_buy.principal_id,
start_time=media_buy.start_time,
end_time=media_buy.end_time,
total_budget=float(media_buy.total_budget),
time_acceleration=delivery_sim_config.get("time_acceleration", 3600),
update_interval_seconds=delivery_sim_config.get("update_interval_seconds", 1.0),
)
restarted_count += 1
except Exception as e:
logger.error(f"Failed to restart simulation for {media_buy.media_buy_id}: {e}")

if restarted_count > 0:
logger.info(f"✅ Restarted {restarted_count} delivery simulation(s)")
else:
logger.info("✅ No active simulations to restart")

except Exception as e:
logger.error(f"⚠️ Failed to restart delivery simulations: {e}")
# Don't crash the server if restart fails
import traceback

logger.error(f"Traceback: {traceback.format_exc()}")

def start_simulation(
self,
media_buy_id: str,
Expand Down
149 changes: 73 additions & 76 deletions tests/e2e/schemas/v1/_schemas_v1_core_creative-asset_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/v1/core/creative-asset.json",
"title": "Creative Asset",
"description": "Creative asset for upload to library - supports both hosted assets and third-party snippets",
"description": "Creative asset for upload to library - supports static assets, generative formats, and third-party snippets",
"type": "object",
"properties": {
"creative_id": {
Expand All @@ -13,42 +13,75 @@
"type": "string",
"description": "Human-readable creative name"
},
"format": {
"type": "string",
"description": "Creative format type (e.g., video, audio, display)"
},
"media_url": {
"type": "string",
"format": "uri",
"description": "URL of the creative file (for hosted assets)"
},
"snippet": {
"type": "string",
"description": "Third-party tag, VAST XML, or code snippet (for third-party served assets)"
},
"snippet_type": {
"$ref": "/schemas/v1/enums/snippet-type.json",
"description": "Type of snippet content"
},
"click_url": {
"type": "string",
"format": "uri",
"description": "Landing page URL for the creative"
"format_id": {
"$ref": "/schemas/v1/core/format-id.json",
"description": "Format identifier specifying which format this creative conforms to"
},
"duration": {
"type": "number",
"description": "Duration in milliseconds (for video/audio)",
"minimum": 0
},
"width": {
"type": "number",
"description": "Width in pixels (for video/display)",
"minimum": 0
"assets": {
"type": "object",
"description": "Assets required by the format, keyed by asset_role",
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"oneOf": [
{
"$ref": "/schemas/v1/core/assets/image-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/video-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/audio-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/text-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/html-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/css-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/javascript-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/promoted-offerings-asset.json"
},
{
"$ref": "/schemas/v1/core/assets/url-asset.json"
}
]
}
},
"additionalProperties": false
},
"height": {
"type": "number",
"description": "Height in pixels (for video/display)",
"minimum": 0
"inputs": {
"type": "array",
"description": "Preview contexts for generative formats - defines what scenarios to generate previews for",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Human-readable name for this preview variant"
},
"macros": {
"type": "object",
"description": "Macro values to apply for this preview",
"additionalProperties": {
"type": "string"
}
},
"context_description": {
"type": "string",
"description": "Natural language description of the context for AI-generated content"
}
},
"required": [
"name"
],
"additionalProperties": false
}
},
"tags": {
"type": "array",
Expand All @@ -57,52 +90,16 @@
"type": "string"
}
},
"assets": {
"type": "array",
"description": "Sub-assets for multi-asset formats like carousels",
"items": {
"$ref": "/schemas/v1/core/sub-asset.json"
}
"approved": {
"type": "boolean",
"description": "For generative creatives: set to true to approve and finalize, false to request regeneration with updated assets/message. Omit for non-generative creatives."
}
},
"required": [
"creative_id",
"name",
"format"
],
"oneOf": [
{
"description": "Hosted asset - requires media_url",
"required": [
"media_url"
],
"not": {
"anyOf": [
{
"required": [
"snippet"
]
},
{
"required": [
"snippet_type"
]
}
]
}
},
{
"description": "Third-party asset - requires snippet and snippet_type",
"required": [
"snippet",
"snippet_type"
],
"not": {
"required": [
"media_url"
]
}
}
"format_id",
"assets"
],
"additionalProperties": false
}
20 changes: 18 additions & 2 deletions tests/e2e/schemas/v1/_schemas_v1_core_format_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@
"type": "string",
"description": "Human-readable format name"
},
"description": {
"type": "string",
"description": "Plain text explanation of what this format does and what assets it requires"
},
"preview_image": {
"type": "string",
"format": "uri",
"description": "Optional preview image URL for format browsing/discovery UI"
},
"example_url": {
"type": "string",
"format": "uri",
"description": "Optional URL to showcase page with examples and interactive demos of this format"
},
"type": {
"type": "string",
"description": "Media type of this format - determines rendering method and asset requirements",
Expand Down Expand Up @@ -71,7 +85,8 @@
"text",
"html",
"javascript",
"url"
"url",
"brand_manifest"
]
},
"asset_role": {
Expand Down Expand Up @@ -138,7 +153,8 @@
"text",
"html",
"javascript",
"url"
"url",
"brand_manifest"
]
},
"asset_role": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,13 @@
"$ref": "/schemas/v1/media-buy/package-request.json"
}
},
"brand_card": {
"$ref": "/schemas/v1/core/brand-card.json",
"brand_manifest": {
"$ref": "/schemas/v1/core/brand-manifest.json",
"description": "Brand information manifest serving as the namespace and identity for this media buy. Provides brand context, assets, and product catalog. Can be cached and reused across multiple requests."
},
"promoted_offering": {
"type": "string",
"description": "DEPRECATED: Use brand_card with promoted_products instead. Legacy field for describing what is being promoted."
},
"promoted_products": {
"$ref": "/schemas/v1/core/promoted-products.json",
"description": "Products or offerings being promoted in this media buy. Supports SKU selection from brand card's product catalog, or inline offerings for non-commerce campaigns."
"description": "DEPRECATED: Use brand_manifest instead. Legacy field for describing what is being promoted."
},
"po_number": {
"type": "string",
Expand Down Expand Up @@ -97,7 +93,7 @@
"required": [
"buyer_ref",
"packages",
"brand_card",
"brand_manifest",
"start_time",
"end_time",
"budget"
Expand Down
Loading