Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
70c6ab9
feat: Add weekly schedule for Azure Template validation,split azure-d…
VishalS-Microsoft Mar 31, 2026
39c5076
feat: Enable push trigger for Azure Dev Deploy workflow on psl-weekly…
VishalS-Microsoft Mar 31, 2026
013bd72
feat: Remove push trigger for psl-weeklyschedule-codmod branch in wor…
VishalS-Microsoft Apr 1, 2026
db80ea7
feat: Add push trigger and timestamp setting for Azure workflows on p…
VishalS-Microsoft Apr 2, 2026
5e5b2da
feat: Remove push trigger for psl-weeklyschedule-codmod branch in wor…
VishalS-Microsoft Apr 2, 2026
d7c6779
Enhance CosmosDBClient and DatabaseFactory for improved error handlin…
Pavan-Microsoft Apr 3, 2026
73f054a
Merge remote-tracking branch 'origin/dev' into psl-pk-cosavmfix
Pavan-Microsoft Apr 3, 2026
a6b843a
Refactor test_create_batch_exists to mock read_item instead of get_ba…
Pavan-Microsoft Apr 3, 2026
7815d5c
Enhance CosmosDBClient error logging and improve test assertions for …
Pavan-Microsoft Apr 3, 2026
e481341
Refactor DatabaseFactory to lazily initialize the lock and improve fi…
Pavan-Microsoft Apr 3, 2026
6bdd50e
fix: avoid leaking batch existence on user mismatch (Copilot review)
Pavan-Microsoft Apr 3, 2026
bda0369
fix: update mock_query_items to accept additional kwargs for improved…
Pavan-Microsoft Apr 3, 2026
79d5964
fix: update fallback behavior to check batch status instead of forcin…
Pavan-Microsoft Apr 6, 2026
27e2f53
Revert "fix: update fallback behavior to check batch status instead o…
Pavan-Microsoft Apr 6, 2026
4b70934
Merge pull request #392 from microsoft/psl-pk-cosavmfix
Roopan-Microsoft Apr 6, 2026
5e765a8
fix: add dependency on aiProject for cognitive service deployments
Pavan-Microsoft Apr 7, 2026
6c81d2b
Merge remote-tracking branch 'origin/dev' into psl-pk-cosavmfix
Pavan-Microsoft Apr 7, 2026
99f379d
fix: update bicep version and template hashes in main.json
Pavan-Microsoft Apr 7, 2026
1d4a220
Merge pull request #395 from microsoft/psl-pk-cosavmfix
Roopan-Microsoft Apr 7, 2026
fc8de18
fix: enhance batch handling in CosmosDBClient and add tests for confl…
Pavan-Microsoft Apr 7, 2026
75e0523
Merge pull request #399 from microsoft/psl-pk-cosavmfix
Roopan-Microsoft Apr 8, 2026
74d876d
Merge pull request #388 from microsoft/psl-weeklyschedule-codmod
Prajwal-Microsoft Apr 8, 2026
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
64 changes: 35 additions & 29 deletions infra/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "3093757051086668797"
"version": "0.42.1.51946",
"templateHash": "7222423000870488333"
},
"name": "Modernize Your Code Solution Accelerator",
"description": "CSA CTO Gold Standard Solution Accelerator for Modernize Your Code. \r\n"
Expand Down Expand Up @@ -5052,8 +5052,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "8663094775498995429"
"version": "0.42.1.51946",
"templateHash": "3406526791248457038"
}
},
"definitions": {
Expand Down Expand Up @@ -12895,11 +12895,11 @@
},
"dependsOn": [
"applicationInsights",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').ods)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').agentSvc)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').monitor)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').oms)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').agentSvc)]",
"dataCollectionEndpoint",
"logAnalyticsWorkspace",
"virtualNetwork"
Expand Down Expand Up @@ -25611,8 +25611,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "17285204072656433491"
"version": "0.42.1.51946",
"templateHash": "16969185198334420434"
},
"name": "AI Services and Project Module",
"description": "This module creates an AI Services resource and an AI Foundry project within it. It supports private networking, OpenAI deployments, and role assignments."
Expand Down Expand Up @@ -26952,8 +26952,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "5121330425393020264"
"version": "0.42.1.51946",
"templateHash": "4140498216793917924"
}
},
"definitions": {
Expand Down Expand Up @@ -27910,7 +27910,10 @@
"raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]",
"versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]"
},
"sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]"
"sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]",
"dependsOn": [
"aiProject"
]
},
"cognitiveService_lock": {
"condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]",
Expand Down Expand Up @@ -28664,8 +28667,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "10989408486030617267"
"version": "0.42.1.51946",
"templateHash": "2422737205646151487"
}
},
"definitions": {
Expand Down Expand Up @@ -28818,8 +28821,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "7933643033523871028"
"version": "0.42.1.51946",
"templateHash": "11911242767938607365"
}
},
"definitions": {
Expand Down Expand Up @@ -29036,8 +29039,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "5121330425393020264"
"version": "0.42.1.51946",
"templateHash": "4140498216793917924"
}
},
"definitions": {
Expand Down Expand Up @@ -29994,7 +29997,10 @@
"raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]",
"versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]"
},
"sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]"
"sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]",
"dependsOn": [
"aiProject"
]
},
"cognitiveService_lock": {
"condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]",
Expand Down Expand Up @@ -30748,8 +30754,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "10989408486030617267"
"version": "0.42.1.51946",
"templateHash": "2422737205646151487"
}
},
"definitions": {
Expand Down Expand Up @@ -30902,8 +30908,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "7933643033523871028"
"version": "0.42.1.51946",
"templateHash": "11911242767938607365"
}
},
"definitions": {
Expand Down Expand Up @@ -31917,8 +31923,8 @@
"dependsOn": [
"aiServices",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]",
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]",
"virtualNetwork"
]
},
Expand Down Expand Up @@ -31974,8 +31980,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "3881415837167031634"
"version": "0.42.1.51946",
"templateHash": "522477461329004641"
}
},
"definitions": {
Expand Down Expand Up @@ -40219,8 +40225,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "15962472891869337617"
"version": "0.42.1.51946",
"templateHash": "15355322017409205910"
}
},
"definitions": {
Expand Down Expand Up @@ -44082,8 +44088,8 @@
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "17636459140972536078"
"version": "0.42.1.51946",
"templateHash": "4242598725709304634"
}
},
"definitions": {
Expand Down
3 changes: 3 additions & 0 deletions infra/modules/ai-foundry/dependencies.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/depl
size: sku.?size
family: sku.?family
}
dependsOn: [
aiProject
]
}
]

Expand Down
42 changes: 32 additions & 10 deletions src/backend/common/database/cosmosdb.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import asyncio
from datetime import datetime, timezone
from typing import Dict, List, Optional
from uuid import UUID, uuid4

from azure.cosmos.aio import CosmosClient
from azure.cosmos.aio._database import DatabaseProxy
from azure.cosmos.exceptions import (
CosmosResourceExistsError
CosmosResourceExistsError,
CosmosResourceNotFoundError,
)

from common.database.database_base import DatabaseBase
Expand Down Expand Up @@ -85,9 +87,26 @@ async def create_batch(self, user_id: str, batch_id: UUID) -> BatchRecord:
await self.batch_container.create_item(body=batch.dict())
return batch
except CosmosResourceExistsError:
self.logger.info(f"Batch with ID {batch_id} already exists")
batchexists = await self.get_batch(user_id, str(batch_id))
return batchexists
self.logger.info("Batch already exists, reading existing record", batch_id=str(batch_id))
# Retry read with backoff to handle replication lag after 409 conflict
for attempt in range(3):
try:
batchexists = await self.batch_container.read_item(
item=str(batch_id), partition_key=str(batch_id)
)
if batchexists.get("user_id") != user_id:
self.logger.error("Batch belongs to a different user", batch_id=str(batch_id))
raise CosmosResourceNotFoundError(message="Batch not found")
self.logger.info("Returning existing batch record", batch_id=str(batch_id))
return BatchRecord.fromdb(batchexists)
except CosmosResourceNotFoundError:
if attempt < 2:
Comment thread
Pavan-Microsoft marked this conversation as resolved.
Outdated
self.logger.info("Batch read returned 404 after conflict, retrying", batch_id=str(batch_id), attempt=attempt + 1)
await asyncio.sleep(0.5 * (attempt + 1))
else:
raise RuntimeError(
f"Batch {batch_id} already exists but could not be read after retries"
)

Comment thread
Pavan-Microsoft marked this conversation as resolved.
Outdated
except Exception as e:
self.logger.error("Failed to create batch", error=str(e))
Expand Down Expand Up @@ -158,7 +177,7 @@ async def get_batch(self, user_id: str, batch_id: str) -> Optional[Dict]:
]
batch = None
async for item in self.batch_container.query_items(
query=query, parameters=params
query=query, parameters=params, partition_key=batch_id
):
batch = item

Expand All @@ -173,7 +192,7 @@ async def get_file(self, file_id: str) -> Optional[Dict]:
params = [{"name": "@file_id", "value": file_id}]
file_entry = None
async for item in self.file_container.query_items(
query=query, parameters=params
query=query, parameters=params, partition_key=file_id
):
file_entry = item
return file_entry
Expand Down Expand Up @@ -209,7 +228,7 @@ async def get_batch_from_id(self, batch_id: str) -> Dict:

batch = None # Store the batch
async for item in self.batch_container.query_items(
query=query, parameters=params
query=query, parameters=params, partition_key=batch_id
):
batch = item # Assign the batch to the variable

Expand Down Expand Up @@ -335,11 +354,14 @@ async def add_file_log(
raise

async def update_batch_entry(
self, batch_id: str, user_id: str, status: ProcessStatus, file_count: int
self, batch_id: str, user_id: str, status: ProcessStatus, file_count: int,
existing_batch: Optional[Dict] = None
):
"""Update batch status."""
"""Update batch status. If existing_batch is provided, skip the re-fetch."""
try:
batch = await self.get_batch(user_id, batch_id)
batch = existing_batch
if batch is None:
batch = await self.get_batch(user_id, batch_id)
if not batch:
raise ValueError("Batch not found")

Expand Down
37 changes: 26 additions & 11 deletions src/backend/common/database/database_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,40 @@

class DatabaseFactory:
_instance: Optional[DatabaseBase] = None
_lock: Optional[asyncio.Lock] = None
_logger = AppLogger("DatabaseFactory")

@staticmethod
def _get_lock() -> asyncio.Lock:
if DatabaseFactory._lock is None:
DatabaseFactory._lock = asyncio.Lock()
return DatabaseFactory._lock

@staticmethod
async def get_database():
if DatabaseFactory._instance is not None:
return DatabaseFactory._instance

async with DatabaseFactory._get_lock():
# Double-check after acquiring the lock
if DatabaseFactory._instance is not None:
return DatabaseFactory._instance

config = Config() # Create an instance of Config
config = Config() # Create an instance of Config

cosmos_db_client = CosmosDBClient(
endpoint=config.cosmosdb_endpoint,
credential=config.get_azure_credentials(),
database_name=config.cosmosdb_database,
batch_container=config.cosmosdb_batch_container,
file_container=config.cosmosdb_file_container,
log_container=config.cosmosdb_log_container,
)
cosmos_db_client = CosmosDBClient(
endpoint=config.cosmosdb_endpoint,
credential=config.get_azure_credentials(),
database_name=config.cosmosdb_database,
batch_container=config.cosmosdb_batch_container,
file_container=config.cosmosdb_file_container,
log_container=config.cosmosdb_log_container,
)

await cosmos_db_client.initialize_cosmos()
await cosmos_db_client.initialize_cosmos()

return cosmos_db_client
DatabaseFactory._instance = cosmos_db_client
return cosmos_db_client


# Local testing of config and code
Expand Down
9 changes: 6 additions & 3 deletions src/backend/common/services/batch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,9 @@ async def upload_file_to_batch(self, batch_id: str, user_id: str, file: UploadFi
self.logger.info("File uploaded to blob storage", filename=file.filename, batch_id=batch_id)

# Create file entry
await self.database.add_file(batch_id, file_id, file.filename, blob_path)
file_record = await self.database.get_file(file_id)
file_record_obj = await self.database.add_file(batch_id, file_id, file.filename, blob_path)
file_record_dict = getattr(file_record_obj, "dict", None)
file_record = file_record_dict() if callable(file_record_dict) else file_record_obj
Comment thread
Pavan-Microsoft marked this conversation as resolved.
Outdated

await self.database.add_file_log(
UUID(file_id),
Expand All @@ -308,6 +309,7 @@ async def upload_file_to_batch(self, batch_id: str, user_id: str, file: UploadFi
user_id,
ProcessStatus.READY_TO_PROCESS,
batch["file_count"],
existing_batch=batch,
)
# Return response
return {"batch": batch, "file": file_record}
Expand All @@ -318,7 +320,8 @@ async def upload_file_to_batch(self, batch_id: str, user_id: str, file: UploadFi
batch.file_count = len(files)
batch.updated_at = datetime.utcnow().isoformat()
await self.database.update_batch_entry(
batch_id, user_id, ProcessStatus.READY_TO_PROCESS, batch.file_count
batch_id, user_id, ProcessStatus.READY_TO_PROCESS, batch.file_count,
existing_batch=batch.dict(),
)
# Return response
return {"batch": batch, "file": file_record}
Expand Down
Loading
Loading