Skip to content

Commit 56ca532

Browse files
Merge remote-tracking branch 'origin/dev' into psl-bicep-param
2 parents 760c6fe + 64111e2 commit 56ca532

11 files changed

Lines changed: 307 additions & 94 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: AZD Template Validation
2+
3+
on:
4+
schedule:
5+
- cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC)
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
id-token: write
11+
pull-requests: write
12+
13+
jobs:
14+
template_validation:
15+
runs-on: ubuntu-latest
16+
name: azd template validation
17+
environment: production
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v6
21+
22+
- name: Set timestamp
23+
run: echo "HHMM=$(date -u +'%H%M')" >> $GITHUB_ENV
24+
25+
- name: Validate Azure Template
26+
id: validation
27+
uses: microsoft/template-validation-action@v0.4.3
28+
with:
29+
validateAzd: ${{ vars.TEMPLATE_VALIDATE_AZD }}
30+
validateTests: ${{ vars.TEMPLATE_VALIDATE_TESTS }}
31+
useDevContainer: ${{ vars.TEMPLATE_USE_DEV_CONTAINER }}
32+
33+
env:
34+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
35+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
36+
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
37+
AZURE_ENV_NAME: azd-${{ secrets.AZURE_ENV_NAME }}-${{ env.HHMM }}
38+
AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }}
39+
AZURE_ENV_AI_SERVICE_LOCATION: ${{ secrets.AZURE_AI_DEPLOYMENT_LOCATION || secrets.AZURE_LOCATION }}
40+
AZURE_ENV_MODEL_CAPACITY: 1
41+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42+
AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
43+
44+
- name: Print result
45+
shell: bash
46+
run: cat "${{ steps.validation.outputs.resultFile }}"

.github/workflows/azure-dev.yml

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,65 @@
1-
name: Azure Template Validation
2-
on:
3-
workflow_dispatch:
4-
5-
permissions:
6-
contents: read
7-
id-token: write
8-
pull-requests: write
9-
jobs:
10-
template_validation_job:
11-
runs-on: ubuntu-latest
1+
name: Azure Dev Deploy
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
id-token: write
9+
10+
jobs:
11+
deploy:
12+
runs-on: ubuntu-latest
1213
environment: production
13-
name: Template validation
14-
steps:
15-
# Step 1: Checkout the code from your repository
16-
- name: Checkout code
17-
uses: actions/checkout@v6
18-
# Step 2: Validate the Azure template using microsoft/template-validation-action
19-
- name: Validate Azure Template
20-
uses: microsoft/template-validation-action@v0.4.3
21-
with:
22-
validateAzd: true
23-
useDevContainer: false
24-
id: validation
25-
env:
26-
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
27-
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
28-
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
29-
AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}
30-
AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }}
31-
AZURE_AI_DEPLOYMENT_LOCATION : ${{ secrets.AZURE_AI_DEPLOYMENT_LOCATION }}
32-
AZURE_ENV_GPT_MODEL_CAPACITY : 1
33-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34-
AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
35-
# Step 3: Print the result of the validation
36-
- name: Print result
37-
run: cat ${{ steps.validation.outputs.resultFile }}
14+
env:
15+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
16+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
17+
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
18+
AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}
19+
AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }}
20+
AZURE_AI_DEPLOYMENT_LOCATION: ${{ secrets.AZURE_AI_DEPLOYMENT_LOCATION || secrets.AZURE_LOCATION }}
21+
AZURE_ENV_GPT_MODEL_CAPACITY: 1
22+
AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v6
26+
27+
- name: Set timestamp and env name
28+
run: |
29+
HHMM=$(date -u +'%H%M')
30+
echo "AZURE_ENV_NAME=azd-${{ vars.AZURE_ENV_NAME }}-${HHMM}" >> $GITHUB_ENV
31+
32+
- name: Install azd
33+
uses: Azure/setup-azd@v2
34+
35+
- name: Login to Azure
36+
uses: azure/login@v2
37+
with:
38+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
39+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
40+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
41+
42+
- name: Login to AZD
43+
shell: bash
44+
run: |
45+
azd auth login \
46+
--client-id "$AZURE_CLIENT_ID" \
47+
--federated-credential-provider "github" \
48+
--tenant-id "$AZURE_TENANT_ID"
49+
50+
- name: Provision and deploy
51+
shell: bash
52+
run: |
53+
set -e
54+
55+
if ! azd env select "$AZURE_ENV_NAME"; then
56+
azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt
57+
fi
58+
59+
azd config set defaults.subscription "$AZURE_SUBSCRIPTION_ID"
60+
azd env set AZURE_SUBSCRIPTION_ID "$AZURE_SUBSCRIPTION_ID"
61+
azd env set AZURE_LOCATION "$AZURE_LOCATION"
62+
azd env set AZURE_ENV_AI_SERVICE_LOCATION "${AZURE_AI_DEPLOYMENT_LOCATION:-$AZURE_LOCATION}"
63+
azd env set AZURE_ENV_GPT_MODEL_CAPACITY "$AZURE_ENV_GPT_MODEL_CAPACITY"
64+
65+
azd up --no-prompt

azure.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ metadata:
44

55
requiredVersions:
66
azd: '>= 1.18.0 != 1.23.9'
7+
bicep: '>= 0.33.0'
78

89
parameters:
910
AzureAiServiceLocation:

infra/main.json

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"_generator": {
77
"name": "bicep",
88
"version": "0.40.2.10011",
9-
"templateHash": "12712615740947825780"
9+
"templateHash": "13589960712112840698"
1010
},
1111
"name": "Modernize Your Code Solution Accelerator",
1212
"description": "CSA CTO Gold Standard Solution Accelerator for Modernize Your Code. \r\n"
@@ -12895,11 +12895,11 @@
1289512895
},
1289612896
"dependsOn": [
1289712897
"applicationInsights",
12898+
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').monitor)]",
12899+
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').oms)]",
1289812900
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]",
1289912901
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').ods)]",
1290012902
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').agentSvc)]",
12901-
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').monitor)]",
12902-
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').oms)]",
1290312903
"dataCollectionEndpoint",
1290412904
"logAnalyticsWorkspace",
1290512905
"virtualNetwork"
@@ -25612,7 +25612,7 @@
2561225612
"_generator": {
2561325613
"name": "bicep",
2561425614
"version": "0.40.2.10011",
25615-
"templateHash": "15268521456197740686"
25615+
"templateHash": "665208465907096971"
2561625616
},
2561725617
"name": "AI Services and Project Module",
2561825618
"description": "This module creates an AI Services resource and an AI Foundry project within it. It supports private networking, OpenAI deployments, and role assignments."
@@ -26953,7 +26953,7 @@
2695326953
"_generator": {
2695426954
"name": "bicep",
2695526955
"version": "0.40.2.10011",
26956-
"templateHash": "6664885964502172426"
26956+
"templateHash": "7604365129625921085"
2695726957
}
2695826958
},
2695926959
"definitions": {
@@ -27910,7 +27910,10 @@
2791027910
"raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]",
2791127911
"versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]"
2791227912
},
27913-
"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')))]"
27913+
"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')))]",
27914+
"dependsOn": [
27915+
"aiProject"
27916+
]
2791427917
},
2791527918
"cognitiveService_lock": {
2791627919
"condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]",
@@ -29037,7 +29040,7 @@
2903729040
"_generator": {
2903829041
"name": "bicep",
2903929042
"version": "0.40.2.10011",
29040-
"templateHash": "6664885964502172426"
29043+
"templateHash": "7604365129625921085"
2904129044
}
2904229045
},
2904329046
"definitions": {
@@ -29994,7 +29997,10 @@
2999429997
"raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]",
2999529998
"versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]"
2999629999
},
29997-
"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')))]"
30000+
"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')))]",
30001+
"dependsOn": [
30002+
"aiProject"
30003+
]
2999830004
},
2999930005
"cognitiveService_lock": {
3000030006
"condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]",

infra/modules/ai-foundry/dependencies.bicep

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/depl
208208
size: sku.?size
209209
family: sku.?family
210210
}
211+
dependsOn: [
212+
aiProject
213+
]
211214
}
212215
]
213216

src/backend/common/database/cosmosdb.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import asyncio
12
from datetime import datetime, timezone
23
from typing import Dict, List, Optional
34
from uuid import UUID, uuid4
45

56
from azure.cosmos.aio import CosmosClient
67
from azure.cosmos.aio._database import DatabaseProxy
78
from azure.cosmos.exceptions import (
8-
CosmosResourceExistsError
9+
CosmosResourceExistsError,
10+
CosmosResourceNotFoundError,
911
)
1012

1113
from common.database.database_base import DatabaseBase
@@ -85,9 +87,28 @@ async def create_batch(self, user_id: str, batch_id: UUID) -> BatchRecord:
8587
await self.batch_container.create_item(body=batch.dict())
8688
return batch
8789
except CosmosResourceExistsError:
88-
self.logger.info(f"Batch with ID {batch_id} already exists")
89-
batchexists = await self.get_batch(user_id, str(batch_id))
90-
return batchexists
90+
self.logger.info("Batch already exists, reading existing record", batch_id=str(batch_id))
91+
# Retry read with backoff to handle replication lag after 409 conflict
92+
for attempt in range(3):
93+
try:
94+
batchexists = await self.batch_container.read_item(
95+
item=str(batch_id), partition_key=str(batch_id)
96+
)
97+
except CosmosResourceNotFoundError:
98+
if attempt < 2:
99+
self.logger.info("Batch read returned 404 after conflict, retrying", batch_id=str(batch_id), attempt=attempt + 1)
100+
await asyncio.sleep(0.5 * (attempt + 1))
101+
continue
102+
raise RuntimeError(
103+
f"Batch {batch_id} already exists but could not be read after retries"
104+
)
105+
106+
if batchexists.get("user_id") != user_id:
107+
self.logger.error("Batch belongs to a different user", batch_id=str(batch_id))
108+
raise PermissionError("Batch not found")
109+
110+
self.logger.info("Returning existing batch record", batch_id=str(batch_id))
111+
return BatchRecord.fromdb(batchexists)
91112

92113
except Exception as e:
93114
self.logger.error("Failed to create batch", error=str(e))
@@ -158,7 +179,7 @@ async def get_batch(self, user_id: str, batch_id: str) -> Optional[Dict]:
158179
]
159180
batch = None
160181
async for item in self.batch_container.query_items(
161-
query=query, parameters=params
182+
query=query, parameters=params, partition_key=batch_id
162183
):
163184
batch = item
164185

@@ -173,7 +194,7 @@ async def get_file(self, file_id: str) -> Optional[Dict]:
173194
params = [{"name": "@file_id", "value": file_id}]
174195
file_entry = None
175196
async for item in self.file_container.query_items(
176-
query=query, parameters=params
197+
query=query, parameters=params, partition_key=file_id
177198
):
178199
file_entry = item
179200
return file_entry
@@ -209,7 +230,7 @@ async def get_batch_from_id(self, batch_id: str) -> Dict:
209230

210231
batch = None # Store the batch
211232
async for item in self.batch_container.query_items(
212-
query=query, parameters=params
233+
query=query, parameters=params, partition_key=batch_id
213234
):
214235
batch = item # Assign the batch to the variable
215236

@@ -335,11 +356,14 @@ async def add_file_log(
335356
raise
336357

337358
async def update_batch_entry(
338-
self, batch_id: str, user_id: str, status: ProcessStatus, file_count: int
359+
self, batch_id: str, user_id: str, status: ProcessStatus, file_count: int,
360+
existing_batch: Optional[Dict] = None
339361
):
340-
"""Update batch status."""
362+
"""Update batch status. If existing_batch is provided, skip the re-fetch."""
341363
try:
342-
batch = await self.get_batch(user_id, batch_id)
364+
batch = existing_batch
365+
if batch is None:
366+
batch = await self.get_batch(user_id, batch_id)
343367
if not batch:
344368
raise ValueError("Batch not found")
345369

src/backend/common/database/database_factory.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,40 @@
99

1010
class DatabaseFactory:
1111
_instance: Optional[DatabaseBase] = None
12+
_lock: Optional[asyncio.Lock] = None
1213
_logger = AppLogger("DatabaseFactory")
1314

15+
@staticmethod
16+
def _get_lock() -> asyncio.Lock:
17+
if DatabaseFactory._lock is None:
18+
DatabaseFactory._lock = asyncio.Lock()
19+
return DatabaseFactory._lock
20+
1421
@staticmethod
1522
async def get_database():
23+
if DatabaseFactory._instance is not None:
24+
return DatabaseFactory._instance
25+
26+
async with DatabaseFactory._get_lock():
27+
# Double-check after acquiring the lock
28+
if DatabaseFactory._instance is not None:
29+
return DatabaseFactory._instance
1630

17-
config = Config() # Create an instance of Config
31+
config = Config() # Create an instance of Config
1832

19-
cosmos_db_client = CosmosDBClient(
20-
endpoint=config.cosmosdb_endpoint,
21-
credential=config.get_azure_credentials(),
22-
database_name=config.cosmosdb_database,
23-
batch_container=config.cosmosdb_batch_container,
24-
file_container=config.cosmosdb_file_container,
25-
log_container=config.cosmosdb_log_container,
26-
)
33+
cosmos_db_client = CosmosDBClient(
34+
endpoint=config.cosmosdb_endpoint,
35+
credential=config.get_azure_credentials(),
36+
database_name=config.cosmosdb_database,
37+
batch_container=config.cosmosdb_batch_container,
38+
file_container=config.cosmosdb_file_container,
39+
log_container=config.cosmosdb_log_container,
40+
)
2741

28-
await cosmos_db_client.initialize_cosmos()
42+
await cosmos_db_client.initialize_cosmos()
2943

30-
return cosmos_db_client
44+
DatabaseFactory._instance = cosmos_db_client
45+
return cosmos_db_client
3146

3247

3348
# Local testing of config and code

0 commit comments

Comments
 (0)