Coverage for app / backend / src / app.py: 0%

66 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-03 06:18 +0000

1import logging 

2import signal 

3import sys 

4from os import environ 

5from tempfile import TemporaryDirectory 

6from types import TracebackType 

7 

8# these two lines need to be at the top of the file before we span child processes 

9# this temp dir will be destroyed when prometheus_multiproc_dir is destroyed, aka at the end of the program. 

10# Also note that this should only be done in the main process. 

11if __name__ == "__main__": 

12 prometheus_multiproc_dir = TemporaryDirectory() 

13 environ["PROMETHEUS_MULTIPROC_DIR"] = prometheus_multiproc_dir.name 

14# ruff: noqa: E402 

15 

16import sentry_sdk 

17from sentry_sdk.integrations import excepthook 

18from sqlalchemy.sql import text 

19 

20from couchers.config import check_config, config 

21from couchers.db import apply_migrations, session_scope 

22from couchers.experimentation import setup_experimentation 

23from couchers.i18n.localize import get_main_i18next 

24from couchers.jobs.worker import start_jobs_scheduler, start_jobs_worker 

25from couchers.metrics import create_prometheus_server 

26from couchers.server import create_main_server, create_media_server 

27from couchers.tracing import setup_tracing 

28from dummy_data import add_dummy_data 

29 

30check_config(config) 

31 

32logging.basicConfig( 

33 format="[%(process)5d:%(thread)20d] %(asctime)s: %(name)s:%(lineno)d: %(message)s", level=logging.INFO 

34) 

35logger = logging.getLogger(__name__) 

36 

37 

38def log_unhandled_exception( 

39 exc_type: type[BaseException], 

40 exc_value: BaseException, 

41 exc_traceback: TracebackType | None, 

42) -> None: 

43 """Make sure that any unhandled exceptions will write to the logs""" 

44 if issubclass(exc_type, KeyboardInterrupt): 

45 # call the default excepthook saved at __excepthook__ 

46 sys.__excepthook__(exc_type, exc_value, exc_traceback) 

47 return 

48 logger.critical("Unhandled exception", exc_info=(exc_type, exc_value, exc_traceback)) 

49 

50 

51def common_init() -> None: 

52 sys.excepthook = log_unhandled_exception 

53 

54 if config["SENTRY_ENABLED"]: 

55 # Sends exception tracebacks to Sentry, a cloud service for collecting exceptions 

56 sentry_sdk.init( 

57 config["SENTRY_URL"], 

58 traces_sample_rate=0.0, 

59 environment=config["COOKIE_DOMAIN"], 

60 release=config["VERSION"], 

61 # The global excepthook picks up already handled gRPC errors (e.g. grpc.StatusCode.NOT_FOUND) 

62 disabled_integrations=[ 

63 excepthook.ExcepthookIntegration(), 

64 ], 

65 ) 

66 

67 logger.info("Checking DB connection") 

68 with session_scope() as session: 

69 res = session.execute(text("SELECT 42;")) 

70 if list(res) != [(42,)]: 

71 raise Exception("Failed to connect to DB") 

72 

73 

74def main() -> None: 

75 # used to export metrics 

76 create_prometheus_server(8000) 

77 

78 logger.info("Running DB migrations") 

79 

80 apply_migrations() 

81 

82 get_main_i18next() # Force eager loading of translations 

83 

84 if config["ADD_DUMMY_DATA"]: 

85 add_dummy_data() 

86 

87 logger.info("Starting") 

88 

89 if config["ROLE"] in ["scheduler", "all"]: 

90 start_jobs_scheduler() 

91 

92 if config["ROLE"] in ["worker", "all"]: 

93 for _ in range(config["BACKGROUND_WORKER_COUNT"]): 

94 start_jobs_worker() 

95 

96 setup_tracing() 

97 

98 # Initialize the experimentation framework for feature flags in the main process. 

99 # IMPORTANT: This MUST be called AFTER worker processes are spawned (above). 

100 # The underlying SDK uses internal threading that doesn't survive fork(). 

101 # Worker processes initialize their own instance in _run_forever(). 

102 setup_experimentation() 

103 

104 if config["ROLE"] in ["api", "all"]: 

105 server = create_main_server(port=1751) 

106 server.start() 

107 media_server = create_media_server(port=1753) 

108 media_server.start() 

109 logger.info("Serving on 1751 (secure) and 1753 (media)") 

110 

111 logger.info("App waiting for signal...") 

112 

113 signal.pause() 

114 

115 

116if __name__ == "__main__": 

117 common_init() 

118 main() 

119elif __name__ == "__mp_main__": # processes created via multiprocessing 

120 common_init()