Skip to content

Fix bg tasks #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b7811bf
fix: separate scheduled tasks
samhotep Apr 14, 2025
cbcb3eb
fix: pass additional process args in order
samhotep Apr 15, 2025
f8a1c4e
chore: use celery tasks if available
samhotep Apr 15, 2025
44247cd
fix: remove rabbitmq dependency
samhotep Apr 16, 2025
47751d2
fix: start scheduled tasks on startup
samhotep Apr 16, 2025
a01a728
chore: check for redis host as proof for celery task
samhotep Apr 16, 2025
de82347
fix: pass correct celery object to tasks
samhotep Apr 16, 2025
26460cc
feat: use shared memory lock for migrations
samhotep Apr 16, 2025
43a5cde
run migrations with app context
samhotep Apr 23, 2025
b73c4ac
feat: use filecache lock
samhotep Apr 23, 2025
c1e13a4
Update CI.yml
samhotep Apr 23, 2025
ed9645f
Update CI.yml
samhotep Apr 23, 2025
9514d5a
fix: crash on filecache object deletion
samhotep Apr 23, 2025
c0a7b68
fix: pass process variables at runtime, and log errors
samhotep Apr 28, 2025
20842a3
feat: pre-register celery tasks
samhotep May 5, 2025
1d4b5e4
feat: return task result, not task
samhotep May 5, 2025
260669f
feat: use app context in scheduled tasks
samhotep May 5, 2025
cbe1f49
feat: use debug mode on ci
samhotep May 5, 2025
140b639
feat: increase curl delay
samhotep May 5, 2025
490a784
feat: use pathlib for directory scans
samhotep May 5, 2025
5476b5a
retrigger checks
samhotep May 5, 2025
350280f
feat: add ci job without redis
samhotep May 5, 2025
5817661
Update CI.yml
samhotep May 5, 2025
2b5765c
feat: print errors on close
samhotep May 6, 2025
bebb6e2
feat: use avaiable cache
samhotep May 6, 2025
a335144
feat: improve error handling in scheduled task
samhotep May 6, 2025
a782294
Update models.py
samhotep May 6, 2025
dad4725
Update playwright.yml
samhotep May 6, 2025
a92ed33
Update CI.yml
samhotep May 6, 2025
0ee0d65
Update CI.yml
samhotep May 6, 2025
aa9a8ac
feat: mark root pages with no children as incomplete
samhotep May 6, 2025
759f439
Update CI.yml
samhotep May 6, 2025
fd47a72
Update CI.yml
samhotep May 6, 2025
f80d302
feat: update CI.yaml
samhotep May 6, 2025
452cafe
Update CI.yml
samhotep May 6, 2025
fb407a0
Merge branch 'main' into fix-bg-tasks
muhammad-ali-pk May 7, 2025
978317f
feat: use lock to check if site is being cloned
samhotep May 7, 2025
589950d
Update site_repository.py
samhotep May 7, 2025
a663b83
Update README.md
samhotep May 7, 2025
23f4f11
Update CI.yml
samhotep May 7, 2025
31aef79
Update CI.yml
samhotep May 7, 2025
dcab8b0
Update entrypoint
samhotep May 7, 2025
453bd14
Create .dockerignore
samhotep May 7, 2025
809762d
Update Dockerfile
samhotep May 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
repositories
tree-cache
.venv
venv/
__pycache__/
*.so
build/
dist/
.eggs/
wheels/
*.egg-info/
.installed.cfg
*.egg
.pytest_cache/
.coverage
htmlcov/
.tox/
.coverage.*
.cache/
Dockerfile
docker-compose.yml
51 changes: 47 additions & 4 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,51 @@ jobs:
run-python:
name: Run Python
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run service
env:
GOOGLE_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
GOOGLE_PRIVATE_KEY_ID: ${{ secrets.PRIVATE_KEY_ID }}
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
SECRET_KEY: secret_key
GH_TOKEN: ${{ github.token }}
REPO_ORG: https://github.com/canonical
JIRA_EMAIL: [email protected]
JIRA_TOKEN: jiratoken
JIRA_URL: https://example.atlassian.net
JIRA_LABELS: somelabel
JIRA_COPY_UPDATES_EPIC: WD-9999999
GOOGLE_DRIVE_FOLDER_ID: folderid
COPYDOC_TEMPLATE_ID: templateid
FLASK_DEBUG: 1
run: |
gunicorn webapp.app:app --workers=2 --bind 0.0.0.0:8104 & sleep 3
curl --head --fail --retry-delay 1 --retry 30 --retry-connrefused http://localhost:8104

run-python-redis:
name: Run Python Redis
runs-on: ubuntu-latest
services:
redis:
image: redis
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
Expand Down Expand Up @@ -134,9 +176,10 @@ jobs:
JIRA_COPY_UPDATES_EPIC: WD-9999999
GOOGLE_DRIVE_FOLDER_ID: folderid
COPYDOC_TEMPLATE_ID: templateid
FLASK_DEBUG: 1
run: |
gunicorn webapp.app:app --daemon --bind 0.0.0.0:8104
curl --head --fail --retry-delay 1 --retry 30 --retry-connrefused http://localhost:8104
flask --app webapp.app run --debug & sleep 3
curl --head --fail --retry-delay 1 --retry 30 --retry-connrefused http://localhost:5000

run-image:
name: Run Image
Expand Down Expand Up @@ -170,11 +213,11 @@ jobs:
GOOGLE_PRIVATE_KEY_ID: ${{ secrets.PRIVATE_KEY_ID }}
run: |
docker run \
-p 8104:8104 \
-p 8104:80 \
-e SECRET_KEY=secret_key \
-e REDIS_HOST=localhost \
-e REDIS_PORT=6379 \
-e GH_TOKEN=${{ github.token }} \
-e GH_TOKEN=github.token \
-e REPO_ORG=https://github.com/canonical \
-e DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres \
-e [email protected] \
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ ARG BUILD_ID
ENV TALISKER_REVISION_ID="${BUILD_ID}"

# Setup commands to run web service
RUN chmod +x ./entrypoint
ENTRYPOINT ["./entrypoint"]
CMD ["0.0.0.0:80"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ If you do not want to use a dedicated cache, a simple filecache has been include

```bash
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres
docker run -d -p 6379:6379 valkey/valkey
docker run -d -p 6379:6379 redis
```

#### Virtual Environment
Expand Down Expand Up @@ -253,7 +253,7 @@ Please note, make sure the containers for postgres and valkey are already runnin

```bash
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres
docker run -d -p 6379:6379 valkey/valkey
docker run -d -p 6379:6379 redis
```

You can optionally use dotrun to start the service. When the 1.1.0-rc1 branch is merged, then we can use dotrun without the `--release` flag.
Expand Down
2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from webapp.app import app # noqa F401
from webapp.app import app # noqa: F401
3 changes: 0 additions & 3 deletions entrypoint
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,5 @@ activate() {
RUN_COMMAND="${RUN_COMMAND} --reload --log-level debug --timeout 9999"
fi

# Run new migrations if needed
flask db upgrade

${RUN_COMMAND}
}
34 changes: 23 additions & 11 deletions webapp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from werkzeug.middleware.proxy_fix import ProxyFix

from webapp.cache import init_cache
from webapp.celery import init_celery
from webapp.context import RegexConverter, base_context, clear_trailing_slash
from webapp.gdrive import init_gdrive
from webapp.github import init_github
from webapp.jira import init_jira
from webapp.models import init_db
from webapp.sso import init_sso
Expand Down Expand Up @@ -47,8 +47,8 @@ def create_app():
# Initialize gdrive
init_gdrive(app)

# Initialize celery
init_celery(app)
# Initialize github
init_github(app)

return app

Expand All @@ -67,7 +67,8 @@ def set_security_headers(response):
if flask.request.endpoint in flask.current_app.view_functions:
view_func = flask.current_app.view_functions[flask.request.endpoint]
add_xframe_options_header = not hasattr(
view_func, "_exclude_xframe_options_header"
view_func,
"_exclude_xframe_options_header",
)

if add_xframe_options_header and "X-Frame-Options" not in response.headers:
Expand Down Expand Up @@ -106,10 +107,14 @@ def set_cache_control_headers(response):

max_age = response.cache_control.max_age
stale_while_revalidate = response.cache_control._get_cache_value(
"stale-while-revalidate", False, int
"stale-while-revalidate",
False,
int,
)
stale_if_error = response.cache_control._get_cache_value(
"stale-if-error", False, int
"stale-if-error",
False,
int,
)

if type(max_age) is not int:
Expand Down Expand Up @@ -142,7 +147,9 @@ def set_cache_control_headers(response):
#
# An additional day will hopefully be long enough for most cases.
response.cache_control._set_cache_value(
"stale-while-revalidate", "86400", int
"stale-while-revalidate",
"86400",
int,
)

if type(stale_if_error) is not int:
Expand All @@ -164,7 +171,9 @@ def set_cache_control_headers(response):
#
# So we set this to 5 minutes following expiry as a trade-off.
response.cache_control._set_cache_value(
"stale-if-error", "300", int
"stale-if-error",
"300",
int,
)

return response
Expand Down Expand Up @@ -267,7 +276,8 @@ def __init__(
def not_found_error(error):
return (
flask.render_template(
template_404, message=error.description
template_404,
message=error.description,
),
404,
)
Expand All @@ -278,7 +288,8 @@ def not_found_error(error):
def internal_error(error):
return (
flask.render_template(
template_500, message=error.description
template_500,
message=error.description,
),
500,
)
Expand All @@ -294,7 +305,8 @@ def status_check():
@self.route("/favicon.ico")
def favicon():
return flask.send_file(
favicon_path, mimetype="image/vnd.microsoft.icon"
favicon_path,
mimetype="image/vnd.microsoft.icon",
)

elif favicon_url:
Expand Down
17 changes: 12 additions & 5 deletions webapp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@
from flask import render_template

from webapp import create_app
from webapp.github import init_github
from webapp.celery import init_celery
from webapp.routes.jira import jira_blueprint
from webapp.routes.product import product_blueprint
from webapp.routes.tree import tree_blueprint
from webapp.routes.user import user_blueprint
from webapp.scheduled_tasks import init_scheduled_tasks
from webapp.sso import login_required

app = create_app()

# Tasks
init_github(app)
# Initialize celery
celery_app = init_celery(app)

# Initialize scheduled tasks
init_scheduled_tasks()


# Server-side routes
app.register_blueprint(tree_blueprint)
Expand All @@ -28,13 +33,15 @@
@login_required
def index():
return render_template(
"index.html", is_dev=environ.get("FLASK_ENV") == "development"
"index.html",
is_dev=environ.get("FLASK_ENV") == "development",
)


@app.route("/app/<path:path>")
@login_required
def webpage(path):
return render_template(
"index.html", is_dev=environ.get("FLASK_ENV") == "development"
"index.html",
is_dev=environ.get("FLASK_ENV") == "development",
)
42 changes: 22 additions & 20 deletions webapp/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@
from redis import exceptions as redis_exceptions


def init_cache(app: Flask):
try:
cache = RedisCache(app)
except Exception as e:
cache = FileCache(app)
app.logger.info(
f"Error: {e} Redis cache is not available."
" Using FileCache instead."
)
app.config["CACHE"] = cache
return cache


class Cache(ABC):
"""Abstract Cache class"""

Expand All @@ -32,22 +19,18 @@ class Cache(ABC):
@abstractmethod
def get(self, key: str):
"""Get a value from the cache"""
pass

@abstractmethod
def set(self, key: str, value: Any):
"""Set a value in the cache"""
pass

@abstractmethod
def delete(self, key: str):
"""Delete a value from the cache"""
pass

@abstractmethod
def is_available(self):
"""Check if the cache is available"""
pass


class RedisCache(Cache):
Expand Down Expand Up @@ -139,7 +122,8 @@ def connect(self):

path_exists = Path(self.cache_path).exists()
path_writable = Path(self.cache_path).is_dir() and os.access(
self.cache_path, os.W_OK
self.cache_path,
os.W_OK,
)
if not path_exists and path_writable:
raise ConnectionError("Cache directory is not writable")
Expand All @@ -151,7 +135,13 @@ def save_to_file(self, key: str, value: Any):
data = json.dumps(value)
# Delete the file if it exists
if Path(self.cache_path + "/" + key).exists():
os.remove(self.cache_path + "/" + key)
try:
os.remove(self.cache_path + "/" + key)
except FileNotFoundError:
# We catch this as due to different processes potentially
# accessing this method, deleting a file could face a race
# condition.
self.logger.info("File already removed")
# Create base directory if it does not exist
if not Path(self.cache_path).exists():
Path(self.cache_path).mkdir(parents=True, exist_ok=True)
Expand All @@ -165,7 +155,7 @@ def load_from_file(self, key: str):
# Check if the file exists
if not Path(self.cache_path + "/" + key).exists():
return None
with open(self.cache_path + "/" + key, "r") as f:
with open(self.cache_path + "/" + key) as f:
data = f.read()
return json.loads(data)

Expand All @@ -187,3 +177,15 @@ def onerror(*args, **kwargs):
os.chmod(self.cache_path + "/" + key, 0o777)

return shutil.rmtree(self.cache_path + "/" + key, onerror=onerror)


def init_cache(app: Flask) -> Cache:
try:
cache = RedisCache(app)
except Exception as e:
cache = FileCache(app)
msg = f"Error: {e} Redis cache is not available."
" Using FileCache instead."
app.logger.info(msg)
app.config["CACHE"] = cache
return cache
Loading
Loading