background.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. # pylint: disable=missing-module-docstring, cyclic-import
  3. import json
  4. import time
  5. import threading
  6. import os
  7. import signal
  8. from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
  9. import redis.exceptions
  10. from searx import logger, settings, sxng_debug
  11. from searx.redisdb import client as get_redis_client
  12. from searx.exceptions import SearxSettingsException
  13. from searx.search.processors import PROCESSORS
  14. from searx.search.checker import Checker
  15. from searx.search.checker.scheduler import scheduler_function
  16. REDIS_RESULT_KEY = 'SearXNG_checker_result'
  17. REDIS_LOCK_KEY = 'SearXNG_checker_lock'
  18. CheckerResult = Union['CheckerOk', 'CheckerErr', 'CheckerOther']
  19. class CheckerOk(TypedDict):
  20. """Checking the engines succeeded"""
  21. status: Literal['ok']
  22. engines: Dict[str, 'EngineResult']
  23. timestamp: int
  24. class CheckerErr(TypedDict):
  25. """Checking the engines failed"""
  26. status: Literal['error']
  27. timestamp: int
  28. class CheckerOther(TypedDict):
  29. """The status is unknown or disabled"""
  30. status: Literal['unknown', 'disabled']
  31. EngineResult = Union['EngineOk', 'EngineErr']
  32. class EngineOk(TypedDict):
  33. """Checking the engine succeeded"""
  34. success: Literal[True]
  35. class EngineErr(TypedDict):
  36. """Checking the engine failed"""
  37. success: Literal[False]
  38. errors: Dict[str, List[str]]
  39. def _get_interval(every: Any, error_msg: str) -> Tuple[int, int]:
  40. if isinstance(every, int):
  41. return (every, every)
  42. if (
  43. not isinstance(every, (tuple, list))
  44. or len(every) != 2 # type: ignore
  45. or not isinstance(every[0], int)
  46. or not isinstance(every[1], int)
  47. ):
  48. raise SearxSettingsException(error_msg, None)
  49. return (every[0], every[1])
  50. def get_result() -> CheckerResult:
  51. client = get_redis_client()
  52. if client is None:
  53. # without Redis, the checker is disabled
  54. return {'status': 'disabled'}
  55. serialized_result: Optional[bytes] = client.get(REDIS_RESULT_KEY)
  56. if serialized_result is None:
  57. # the Redis key does not exist
  58. return {'status': 'unknown'}
  59. return json.loads(serialized_result)
  60. def _set_result(result: CheckerResult):
  61. client = get_redis_client()
  62. if client is None:
  63. # without Redis, the function does nothing
  64. return
  65. client.set(REDIS_RESULT_KEY, json.dumps(result))
  66. def _timestamp():
  67. return int(time.time() / 3600) * 3600
  68. def run():
  69. try:
  70. # use a Redis lock to make sure there is no checker running at the same time
  71. # (this should not happen, this is a safety measure)
  72. with get_redis_client().lock(REDIS_LOCK_KEY, blocking_timeout=60, timeout=3600):
  73. logger.info('Starting checker')
  74. result: CheckerOk = {'status': 'ok', 'engines': {}, 'timestamp': _timestamp()}
  75. for name, processor in PROCESSORS.items():
  76. logger.debug('Checking %s engine', name)
  77. checker = Checker(processor)
  78. checker.run()
  79. if checker.test_results.successful:
  80. result['engines'][name] = {'success': True}
  81. else:
  82. result['engines'][name] = {'success': False, 'errors': checker.test_results.errors}
  83. _set_result(result)
  84. logger.info('Check done')
  85. except redis.exceptions.LockError:
  86. _set_result({'status': 'error', 'timestamp': _timestamp()})
  87. logger.exception('Error while running the checker')
  88. except Exception: # pylint: disable=broad-except
  89. _set_result({'status': 'error', 'timestamp': _timestamp()})
  90. logger.exception('Error while running the checker')
  91. def _signal_handler(_signum: int, _frame: Any):
  92. t = threading.Thread(target=run)
  93. t.daemon = True
  94. t.start()
  95. def initialize():
  96. if hasattr(signal, 'SIGUSR1'):
  97. # Windows doesn't support SIGUSR1
  98. logger.info('Send SIGUSR1 signal to pid %i to start the checker', os.getpid())
  99. signal.signal(signal.SIGUSR1, _signal_handler)
  100. # special case when debug is activate
  101. if sxng_debug and settings['checker']['off_when_debug']:
  102. logger.info('debug mode: checker is disabled')
  103. return
  104. # check value of checker.scheduling.every now
  105. scheduling = settings['checker']['scheduling']
  106. if scheduling is None or not scheduling:
  107. logger.info('Checker scheduler is disabled')
  108. return
  109. # make sure there is a Redis connection
  110. if get_redis_client() is None:
  111. logger.error('The checker requires Redis')
  112. return
  113. # start the background scheduler
  114. every_range = _get_interval(scheduling.get('every', (300, 1800)), 'checker.scheduling.every is not a int or list')
  115. start_after_range = _get_interval(
  116. scheduling.get('start_after', (300, 1800)), 'checker.scheduling.start_after is not a int or list'
  117. )
  118. t = threading.Thread(
  119. target=scheduler_function,
  120. args=(start_after_range[0], start_after_range[1], every_range[0], every_range[1], run),
  121. name='checker_scheduler',
  122. )
  123. t.daemon = True
  124. t.start()