Production symptom: previews of all types (pdf/pyvips/onlyoffice) start hitting the 10s timeout and never recover until server restart, while the rest of the server stays healthy. Root cause (reproduced on Python 3.12): workers were spawned with stderr=PIPE that nothing drained after startup. Once the OS pipe buffer filled from accumulated worker tracebacks and library warnings, asyncio flow control stopped the parent reading it and the worker blocked forever mid-request on a stderr write. The 10s timeout then fired, but _replace_worker hung forever in proc.wait() even after kill() — the flow-control-paused pipe transport never sees EOF — permanently wedging one dispatcher per stuck worker. Once all dispatchers were stuck, every preview request timed out. Restart cleared it. Fixes: - Spawn workers with inherited stderr (stderr=None) so worker diagnostics go straight to the server log and no undrained pipe can exist. - Bound proc.wait() in worker kill() with a 5s grace timeout so a wedged transport can never hang a dispatcher; log the worker pid instead. - Guard the dispatch loop with an outer exception handler so a dispatcher can never die silently and shrink pool capacity. - Retry failed worker replacement spawns with 1s-30s backoff instead of silently shrinking the pool. - Fix latent crash: except-tuple referenced msgspec.json.DecodeError, which does not exist in the installed msgspec; any protocol failure would itself raise AttributeError. Use msgspec.DecodeError. - Worker: redirect Python-level sys.stdout to stderr in persistent mode and keep the raw buffer solely for the binary protocol, so a library print() can never corrupt the command channel again (cf. the pymupdf deprecation warning that crashed workers at startup). - Worker: close the pymupdf document explicitly in process_pdf. - Log worker pid on timeout/protocol/checksum failures, and log failed kills and replacement retries, for future production diagnostics. Add tests/test_preview_pool.py with an end-to-end regression recreating the wedged-worker setup (piped, undrained stderr) plus kill-grace, respawn-retry and dispatcher-survival tests.
205 lines
5.8 KiB
TOML
205 lines
5.8 KiB
TOML
[build-system]
|
|
requires = ["hatchling", "hatch-vcs"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[project]
|
|
name = "cista"
|
|
dynamic = ["version"]
|
|
description = "Dropbox-like file server with modern web interface"
|
|
readme = "README.md"
|
|
authors = [
|
|
{ name = "Vasanko" },
|
|
]
|
|
maintainers = [
|
|
{ name = "Vasanko" },
|
|
]
|
|
keywords = ["file-server", "web-interface", "dropbox", "storage"]
|
|
classifiers = [
|
|
"Development Status :: 5 - Production/Stable",
|
|
"Environment :: Web Environment",
|
|
"Intended Audience :: End Users/Desktop",
|
|
"Intended Audience :: System Administrators",
|
|
"License :: Public Domain",
|
|
"License :: OSI Approved :: MIT License",
|
|
]
|
|
requires-python = ">=3.11"
|
|
dependencies = [
|
|
"argon2-cffi>=25.1.0",
|
|
"av>=15.0.0",
|
|
"blake3>=1.0.5",
|
|
"docopt-ng>=0.9.0",
|
|
"fastapi-vue>=0.5.2",
|
|
"fastapi[standard]>=0.128.0",
|
|
"html5tagger>=1.3.0",
|
|
"httpx>=0.28.0",
|
|
"inotify>=0.2.12",
|
|
"msgspec>=0.19.0",
|
|
"natsort>=8.4.0",
|
|
"numpy>=2.3.2",
|
|
"pathvalidate>=3.3.1",
|
|
"pillow>=11.3.0",
|
|
"pillow-heif>=1.1.0",
|
|
"pyjwt>=2.10.1",
|
|
"pymupdf>=1.26.3",
|
|
"pyvips[binary]>=3.1.1",
|
|
"sanic>=25.12.0",
|
|
"setproctitle>=1.3.6",
|
|
"stream-zip>=0.0.83",
|
|
"tomli_w>=1.2.0",
|
|
"tracerite>=2.3.1",
|
|
"zstandard>=0.24.0",
|
|
]
|
|
|
|
[project.urls]
|
|
Homepage = "https://git.zi.fi/Vasanko/cista-storage"
|
|
|
|
[project.scripts]
|
|
cista = "cista.__main__:main"
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.4.1",
|
|
"ruff>=0.8.0",
|
|
"mypy>=1.13.0",
|
|
"pre-commit>=4.0.0",
|
|
]
|
|
test = [
|
|
"pytest>=8.4.1",
|
|
"pytest-cov>=6.0.0",
|
|
"pytest-asyncio>=0.25.0",
|
|
]
|
|
docs = [
|
|
"sphinx>=8.0.0",
|
|
"sphinx-rtd-theme>=3.0.0",
|
|
]
|
|
|
|
[tool.hatch.version]
|
|
source = "vcs"
|
|
|
|
[tool.hatch.build]
|
|
artifacts = ["cista/frontend-build", "cista/docker"]
|
|
targets.sdist.hooks.custom.path = "scripts/fastapi-vue/buildhook.py"
|
|
targets.sdist.include = [
|
|
"/cista",
|
|
]
|
|
hooks.vcs.version-file = "cista/_version.py"
|
|
hooks.vcs.template = """
|
|
# This file is automatically generated by hatch build.
|
|
__version__ = {version!r}
|
|
"""
|
|
only-packages = true
|
|
packages = ["cista"]
|
|
|
|
[tool.pytest.ini_options]
|
|
addopts = [
|
|
"--import-mode=importlib",
|
|
"--verbosity=2",
|
|
"--strict-markers",
|
|
"--strict-config",
|
|
"--cov=cista",
|
|
"--cov-report=term-missing",
|
|
"--cov-report=html",
|
|
"--cov-branch",
|
|
]
|
|
testpaths = ["tests"]
|
|
python_files = ["test_*.py", "*_test.py"]
|
|
python_classes = ["Test*"]
|
|
python_functions = ["test_*"]
|
|
markers = [
|
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
"integration: marks tests as integration tests",
|
|
]
|
|
filterwarnings = [
|
|
"error",
|
|
"ignore::UserWarning",
|
|
"ignore::DeprecationWarning",
|
|
]
|
|
|
|
[tool.ruff]
|
|
target-version = "py311"
|
|
|
|
[tool.ruff.lint]
|
|
select = ["ALL"]
|
|
ignore = [
|
|
"COM812", # formatter compatibility
|
|
"ISC001", # formatter compatibility
|
|
"ANN001", # legacy codebase: no full runtime annotation coverage yet
|
|
"ANN002", # legacy codebase: no full runtime annotation coverage yet
|
|
"ANN003", # legacy codebase: no full runtime annotation coverage yet
|
|
"ANN201", # legacy codebase: no full runtime annotation coverage yet
|
|
"ANN202", # legacy codebase: no full runtime annotation coverage yet
|
|
"ANN204", # legacy codebase: no full runtime annotation coverage yet
|
|
"ANN205", # legacy codebase: no full runtime annotation coverage yet
|
|
"BLE001", # broad catch remains in boundary/proxy/error-handling paths
|
|
"C901", # legacy complexity; keep other correctness rules enabled
|
|
"CPY", # copyright notices not wanted in this codebase
|
|
"D100", # legacy docs not yet standardized
|
|
"D101", # legacy docs not yet standardized
|
|
"D102", # legacy docs not yet standardized
|
|
"D103", # legacy docs not yet standardized
|
|
"D104", # legacy docs not yet standardized
|
|
"D105", # legacy docs not yet standardized
|
|
"D107", # legacy docs not yet standardized
|
|
"D200", # legacy docs not yet standardized
|
|
"D203", # avoid D203/D211 conflict under ALL selection
|
|
"D212", # legacy docs not yet standardized
|
|
"D213", # legacy docs not yet standardized
|
|
"D400", # legacy docs not yet standardized
|
|
"D401", # legacy docs not yet standardized
|
|
"D413", # legacy docs not yet standardized
|
|
"D415", # legacy docs not yet standardized
|
|
"E501", # existing long literals/log strings
|
|
"EM101", # exception-message style; low signal for this project
|
|
"EM102", # exception-message style; low signal for this project
|
|
"INP001", # scripts folder intentionally lacks package markers
|
|
"PLR0911", # legacy complexity; keep other correctness rules enabled
|
|
"PLR0912", # legacy complexity; keep other correctness rules enabled
|
|
"PLR0913", # legacy complexity; keep other correctness rules enabled
|
|
"PLR0915", # legacy complexity; keep other correctness rules enabled
|
|
"PLR2004", # we like magic numbers (don't remove this suppression)
|
|
"PLW0603", # module-level shared state exists in server runtime code
|
|
"TRY003", # exception-message strictness too noisy on legacy handlers
|
|
]
|
|
isort.known-first-party = ["cista"]
|
|
per-file-ignores."tests/*" = ["S", "ANN", "D", "INP", "PLR2004", "ARG001", "SLF001"]
|
|
per-file-ignores."scripts/*" = ["T20"]
|
|
|
|
[dependency-groups]
|
|
dev = [
|
|
"pytest>=8.4.1",
|
|
"pytest-asyncio>=0.25.0",
|
|
"pytest-cov>=7.0.0",
|
|
"ruff>=0.8.0",
|
|
"mypy>=1.13.0",
|
|
"pre-commit>=4.0.0",
|
|
"httpx>=0.28.1",
|
|
"sanic-testing>=24.6.0",
|
|
]
|
|
|
|
[tool.coverage.run]
|
|
source = ["cista"]
|
|
branch = true
|
|
omit = [
|
|
"*/tests/*",
|
|
"*/test_*",
|
|
"*/__pycache__/*",
|
|
"cista/_version.py",
|
|
]
|
|
|
|
[tool.coverage.report]
|
|
exclude_lines = [
|
|
"pragma: no cover",
|
|
"def __repr__",
|
|
"if self.debug:",
|
|
"if settings.DEBUG",
|
|
"raise AssertionError",
|
|
"raise NotImplementedError",
|
|
"if 0:",
|
|
"if __name__ == .__main__.:",
|
|
"class .*\\bProtocol\\):",
|
|
"@(abc\\.)?abstractmethod",
|
|
]
|
|
show_missing = true
|
|
skip_covered = false
|
|
precision = 2
|