tina4-python 3.13.76__tar.gz → 3.13.77__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {tina4_python-3.13.76 → tina4_python-3.13.77}/PKG-INFO +1 -1
  2. {tina4_python-3.13.76 → tina4_python-3.13.77}/pyproject.toml +4 -1
  3. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/server.py +60 -26
  4. {tina4_python-3.13.76 → tina4_python-3.13.77}/.gitignore +0 -0
  5. {tina4_python-3.13.76 → tina4_python-3.13.77}/README.md +0 -0
  6. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/CLAUDE.md +0 -0
  7. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/HtmlElement.py +0 -0
  8. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/Testing.py +0 -0
  9. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/__init__.py +0 -0
  10. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/context/__init__.py +0 -0
  17. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/context/chunker.py +0 -0
  18. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/crud/__init__.py +0 -0
  28. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/__init__.py +0 -0
  29. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/adapter.py +0 -0
  30. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/connection.py +0 -0
  31. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/firebird.py +0 -0
  32. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/mongodb.py +0 -0
  33. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/mssql.py +0 -0
  34. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/mysql.py +0 -0
  35. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/postgres.py +0 -0
  37. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/dev_admin/__init__.py +0 -0
  41. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/dev_admin/metrics.py +0 -0
  42. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/dev_admin/plan.py +0 -0
  43. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/dev_admin/project_index.py +0 -0
  44. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/docs.py +0 -0
  45. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/docstore/__init__.py +0 -0
  46. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/dotenv/__init__.py +0 -0
  47. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/env.py +0 -0
  48. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/frond/FROND.md +0 -0
  49. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/frond/__init__.py +0 -0
  50. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/frond/engine.py +0 -0
  51. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/auth/meta.json +0 -0
  52. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  53. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/database/meta.json +0 -0
  54. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  55. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/error-overlay/meta.json +0 -0
  56. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  57. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/orm/meta.json +0 -0
  58. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  59. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  60. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/queue/meta.json +0 -0
  61. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  62. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/rest-api/meta.json +0 -0
  63. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  64. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/templates/meta.json +0 -0
  65. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  66. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  67. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/graphql/__init__.py +0 -0
  68. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/i18n/__init__.py +0 -0
  69. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/mcp/__init__.py +0 -0
  70. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/mcp/protocol.py +0 -0
  71. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/mcp/tools.py +0 -0
  72. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/messenger/__init__.py +0 -0
  73. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/migration/__init__.py +0 -0
  74. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/migration/runner.py +0 -0
  75. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/orm/__init__.py +0 -0
  76. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/orm/fields.py +0 -0
  77. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/orm/model.py +0 -0
  78. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/__feedback/widget.js +0 -0
  79. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/css/tina4.css +0 -0
  80. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/css/tina4.min.css +0 -0
  81. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/favicon.ico +0 -0
  82. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/images/logo.svg +0 -0
  83. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  84. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/js/frond.js +0 -0
  85. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/js/frond.min.js +0 -0
  86. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  87. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  88. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/js/tina4.min.js +0 -0
  89. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/js/tina4js.min.js +0 -0
  90. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/swagger/index.html +0 -0
  91. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  92. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/query_builder/__init__.py +0 -0
  93. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue/__init__.py +0 -0
  94. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue/job.py +0 -0
  95. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue/kafka_backend.py +0 -0
  96. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue/lite_backend.py +0 -0
  97. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue/mongo_backend.py +0 -0
  98. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue_backends/__init__.py +0 -0
  100. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue_backends/kafka_backend.py +0 -0
  101. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue_backends/mongo_backend.py +0 -0
  102. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  103. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/__init__.py +0 -0
  104. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/models/Attachment.py +0 -0
  105. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/models/Channel.py +0 -0
  106. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/models/ChannelMember.py +0 -0
  107. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/models/Message.py +0 -0
  108. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/models/Workspace.py +0 -0
  109. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/models/__init__.py +0 -0
  110. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/realtime/storage.py +0 -0
  111. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/__init__.py +0 -0
  112. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  113. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_badges.scss +0 -0
  114. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  115. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_cards.scss +0 -0
  116. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_forms.scss +0 -0
  117. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_grid.scss +0 -0
  118. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_modals.scss +0 -0
  119. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_nav.scss +0 -0
  120. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_reset.scss +0 -0
  121. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_tables.scss +0 -0
  122. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_typography.scss +0 -0
  123. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  124. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/_variables.scss +0 -0
  125. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/base.scss +0 -0
  126. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/colors.scss +0 -0
  127. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/scss/tina4css/tina4.scss +0 -0
  128. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.76 → tina4_python-3.13.77}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.76
3
+ Version: 3.13.77
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.76"
3
+ version = "3.13.77"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -64,6 +64,9 @@ tina4python = "tina4_python.cli:main"
64
64
  [tool.pytest.ini_options]
65
65
  asyncio_mode = "auto"
66
66
  testpaths = ["tests"]
67
+ markers = [
68
+ "slow: a test that needs real wall-clock time to reproduce a timing bug (deselect with -m 'not slow')",
69
+ ]
67
70
 
68
71
  [tool.ruff]
69
72
  target-version = "py312"
@@ -49,6 +49,58 @@ def background(callback, interval: float = 1.0):
49
49
  _background_tasks.append({"callback": callback, "interval": interval})
50
50
 
51
51
 
52
+ async def background_tick_loop(callback, interval: float, executor, shutdown):
53
+ """Run one registered background task on its interval until shutdown.
54
+
55
+ A task NEVER overlaps itself: each run is awaited to completion before the
56
+ interval sleep and the next tick. That is the only sound reading of an
57
+ interval scheduler, and it is what makes the one-worker-per-task pool sizing
58
+ in `_serve` safe.
59
+
60
+ Args:
61
+ callback: The registered callable (sync or async, no arguments).
62
+ interval: Seconds to sleep between runs.
63
+ executor: ThreadPoolExecutor used for sync callbacks.
64
+ shutdown: asyncio.Event that ends the loop when set.
65
+ """
66
+ timeout = max(interval * 2, 5.0)
67
+ loop = asyncio.get_running_loop()
68
+ while not shutdown.is_set():
69
+ try:
70
+ if asyncio.iscoroutinefunction(callback):
71
+ # A coroutine IS cancellable, so the timeout really interrupts it.
72
+ await asyncio.wait_for(callback(), timeout=timeout)
73
+ else:
74
+ # Run sync callback in a thread pool — never blocks the event loop.
75
+ #
76
+ # A callable already running in a ThreadPoolExecutor CANNOT be
77
+ # cancelled: Future.cancel() returns False once it has started and
78
+ # there is no way to interrupt the thread. So this must NOT use
79
+ # wait_for() — that abandons the wrapper future while the thread
80
+ # keeps running, and the next tick then starts a SECOND copy
81
+ # alongside the first (silent double-execution of a slow sweep,
82
+ # with every later tick piling on another). Wait for the run to
83
+ # finish and only WARN about the overrun.
84
+ fut = loop.run_in_executor(executor, callback)
85
+ done, _pending = await asyncio.wait({fut}, timeout=timeout)
86
+ if not done:
87
+ Log.warning(
88
+ f"Background task is still running after {timeout:.1f}s. "
89
+ f"The next run is deferred until it finishes (a running sync "
90
+ f"task cannot be interrupted). Use non-blocking calls "
91
+ f"(e.g. queue.pop() instead of queue.consume())."
92
+ )
93
+ await fut # never overlap; re-raises the callback's error
94
+ except asyncio.TimeoutError:
95
+ Log.warning(
96
+ f"Background task exceeded {timeout:.1f}s timeout and was interrupted. "
97
+ f"Use non-blocking calls (e.g. queue.pop() instead of queue.consume())."
98
+ )
99
+ except Exception as e:
100
+ Log.error(f"Background task error: {e}")
101
+ await asyncio.sleep(interval)
102
+
103
+
52
104
  # module_name → source-file mtime at the last (re)import. Drives the
53
105
  # changed-file detection in ``_auto_discover``: a file whose mtime is newer
54
106
  # than the recorded value is re-executed in place, so editing an existing
@@ -2743,7 +2795,9 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
2743
2795
 
2744
2796
  # Start registered background tasks as asyncio tasks.
2745
2797
  # Sync callbacks run in a thread pool so they CANNOT block the event loop.
2746
- # A timeout (2x interval, min 5s) cancels runaway tasks.
2798
+ # max_workers is one per registered task sound only because a task never
2799
+ # overlaps itself (see background_tick_loop), so tasks cannot starve each
2800
+ # other of workers.
2747
2801
  import concurrent.futures
2748
2802
  _executor = concurrent.futures.ThreadPoolExecutor(
2749
2803
  max_workers=max(len(_background_tasks), 2),
@@ -2751,31 +2805,11 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
2751
2805
  )
2752
2806
  bg_tasks = []
2753
2807
  for task_def in _background_tasks:
2754
- cb = task_def["callback"]
2755
- iv = task_def["interval"]
2756
-
2757
- async def _tick_loop(_cb=cb, _iv=iv):
2758
- timeout = max(_iv * 2, 5.0)
2759
- while not shutdown.is_set():
2760
- try:
2761
- if asyncio.iscoroutinefunction(_cb):
2762
- await asyncio.wait_for(_cb(), timeout=timeout)
2763
- else:
2764
- # Run sync callback in thread pool — never blocks the event loop
2765
- await asyncio.wait_for(
2766
- loop.run_in_executor(_executor, _cb),
2767
- timeout=timeout,
2768
- )
2769
- except asyncio.TimeoutError:
2770
- Log.warning(
2771
- f"Background task exceeded {timeout:.1f}s timeout and was interrupted. "
2772
- f"Use non-blocking calls (e.g. queue.pop() instead of queue.consume())."
2773
- )
2774
- except Exception as e:
2775
- Log.error(f"Background task error: {e}")
2776
- await asyncio.sleep(_iv)
2777
-
2778
- bg_tasks.append(asyncio.create_task(_tick_loop()))
2808
+ bg_tasks.append(asyncio.create_task(
2809
+ background_tick_loop(
2810
+ task_def["callback"], task_def["interval"], _executor, shutdown,
2811
+ )
2812
+ ))
2779
2813
 
2780
2814
  await shutdown.wait()
2781
2815
 
File without changes