11from contextlib import asynccontextmanager
22from pathlib import Path
33from typing import Optional
4+ import logging
45
56from fastapi import FastAPI , Form , Request , status
6- from fastapi .responses import HTMLResponse , PlainTextResponse , RedirectResponse
7+ from fastapi .responses import HTMLResponse , PlainTextResponse , RedirectResponse , JSONResponse
78from fastapi .staticfiles import StaticFiles
89from fastapi .templating import Jinja2Templates
910from starlette .middleware .sessions import SessionMiddleware
1011
1112from app .api .fast_api import app as api_app
12- from app .utils import data as db_data
13+ from app .utils import db
1314from app .utils .cache import (
1415 get_from_cache ,
1516 get_recent_from_cache ,
3334# -----------------------------
3435@asynccontextmanager
3536async def lifespan (app : FastAPI ):
36- db_data .connect_db ()
37+ logger = logging .getLogger (__name__ )
38+ logger .info ("Application startup: Connecting to database..." )
39+ db .connect_db ()
40+ db .start_health_check ()
41+ logger .info ("Application startup complete" )
42+
3743 yield
44+
45+ logger .info ("Application shutdown: Cleaning up..." )
46+ await db .stop_health_check ()
47+
48+ # Close MongoDB client gracefully
49+ try :
50+ if db .client is not None :
51+ db .client .close ()
52+ logger .info ("MongoDB client closed" )
53+ except Exception as e :
54+ logger .error (f"Error closing MongoDB client: { str (e )} " )
55+
56+ logger .info ("Application shutdown complete" )
3857
3958
4059app = FastAPI (title = "TinyURL" , lifespan = lifespan )
@@ -75,7 +94,7 @@ async def index(request: Request):
7594 generate_qr_with_logo (qr_data , str (qr_dir / qr_filename ))
7695 qr_image = f"/static/qr/{ qr_filename } "
7796
78- all_urls = db_data .get_recent_urls (MAX_RECENT_URLS ) or get_recent_from_cache (
97+ all_urls = db .get_recent_urls (MAX_RECENT_URLS ) or get_recent_from_cache (
7998 MAX_RECENT_URLS
8099 )
81100
@@ -91,7 +110,7 @@ async def index(request: Request):
91110 "original_url" : original_url ,
92111 "error" : error ,
93112 "info_message" : info_message ,
94- "db_available" : db_data .get_collection () is not None ,
113+ "db_available" : db .get_collection () is not None ,
95114 },
96115 )
97116
@@ -103,6 +122,8 @@ async def create_short_url(
103122 generate_qr : Optional [str ] = Form (None ),
104123 qr_type : str = Form ("short" ),
105124) -> RedirectResponse :
125+ logger = logging .getLogger (__name__ )
126+
106127 session = request .session
107128 qr_enabled = bool (generate_qr )
108129 original_url = sanitize_url (original_url )
@@ -116,25 +137,28 @@ async def create_short_url(
116137 short_code : Optional [str ] = get_short_from_cache (original_url )
117138
118139 if not short_code :
119- # 2. Try Database
120- existing = db_data . find_by_original_url ( original_url )
121- # Pull the value and check it in one go
122- db_code = existing .get ("short_code" ) if existing else None
123- if isinstance (db_code , str ):
124- short_code = db_code
125- set_cache_pair (short_code , original_url ) # Cache it for future requests
140+ # 2. Try Database if connected
141+ if db . is_connected ():
142+ existing = db . find_by_original_url ( original_url )
143+ db_code = existing .get ("short_code" ) if existing else None
144+ if isinstance (db_code , str ):
145+ short_code = db_code
146+ set_cache_pair (short_code , original_url )
126147
127148 # 3. Generate New if still None
128149 if not short_code :
129150 short_code = generate_code ()
130151 set_cache_pair (short_code , original_url )
131- db_data .insert_url (short_code , original_url )
152+
153+ # Only write to database if connected
154+ if db .is_connected ():
155+ db .insert_url (short_code , original_url )
156+ else :
157+ logger .warning (f"Database not connected, URL { short_code } created in cache only" )
158+ session ["info_message" ] = "URL created (database temporarily unavailable)"
132159
133160 # --- TYPE GUARD FOR MYPY ---
134- # At this point, short_code could still technically be Optional[str]
135- # if generate_code() wasn't strictly typed. We cast or assert.
136161 if not isinstance (short_code , str ):
137- # This acts as a final safety net for production
138162 session ["error" ] = "Internal server error: Code generation failed."
139163 return RedirectResponse ("/" , status_code = status .HTTP_303_SEE_OTHER )
140164
@@ -156,7 +180,7 @@ async def create_short_url(
156180
157181@app .get ("/recent" , response_class = HTMLResponse )
158182async def recent_urls (request : Request ):
159- recent_urls_list = db_data .get_recent_urls (
183+ recent_urls_list = db .get_recent_urls (
160184 MAX_RECENT_URLS
161185 ) or get_recent_from_cache (MAX_RECENT_URLS )
162186
@@ -183,7 +207,7 @@ async def recent_urls(request: Request):
183207
184208@app .post ("/delete/{short_code}" )
185209async def delete_url (request : Request , short_code : str ):
186- db_data .delete_by_short_code (short_code )
210+ db .delete_by_short_code (short_code )
187211
188212 cached = url_cache .pop (short_code , None )
189213 if cached :
@@ -194,18 +218,27 @@ async def delete_url(request: Request, short_code: str):
194218
195219@app .get ("/{short_code}" )
196220async def redirect_short (request : Request , short_code : str ):
197- doc = db_data . increment_visit ( short_code )
198-
221+ logger = logging . getLogger ( __name__ )
222+ # Try cache first
199223 cached_url = get_from_cache (short_code )
200224 if cached_url :
201225 return RedirectResponse (cached_url )
202-
226+
227+ # Check if database is connected
228+ if not db .is_connected ():
229+ logger .warning (f"Database not connected, cannot redirect { short_code } " )
230+ return PlainTextResponse (
231+ "Service temporarily unavailable. Please try again later." ,
232+ status_code = 503 ,
233+ headers = {"Retry-After" : "30" }
234+ )
235+
236+ # Try database
237+ doc = db .increment_visit (short_code )
203238 if doc :
204239 set_cache_pair (short_code , doc ["original_url" ])
205240 return RedirectResponse (doc ["original_url" ])
206- if db_data .get_collection () is None :
207- return PlainTextResponse ("Database is not connected." , status_code = 503 )
208-
241+
209242 return PlainTextResponse ("Invalid or expired short URL" , status_code = 404 )
210243
211244
@@ -214,6 +247,23 @@ async def coming_soon(request: Request):
214247 return templates .TemplateResponse ("coming-soon.html" , {"request" : request })
215248
216249
250+ @app .get ("/health" )
251+ async def health_check ():
252+ """Health check endpoint showing database and cache status."""
253+ state = db .get_connection_state ()
254+
255+ response_data = {
256+ "database" : state ,
257+ "cache" : {
258+ "enabled" : True ,
259+ "size" : len (url_cache ),
260+ }
261+ }
262+
263+ status_code = 200 if state ["connected" ] else 503
264+ return JSONResponse (content = response_data , status_code = status_code )
265+
266+
217267app .mount ("/api" , api_app )
218268
219269
0 commit comments