| Server IP : 138.197.107.151 / Your IP : 216.73.217.10 Web Server : Apache/2.4.58 (Ubuntu) System : Linux BloxBy-Builder 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 User : wpbetasites_mrakzqskir ( 1022) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bsd-crawler-parser/viewer/ |
Upload File : |
"""Gunicorn config for the AI crawler viewer.
Run with: gunicorn -c gunicorn.conf.py app:app
WHY ONE WORKER, MANY THREADS
The app holds a large in-memory shard cache (a week is ~3,250 shards / ~470MB).
A second worker process would not share it -- it would hold a second, cold copy,
and any request the OS routed to the cold worker would pay the full scan again.
So we run a single worker and get concurrency from threads inside it.
That is safe here because the work is I/O-bound: a request spends its time
waiting on S3, not on the GIL. The heavy S3 fan-out is itself bounded by a
single shared pool in s3data (see _executor there), so more threads here cannot
translate into more concurrent S3 connections -- they just let more HTTP
requests be in flight while that one pool feeds them.
If you ever genuinely outgrow one worker, the caches have to move out of process
(Redis, or a shared mmap) first -- do not just raise `workers`.
"""
import os
import sys
# THIS SERVER IS FOR LINUX (the DigitalOcean box). Gunicorn forks its workers,
# and fork + boto3 is unsafe on macOS: the child either gets SIGKILLed or aborts
# (SIGABRT) deep in a system framework, which pops "Python quit unexpectedly"
# crash dialogs. For local work on a Mac, use ./run.sh (the threaded dev server,
# no fork) instead of this.
#
# The block below makes gunicorn at least survivable on macOS if you must run it
# there -- but it is best-effort, not a guarantee, so prefer run.sh locally. It
# is scoped to darwin so the Linux deployment is completely untouched: in
# particular no_proxy is NOT forced on Linux, where a box may legitimately need
# an HTTP proxy to reach S3.
if sys.platform == "darwin":
# Let the forked child proceed past the ObjC fork-safety check...
os.environ.setdefault("OBJC_DISABLE_INITIALIZE_FORK_SAFETY", "YES")
# ...and stop it crashing in macOS's SystemConfiguration proxy lookup, which
# botocore triggers on its first S3 call in the child. This is the actual
# crash you hit; the line above alone is not enough.
os.environ.setdefault("no_proxy", "*")
os.environ.setdefault("NO_PROXY", "*")
# Keep the master clean of anything fork-unsafe: do NOT import app here or in any
# master-side hook. The app (and boto with it) is imported inside the worker,
# after the fork -- see post_worker_init. preload_app stays off for the same
# reason.
# Bind to loopback by default: put nginx (with TLS) in front in production. Set
# BIND=0.0.0.0:8765 only if something else already terminates TLS and firewalls
# the port -- the pages expose visitor IPs and browsed URLs.
bind = os.environ.get("BIND", "127.0.0.1:8765")
workers = 1
worker_class = "gthread"
threads = 2
# A cold, wide date range can take a couple of minutes to scan the first time.
# The default 30s worker timeout would kill the worker mid-scan and 502 the
# request, so allow for it. Requests are still bounded -- this is a ceiling.
timeout = int(os.environ.get("WEB_TIMEOUT", "300"))
graceful_timeout = 30
# Recycle the worker periodically so any slow leak cannot accumulate for months.
# Jitter avoids a recycle-storm (only one worker here, but cheap insurance).
max_requests = 500
max_requests_jitter = 50
# Log to stdout/stderr; journald/Docker capture them.
accesslog = "-"
errorlog = "-"
# Trust the reverse proxy on loopback for X-Forwarded-* (so request.scheme and
# the client IP reflect nginx, not 127.0.0.1). Only loopback is trusted.
forwarded_allow_ips = "127.0.0.1"
def post_worker_init(worker):
# Runs inside the worker, after the fork -- the only safe place to touch the
# app (and boto). Two jobs: warn loudly if the credentials or bucket are
# wrong (so it is not a mystery 500 on every request), and kick the cache
# prewarm on a background thread that belongs to this process (see the fork
# note on _executor in s3data). app.main() does both for local runs; gunicorn
# never calls it, so we do it here.
import threading
import app
ok, msg = app.s3data.check_access()
if not ok:
worker.log.error("cannot read s3://%s: %s", app.s3data.BUCKET, msg)
# threading.Thread(target=app._prewarm, daemon=True).start()