Skip to content

Commit 2481513

Browse files
max-zillalmarini
andauthored
Generic error catching in DB (#152)
* error class * clean up arg defaults * formatting * fix documentation * add await to log_error * update ES connects to async * Fix circular import Co-authored-by: Luigi Marini <[email protected]>
1 parent e713850 commit 2481513

File tree

10 files changed

+281
-180
lines changed

10 files changed

+281
-180
lines changed

backend/Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ rocrate = "*"
2727
[dev-packages]
2828
requests = "*"
2929
pytest = "*"
30+
pytest-asyncio = "*"
3031
black = "*"
3132
faker = "*"
3233

backend/Pipfile.lock

Lines changed: 181 additions & 166 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/app/database/errors.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
import traceback
3+
import motor.motor_asyncio
4+
from typing import Optional, Generator
5+
from fastapi import Depends
6+
from pymongo import MongoClient
7+
8+
from app.config import settings
9+
from app.mongo import crete_mongo_indexes
10+
from app.models.errors import Error
11+
from app.models.metadata import MongoDBRef
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
async def _get_db() -> Generator:
17+
"""Duplicate of app.dependencies.get_db(), but importing that causes circular import."""
18+
mongo_client = motor.motor_asyncio.AsyncIOMotorClient(settings.MONGODB_URL)
19+
db = mongo_client[settings.MONGO_DATABASE]
20+
await crete_mongo_indexes(db)
21+
yield db
22+
23+
24+
async def log_error(
25+
exception: Exception,
26+
resource: Optional[MongoDBRef] = None,
27+
user: Optional[str] = None,
28+
):
29+
"""Insert new Error into the database.
30+
31+
Arguments:
32+
exception -- instance of an Exception or subclass
33+
resource -- if error relates to a specific resource, you can include it
34+
user --- if error relates to actions performed by a user, you can include them
35+
"""
36+
db = _get_db()
37+
message = str(exception)
38+
trace = traceback.format_exc(exception, limit=4)
39+
40+
logger.error(message)
41+
error_log = Error(message=message, trace=trace, resource=resource, user=user)
42+
await db["errors"].insert_one(error_log.to_mongo())

backend/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
@app.on_event("startup")
146146
async def startup_elasticsearch():
147147
# create elasticsearch indices
148-
es = connect_elasticsearch()
148+
es = await connect_elasticsearch()
149149
create_index(
150150
es, "file", settings.elasticsearch_setting, indexSettings.file_mappings
151151
)

backend/app/models/errors.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
from pydantic import Field
4+
5+
from app.models.mongomodel import MongoModel
6+
from app.models.metadata import MongoDBRef
7+
8+
9+
class ServiceUnreachable(Exception):
10+
"""Raised when Clowder can't connect to an outside service e.g. MongoDB, Elasticsearch."""
11+
12+
def __init__(self, service, *args):
13+
super().__init__(args)
14+
self.service = service
15+
16+
def __str__(self):
17+
return f"{self.service} could not be reached."
18+
19+
20+
class Error(MongoModel):
21+
message: str # Shorthand message of the error
22+
trace: str # Full stack trace of the error
23+
resource: Optional[MongoDBRef] = None
24+
user_id: Optional[str] = None
25+
timestamp: datetime = Field(default_factory=datetime.utcnow)

backend/app/routers/datasets.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from app import dependencies
3232
from app import keycloak_auth
3333
from app.search.connect import (
34+
connect_elasticsearch,
3435
insert_record,
3536
delete_document_by_id,
3637
update_record,
@@ -191,6 +192,7 @@ async def save_dataset(
191192
db: MongoClient = Depends(dependencies.get_db),
192193
es=Depends(dependencies.get_elasticsearchclient),
193194
):
195+
es = await connect_elasticsearch()
194196

195197
# Check all connection and abort if any one of them is not available
196198
if db is None or es is None:
@@ -304,6 +306,7 @@ async def edit_dataset(
304306
user_id=Depends(get_user),
305307
es=Depends(dependencies.get_elasticsearchclient),
306308
):
309+
es = await connect_elasticsearch()
307310

308311
# Check all connection and abort if any one of them is not available
309312
if db is None or es is None:
@@ -347,6 +350,7 @@ async def patch_dataset(
347350
db: MongoClient = Depends(dependencies.get_db),
348351
es=Depends(dependencies.get_elasticsearchclient),
349352
):
353+
es = await connect_elasticsearch()
350354

351355
# Check all connection and abort if any one of them is not available
352356
if db is None or es is None:
@@ -388,6 +392,7 @@ async def delete_dataset(
388392
fs: Minio = Depends(dependencies.get_fs),
389393
es=Depends(dependencies.get_elasticsearchclient),
390394
):
395+
es = await connect_elasticsearch()
391396

392397
# Check all connection and abort if any one of them is not available
393398
if db is None or fs is None or es is None:

backend/app/routers/elasticsearch.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,26 @@
77

88
@router.put("/search", response_model=str)
99
async def search(index_name: str, query: str):
10-
es = connect_elasticsearch()
10+
es = await connect_elasticsearch()
1111
return search_index(es, index_name, query)
1212

1313

1414
@router.post("/file/_msearch")
1515
async def search_file(request: Request):
16-
es = connect_elasticsearch()
16+
es = await connect_elasticsearch()
1717
query = await request.body()
1818
return search_index(es, "file", query)
1919

2020

2121
@router.post("/dataset/_msearch")
2222
async def search_dataset(request: Request):
23-
es = connect_elasticsearch()
23+
es = await connect_elasticsearch()
2424
query = await request.body()
2525
return search_index(es, "dataset", query)
2626

2727

2828
@router.post("/file,dataset/_msearch")
2929
async def search_file_and_dataset(request: Request):
30-
es = connect_elasticsearch()
30+
es = await connect_elasticsearch()
3131
query = await request.body()
3232
return search_index(es, ["file", "dataset"], query)

backend/app/routers/files.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from app import dependencies
2323
from app.config import settings
2424
from app.search.connect import (
25+
connect_elasticsearch,
2526
insert_record,
2627
delete_document_by_id,
2728
update_record,
@@ -55,6 +56,7 @@ async def add_file_entry(
5556
file_db: FileDB object controlling dataset and folder destination
5657
file: bytes to upload
5758
"""
59+
es = await connect_elasticsearch()
5860

5961
# Check all connection and abort if any one of them is not available
6062
if db is None or fs is None or es is None:
@@ -124,6 +126,8 @@ async def remove_file_entry(
124126
"""Remove FileDB object into MongoDB, Minio, and associated metadata and version information."""
125127
# TODO: Deleting individual versions will require updating version_id in mongo, or deleting entire document
126128

129+
es = await connect_elasticsearch()
130+
127131
# Check all connection and abort if any one of them is not available
128132
if db is None or fs is None or es is None:
129133
raise HTTPException(status_code=503, detail="Service not available")
@@ -146,6 +150,7 @@ async def update_file(
146150
file: UploadFile = File(...),
147151
es=Depends(dependencies.get_elasticsearchclient),
148152
):
153+
es = await connect_elasticsearch()
149154

150155
# Check all connection and abort if any one of them is not available
151156
if db is None or fs is None or es is None:

backend/app/search/connect.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,26 @@
77
from app.models.files import FileOut
88
from app.models.search import SearchCriteria
99
from app.models.feeds import SearchObject
10+
from app.models.errors import ServiceUnreachable
11+
from app.database.errors import log_error
1012

1113
logger = logging.getLogger(__name__)
1214
no_of_shards = settings.elasticsearch_no_of_shards
1315
no_of_replicas = settings.elasticsearch_no_of_replicas
1416

1517

16-
def connect_elasticsearch():
18+
async def connect_elasticsearch():
1719
"""To connect to elasticsearch server and return the elasticsearch client"""
1820
_es = None
1921
logger.info(settings.elasticsearch_url)
2022
_es = Elasticsearch(settings.elasticsearch_url)
21-
if _es.ping():
22-
logger.info("Successfully connected to Elasticsearch")
23-
else:
24-
logger.info("Can not connect to Elasticsearch")
23+
try:
24+
if _es.ping():
25+
logger.info("Successfully connected to Elasticsearch")
26+
else:
27+
raise ServiceUnreachable("Elasticsearch")
28+
except ServiceUnreachable as e:
29+
await log_error(e)
2530
return _es
2631

2732

backend/app/tests/test_elastic_search.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import time
3+
import pytest
34
from datetime import datetime
45

56
from bson import ObjectId
@@ -57,8 +58,9 @@
5758
}
5859

5960

60-
def test_files():
61-
es = connect_elasticsearch()
61+
@pytest.mark.asyncio
62+
async def test_files():
63+
es = await connect_elasticsearch()
6264
if es is not None:
6365
create_index(
6466
es,
@@ -95,8 +97,9 @@ def test_files():
9597
delete_index(es, dummy_file_index_name)
9698

9799

98-
def test_datasets():
99-
es = connect_elasticsearch()
100+
@pytest.mark.asyncio
101+
async def test_datasets():
102+
es = await connect_elasticsearch()
100103
if es is not None:
101104
create_index(
102105
es,

0 commit comments

Comments
 (0)