Skip to content
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

Django q #1398

Merged
merged 102 commits into from
Apr 11, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
7bec3ff
django-q
SchrodingersGat Mar 6, 2021
45b3c68
New status info
SchrodingersGat Mar 6, 2021
660fed9
Remove unused code from settings.py
SchrodingersGat Mar 10, 2021
5b68d82
Skeleton for background tasks
SchrodingersGat Mar 10, 2021
1532a0c
Add InvenTree/apps.py
SchrodingersGat Mar 11, 2021
3cf5aec
Refactor
SchrodingersGat Mar 11, 2021
18defcf
Read version number from GitHub
SchrodingersGat Mar 11, 2021
4925f24
Add "up to date" info to the "about" window
SchrodingersGat Mar 11, 2021
bfb0cb3
Add a "heartbeat" task which runs every 5 minutes
SchrodingersGat Mar 12, 2021
5b8eb1c
Newline
SchrodingersGat Mar 12, 2021
006dd10
Delete successful tasks more than a month old
SchrodingersGat Mar 12, 2021
ef4dbda
Catch errors if the DB is not up
SchrodingersGat Mar 12, 2021
51616c8
Merge remote-tracking branch 'upstream/master' into django-q
SchrodingersGat Mar 12, 2021
9d404af
Add 'ignore' rules for the django-q tables
SchrodingersGat Mar 12, 2021
18b559f
Fix for unit test
SchrodingersGat Mar 12, 2021
700effc
Remove celery reference
SchrodingersGat Mar 12, 2021
de85d61
Directly compare version tuples, rather than converting to primitive
SchrodingersGat Mar 14, 2021
f6dd710
Automatically delete old heartbeat messages
SchrodingersGat Mar 14, 2021
6ea846c
Add a #TODO
SchrodingersGat Mar 14, 2021
24823ad
Adds unit tests for version number comparison
SchrodingersGat Mar 14, 2021
c1aed51
Fix import error
SchrodingersGat Mar 14, 2021
c6e154f
PEP style fixes
SchrodingersGat Mar 14, 2021
2836636
First pass at a supervisor.conf file
SchrodingersGat Mar 19, 2021
b7718d9
Specify user and logfile
SchrodingersGat Mar 19, 2021
8fd666e
Improvements for "check for updates" task
SchrodingersGat Mar 22, 2021
edbbfff
Reduce frequency of heartbeat
SchrodingersGat Mar 23, 2021
e3f49b8
Install invoke and gunicorn as part of requirements.txt
SchrodingersGat Mar 23, 2021
ce64feb
Update supervisor conf file
SchrodingersGat Mar 23, 2021
df0ab23
Remove invoke tasks which perform system commands
SchrodingersGat Mar 24, 2021
3a0c68b
Add invoke task to start background worker
SchrodingersGat Mar 24, 2021
3ddbb6a
Check for empty values
SchrodingersGat Mar 30, 2021
39b2c5f
Reintroduce default database config
SchrodingersGat Mar 30, 2021
e7ed4c4
Travis fixes
SchrodingersGat Mar 30, 2021
83f8afe
Add github actions
SchrodingersGat Mar 30, 2021
731ec25
Merge remote-tracking branch 'inventree/master' into django-q
SchrodingersGat Mar 31, 2021
3f25727
Specify directories for CI
SchrodingersGat Mar 31, 2021
c0a0ca4
PEP fix
SchrodingersGat Mar 31, 2021
ab57fd3
Build docker image
SchrodingersGat Mar 31, 2021
1a7b6e2
Fix
SchrodingersGat Mar 31, 2021
6017cad
So apparently I cannot spell...
SchrodingersGat Mar 31, 2021
2746396
Fix tag name
SchrodingersGat Mar 31, 2021
58bfc80
Alpine uses different commands
SchrodingersGat Mar 31, 2021
601aff8
Install git
SchrodingersGat Mar 31, 2021
42b400e
typo fix
SchrodingersGat Mar 31, 2021
1f881dd
Run as root
SchrodingersGat Mar 31, 2021
61f8b98
lib name fix
SchrodingersGat Mar 31, 2021
251ec7a
Fix lib names
SchrodingersGat Mar 31, 2021
24d36e0
Getting there...
SchrodingersGat Mar 31, 2021
286cf9b
gcc required
SchrodingersGat Mar 31, 2021
8b227ce
More required packages, I guess...
SchrodingersGat Mar 31, 2021
ff6b127
Typo fixin'
SchrodingersGat Mar 31, 2021
7683cc1
APK not APT
SchrodingersGat Mar 31, 2021
b9f9b26
Sudo not required, I guess?
SchrodingersGat Mar 31, 2021
b9e81c3
Start supervisord
SchrodingersGat Mar 31, 2021
38b9655
Remove unused workflow
SchrodingersGat Mar 31, 2021
76ab38a
Add docker info
SchrodingersGat Apr 1, 2021
08a1a6c
Add configuration options for the Dockerfile
SchrodingersGat Apr 1, 2021
d446f8d
Add supervisor conf file specific to docker
SchrodingersGat Apr 1, 2021
839c291
Dockerfile updates
SchrodingersGat Apr 1, 2021
148600a
Copy gunicorn.conf.py
SchrodingersGat Apr 1, 2021
db858b3
Install packages inside venv
SchrodingersGat Apr 1, 2021
47ba059
Reference environment variables in supervisor conf file
SchrodingersGat Apr 1, 2021
8e7e360
Fix venv
SchrodingersGat Apr 1, 2021
be41be3
Add "wait_for_db" management command
SchrodingersGat Apr 1, 2021
8d3b9e2
Updates to settings.py
SchrodingersGat Apr 1, 2021
2436b1f
Entrypoint script - start.sh
SchrodingersGat Apr 1, 2021
00c4519
Simplify dockerfile
SchrodingersGat Apr 1, 2021
d915317
Unit testing for task scheduling
SchrodingersGat Apr 7, 2021
4a3ca46
Dockerfile updates
SchrodingersGat Apr 7, 2021
9c38d67
Merge remote-tracking branch 'upstream/master' into django-q
SchrodingersGat Apr 7, 2021
d4d9263
Add option to specify config file via environment variable
SchrodingersGat Apr 7, 2021
14aead0
Adds docker_compose file
SchrodingersGat Apr 7, 2021
ed304f5
Better configuration of github repo
SchrodingersGat Apr 7, 2021
71cac6e
Simplify waiting for db
SchrodingersGat Apr 7, 2021
3926276
Greatly simplified "wait_for_db" command
SchrodingersGat Apr 7, 2021
3381945
Add newline
SchrodingersGat Apr 8, 2021
47a93bc
More environment variables for config.yaml
SchrodingersGat Apr 8, 2021
8eb571b
Update dockerfile
SchrodingersGat Apr 10, 2021
1372343
Updates to docker-compose file
SchrodingersGat Apr 10, 2021
9086c8a
Simplify external directory structure
SchrodingersGat Apr 10, 2021
5e54b0f
Auto-generate key file if it does not exist!
SchrodingersGat Apr 10, 2021
e787c85
Update logger context
SchrodingersGat Apr 10, 2021
178715c
Auto create config file in specified location if it does not exist
SchrodingersGat Apr 10, 2021
823f84e
Simplified volume management in docker-compose
SchrodingersGat Apr 10, 2021
2f1db48
Do not use python virtual environment inside container
SchrodingersGat Apr 10, 2021
91b6f98
Update directory structure to match docker config
SchrodingersGat Apr 10, 2021
5d9e273
Adds nxinx service
SchrodingersGat Apr 10, 2021
8f626d3
Fix location of entrypoint scripts
SchrodingersGat Apr 10, 2021
5a168ab
Separated docker file into separate directory
SchrodingersGat Apr 10, 2021
3da5505
Fix build workflow
SchrodingersGat Apr 10, 2021
b74d365
Merge remote-tracking branch 'upstream/master' into django-q
SchrodingersGat Apr 10, 2021
0e1b647
Remove mariadb test (uses the same backend as mysql!)
SchrodingersGat Apr 10, 2021
c9021fe
Simplify docker build workflow
SchrodingersGat Apr 10, 2021
2e8d3b6
Fix for tasks.py (??)
SchrodingersGat Apr 11, 2021
78bcbe2
Update supervisor conf file
SchrodingersGat Apr 11, 2021
f6f3815
Include worker status in main API call
SchrodingersGat Apr 11, 2021
44fe572
Disgusting hack for tasks.py
SchrodingersGat Apr 11, 2021
5f9236d
Updates to docker files
SchrodingersGat Apr 11, 2021
f9449da
Merge remote-tracking branch 'upstream/master' into django-q
SchrodingersGat Apr 11, 2021
b490c5d
Add new docker workflow for publising docker images on release
SchrodingersGat Apr 11, 2021
8f07efa
Add dockerhub badge
SchrodingersGat Apr 11, 2021
c2f85b0
docker-compose tweaks
SchrodingersGat Apr 11, 2021
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
44 changes: 44 additions & 0 deletions InvenTree/InvenTree/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-

import logging

from django.apps import AppConfig
from django.core.exceptions import AppRegistryNotReady

import InvenTree.tasks


logger = logging.getLogger(__name__)


class InvenTreeConfig(AppConfig):
name = 'InvenTree'

def ready(self):

self.start_background_tasks()

def start_background_tasks(self):

try:
from django_q.models import Schedule
except (AppRegistryNotReady):
return

logger.info("Starting background tasks...")

InvenTree.tasks.schedule_task(
'InvenTree.tasks.delete_successful_tasks',
schedule_type=Schedule.WEEKLY,
)

InvenTree.tasks.schedule_task(
'InvenTree.tasks.check_for_updates',
schedule_type=Schedule.DAILY
)

InvenTree.tasks.schedule_task(
'InvenTree.tasks.heartbeat',
schedule_type=Schedule.MINUTES,
minutes=5
)
16 changes: 14 additions & 2 deletions InvenTree/InvenTree/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,22 @@ def health_status(request):

request._inventree_health_status = True

return {
"system_healthy": InvenTree.status.check_system_health(),
status = {
'django_q_running': InvenTree.status.is_q_cluster_running(),
}

all_healthy = True

for k in status.keys():
Copy link
Contributor

@rcludwick rcludwick Mar 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all_healthy = all([v for v in status.values()])

if status[k] is not True:
all_healthy = False

status['system_healthy'] = all_healthy

status['up_to_date'] = InvenTree.version.isInvenTreeUpToDate()

return status


def status_codes(request):
"""
Expand Down
19 changes: 14 additions & 5 deletions InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def get_setting(environment_var, backup_val, default_value=None):
'report.apps.ReportConfig',
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last

# Third part add-ons
'django_filters', # Extended filter functionality
Expand All @@ -211,6 +212,7 @@ def get_setting(environment_var, backup_val, default_value=None):
'djmoney', # django-money integration
'djmoney.contrib.exchange', # django-money exchange rates
'error_report', # Error reporting in the admin interface
'django_q',
]

MIDDLEWARE = CONFIG.get('middleware', [
Expand Down Expand Up @@ -285,6 +287,18 @@ def get_setting(environment_var, backup_val, default_value=None):

WSGI_APPLICATION = 'InvenTree.wsgi.application'

# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': 4,
'timeout': 90,
'retry': 120,
'queue_limit': 50,
'bulk': 10,
'orm': 'default',
'sync': True,
}

# Markdownx configuration
# Ref: https://neutronx.github.io/django-markdownx/customization/
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
Expand Down Expand Up @@ -401,11 +415,6 @@ def get_setting(environment_var, backup_val, default_value=None):
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'qr-code': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'qr-code-cache',
'TIMEOUT': 3600
}
}

# Password validation
Expand Down
50 changes: 37 additions & 13 deletions InvenTree/InvenTree/status.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
"""
Provides system status functionality checks.
"""
# -*- coding: utf-8 -*-

from django.utils.translation import ugettext as _
from __future__ import unicode_literals

import logging
from datetime import datetime, timedelta

from django.utils.translation import ugettext as _

from django_q.models import Success
from django_q.monitor import Stat

logger = logging.getLogger(__name__)


def is_q_cluster_running(**kwargs):
"""
Return True if at least one cluster worker is running
"""

clusters = Stat.get_all()

if len(clusters) > 0:
Copy link
Contributor

@rcludwick rcludwick Mar 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if clusters:
return True

Since it's more pythonic.

Or if stat.get_all(): return True

return True

"""
Sometimes Stat.get_all() returns [].
In this case we have the 'heartbeat' task running every five minutes.
Check to see if we have a result within the last ten minutes
"""

now = datetime.now()
past = now - timedelta(minutes=10)

results = Success.objects.filter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just delete any heart beat other than the last one while I'm in this code.

func='InvenTree.tasks.heartbeat',
started__gte=past
)

# If any results are returned, then the background worker is running!
return results.exists()


def check_system_health(**kwargs):
"""
Check that the InvenTree system is running OK.
Expand All @@ -19,21 +53,11 @@ def check_system_health(**kwargs):

result = True

if not check_celery_worker(**kwargs):
if not is_q_cluster_running(**kwargs):
result = False
logger.warning(_("Celery worker check failed"))
logger.warning(_("Background worker check failed"))

if not result:
logger.warning(_("InvenTree system health checks failed"))

return result


def check_celery_worker(**kwargs):
"""
Check that a celery worker is running.
"""

# TODO - Checks that the configured celery worker thing is running

return True
118 changes: 118 additions & 0 deletions InvenTree/InvenTree/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import re
import json
import requests
import logging

from datetime import datetime, timedelta

from django.core.exceptions import AppRegistryNotReady
from django.db.utils import OperationalError, ProgrammingError


logger = logging.getLogger(__name__)


def schedule_task(taskname, **kwargs):
"""
Create a scheduled task.
If the task has already been scheduled, ignore!
"""

try:
from django_q.models import Schedule
except (AppRegistryNotReady):
logger.warning("Could not start background tasks - App registry not ready")
return

try:
if Schedule.objects.filter(func=taskname).exists():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens when the task parameters change? Like you want to shift the period from 5 minutes to 30 minutes say?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Code has been updated now.

logger.info(f"Scheduled task '{taskname}' already exists. (Skipping)")
else:
logger.info(f"Creating scheduled task '{taskname}'")

Schedule.objects.create(
func=taskname,
**kwargs
)
except (OperationalError, ProgrammingError):
# Required if the DB is not ready yet
pass


def heartbeat():
"""
Simple task which runs at 5 minute intervals,
so we can determine that the background worker
is actually running.

(There is probably a less "hacky" way of achieving this)
"""
pass


def delete_successful_tasks():
"""
Delete successful task logs
which are more than a month old.
"""

threshold = datetime.now() - timedelta(days=30)

results = Success.objects.filter(
started__lte=threshold
)

results.delete()


def check_for_updates():
"""
Check if there is an update for InvenTree
"""

try:
import common.models
except AppRegistryNotReady:
return

response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')

if not response.status_code == 200:
logger.warning(f'Unexpected status code from GitHub API: {response.status_code}')
return

data = json.loads(response.text)

tag = data.get('tag_name', None)

if not tag:
logger.warning("'tag_name' missing from GitHub response")
return

match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)

if not len(match.groups()) == 3:
logger.warning(f"Version '{tag}' did not match expected pattern")
return

try:
latest_version = [int(x) for x in match.groups()]
except (ValueError):
logger.warning(f"Version '{tag}' not integer format")
return

if not len(latest_version) == 3:
logger.warning(f"Version '{tag}' is not correct format")
return

logger.info(f"Latest InvenTree version: '{tag}'")

# Save the version to the database
common.models.InvenTreeSetting.set_setting(
'INVENTREE_LATEST_VERSION',
tag,
None
)
13 changes: 13 additions & 0 deletions InvenTree/InvenTree/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .validators import validate_overage, validate_part_name
from . import helpers
from . import version

from mptt.exceptions import InvalidMove

Expand Down Expand Up @@ -269,3 +270,15 @@ def test_failures(self):

with self.assertRaises(ValidationError):
e("10, a, 7-70j", 4)


class TestVersionNumber(TestCase):

def test_tuple(self):

v = version.inventreeVersionTuple()
self.assertEqual(len(v), 3)

s = '.'.join([str(i) for i in v])

self.assertTrue(s in version.inventreeVersion())
Loading