44interactions from the Encord platform.
55"""
66
7+ import asyncio
8+ import json
79import typing
810from http import HTTPStatus
11+ from uuid import UUID
912
1013from encord .exceptions import AuthorisationError
14+ from pydantic import ValidationError
15+
16+ from encord_agents .core .data_model import FrameData
1117
1218try :
1319 from fastapi import FastAPI , Request
@@ -102,6 +108,45 @@ async def _authorization_error_exception_handler(request: Request, exc: Authoris
102108 )
103109
104110
111+ class FieldPairLockMiddleware (BaseHTTPMiddleware ):
112+ def __init__ (
113+ self ,
114+ app : ASGIApp ,
115+ ):
116+ super ().__init__ (app )
117+ self .field_locks : dict [tuple [UUID , UUID ], asyncio .Lock ] = {}
118+ self .locks_lock = asyncio .Lock ()
119+
120+ async def get_lock (self , frame_data : FrameData ) -> asyncio .Lock :
121+ lock_key = (frame_data .project_hash , frame_data .data_hash )
122+ async with self .locks_lock :
123+ if lock_key not in self .field_locks :
124+ self .field_locks [lock_key ] = asyncio .Lock ()
125+ return self .field_locks [lock_key ]
126+
127+ async def dispatch (self , request : Request , call_next : RequestResponseEndpoint ) -> Response :
128+ if request .method != "POST" :
129+ return await call_next (request )
130+ try :
131+ body = await request .body ()
132+ try :
133+ frame_data = FrameData .model_validate_json (body )
134+ except ValidationError :
135+ # Hope that route doesn't use FrameData
136+ return await call_next (request )
137+ lock = await self .get_lock (frame_data )
138+ async with lock :
139+ # Create a new request with the same body since we've already consumed it
140+ request ._body = body
141+ return await call_next (request )
142+ except Exception as e :
143+ return Response (
144+ content = json .dumps ({"detail" : f"Error in middleware: { str (e )} " }),
145+ status_code = 500 ,
146+ media_type = "application/json" ,
147+ )
148+
149+
105150def get_encord_app (* , custom_cors_regex : str | None = None ) -> FastAPI :
106151 """
107152 Get a FastAPI app with the Encord middleware.
@@ -114,10 +159,12 @@ def get_encord_app(*, custom_cors_regex: str | None = None) -> FastAPI:
114159 FastAPI: A FastAPI app with the Encord middleware.
115160 """
116161 app = FastAPI ()
162+
117163 app .add_middleware (
118164 EncordCORSMiddleware ,
119165 allow_origin_regex = custom_cors_regex or ENCORD_DOMAIN_REGEX ,
120166 )
121167 app .add_middleware (EncordTestHeaderMiddleware )
168+ app .add_middleware (FieldPairLockMiddleware )
122169 app .exception_handlers [AuthorisationError ] = _authorization_error_exception_handler
123170 return app
0 commit comments