Fixes on task state
This commit is contained in:
parent
6fa29a15b8
commit
13cf3a41ed
2
TODO.md
2
TODO.md
|
@ -355,5 +355,3 @@ make django admin taskstate uncollapse fucking traceback, ( if exists ?)
|
||||||
# backend.context and backned.instance provided when an action is called? like forms.cleaned_data: do it on manager.generation(backend.context = backend.get_context()) or in backend.__getattr__ ? also backend.head,tail,content switching on manager.generate()?
|
# backend.context and backned.instance provided when an action is called? like forms.cleaned_data: do it on manager.generation(backend.context = backend.get_context()) or in backend.__getattr__ ? also backend.head,tail,content switching on manager.generate()?
|
||||||
|
|
||||||
# replace return_code by exit_code everywhere
|
# replace return_code by exit_code everywhere
|
||||||
|
|
||||||
# plan.rate registry
|
|
||||||
|
|
|
@ -105,7 +105,7 @@ def execute(scripts, serialize=False, async=None):
|
||||||
async: do not join threads (overrides route.async)
|
async: do not join threads (overrides route.async)
|
||||||
"""
|
"""
|
||||||
if settings.ORCHESTRATION_DISABLE_EXECUTION:
|
if settings.ORCHESTRATION_DISABLE_EXECUTION:
|
||||||
logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION settings.')
|
logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION.')
|
||||||
return []
|
return []
|
||||||
# Execute scripts on each server
|
# Execute scripts on each server
|
||||||
executions = []
|
executions = []
|
||||||
|
@ -122,6 +122,7 @@ def execute(scripts, serialize=False, async=None):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'async': async,
|
'async': async,
|
||||||
}
|
}
|
||||||
|
# we clone the connection just in case we are isolated inside a transaction
|
||||||
with db.clone(model=BackendLog) as handle:
|
with db.clone(model=BackendLog) as handle:
|
||||||
log = backend.create_log(*args, using=handle.target)
|
log = backend.create_log(*args, using=handle.target)
|
||||||
log._state.db = handle.origin
|
log._state.db = handle.origin
|
||||||
|
|
|
@ -30,6 +30,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_select_related = ('plan', 'account')
|
list_select_related = ('plan', 'account')
|
||||||
search_fields = ('account__username', 'plan__name', 'id')
|
search_fields = ('account__username', 'plan__name', 'id')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Plan, PlanAdmin)
|
admin.site.register(Plan, PlanAdmin)
|
||||||
admin.site.register(ContractedPlan, ContractedPlanAdmin)
|
admin.site.register(ContractedPlan, ContractedPlanAdmin)
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core.validators import validate_name
|
from orchestra.core.validators import validate_name
|
||||||
from orchestra.models import queryset
|
from orchestra.models import queryset
|
||||||
|
from orchestra.utils.functional import cached
|
||||||
|
from orchestra.utils.python import import_class
|
||||||
|
|
||||||
from . import rating
|
from . import settings
|
||||||
|
|
||||||
|
|
||||||
class Plan(models.Model):
|
class Plan(models.Model):
|
||||||
|
@ -66,15 +68,6 @@ class RateQuerySet(models.QuerySet):
|
||||||
|
|
||||||
|
|
||||||
class Rate(models.Model):
|
class Rate(models.Model):
|
||||||
STEP_PRICE = 'STEP_PRICE'
|
|
||||||
MATCH_PRICE = 'MATCH_PRICE'
|
|
||||||
BEST_PRICE = 'BEST_PRICE'
|
|
||||||
RATE_METHODS = {
|
|
||||||
STEP_PRICE: rating.step_price,
|
|
||||||
MATCH_PRICE: rating.match_price,
|
|
||||||
BEST_PRICE: rating.best_price,
|
|
||||||
}
|
|
||||||
|
|
||||||
service = models.ForeignKey('services.Service', verbose_name=_("service"),
|
service = models.ForeignKey('services.Service', verbose_name=_("service"),
|
||||||
related_name='rates')
|
related_name='rates')
|
||||||
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates')
|
plan = models.ForeignKey(Plan, verbose_name=_("plan"), related_name='rates')
|
||||||
|
@ -91,12 +84,18 @@ class Rate(models.Model):
|
||||||
return "{}-{}".format(str(self.price), self.quantity)
|
return "{}-{}".format(str(self.price), self.quantity)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@cached
|
||||||
def get_methods(cls):
|
def get_methods(cls):
|
||||||
return cls.RATE_METHODS
|
return dict((method, import_class(method)) for method in settings.PLANS_RATE_METHODS)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@cached
|
||||||
def get_choices(cls):
|
def get_choices(cls):
|
||||||
choices = []
|
choices = []
|
||||||
for name, method in cls.RATE_METHODS.items():
|
for name, method in cls.get_methods().items():
|
||||||
choices.append((name, method.verbose_name))
|
choices.append((name, method.verbose_name))
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default(cls):
|
||||||
|
return settings.PLANS_DEFAULT_RATE_METHOD
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
from orchestra.contrib.settings import Setting
|
||||||
|
|
||||||
|
|
||||||
|
PLANS_RATE_METHODS = Setting('PLANS_RATE_METHODS',
|
||||||
|
(
|
||||||
|
'orchestra.contrib.plans.rating.step_price',
|
||||||
|
'orchestra.contrib.plans.rating.match_price',
|
||||||
|
'orchestra.contrib.plans.rating.best_price',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PLANS_DEFAULT_RATE_METHOD = Setting('PLANS_DEFAULT_RATE_METHOD',
|
||||||
|
'orchestra.contrib.plans.rating.step_price',
|
||||||
|
)
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('services', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='rate_algorithm',
|
||||||
|
field=models.CharField(choices=[('orchestra.contrib.plans.rating.best_price', 'Best price'), ('orchestra.contrib.plans.rating.step_price', 'Step price'), ('orchestra.contrib.plans.rating.match_price', 'Match price')], help_text='Algorithm used to interprete the rating table.<br> Best price: Produces the best possible price given all active rating lines.<br> Step price: All rates with a quantity lower than the metric are applied. Nominal price will be used when initial block is missing.<br> Match price: Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm', default='orchestra.contrib.plans.rating.step_price'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='service',
|
||||||
|
name='tax',
|
||||||
|
field=models.PositiveIntegerField(choices=[(0, 'Duty free'), (21, '21%')], verbose_name='tax', default=21),
|
||||||
|
),
|
||||||
|
]
|
|
@ -127,11 +127,13 @@ class Service(models.Model):
|
||||||
(ANUAL, _("Anual data")),
|
(ANUAL, _("Anual data")),
|
||||||
),
|
),
|
||||||
default=BILLING_PERIOD)
|
default=BILLING_PERIOD)
|
||||||
rate_algorithm = models.CharField(_("rate algorithm"), max_length=16,
|
rate_algorithm = models.CharField(_("rate algorithm"), max_length=64,
|
||||||
|
choices=rate_class.get_choices(),
|
||||||
|
default=rate_class.get_default(),
|
||||||
help_text=string_concat(_("Algorithm used to interprete the rating table."), *[
|
help_text=string_concat(_("Algorithm used to interprete the rating table."), *[
|
||||||
string_concat('<br> ', method.verbose_name, ': ', method.help_text)
|
string_concat('<br> ', method.verbose_name, ': ', method.help_text)
|
||||||
for name, method in rate_class.get_methods().items()
|
for name, method in rate_class.get_methods().items()
|
||||||
]), choices=rate_class.get_choices(), default=rate_class.get_choices()[0][0])
|
]))
|
||||||
on_cancel = models.CharField(_("on cancel"), max_length=16,
|
on_cancel = models.CharField(_("on cancel"), max_length=16,
|
||||||
help_text=_("Defines the cancellation behaviour of this service."),
|
help_text=_("Defines the cancellation behaviour of this service."),
|
||||||
choices=(
|
choices=(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from functools import partial, wraps, update_wrapper
|
from functools import partial, wraps, update_wrapper
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
|
@ -15,6 +16,9 @@ from orchestra.utils.python import AttrDict, OrderedSet
|
||||||
from .utils import get_name, get_id
|
from .utils import get_name, get_id
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def keep_state(fn):
|
def keep_state(fn):
|
||||||
""" logs task on djcelery's TaskState model """
|
""" logs task on djcelery's TaskState model """
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
|
@ -30,14 +34,14 @@ def keep_state(fn):
|
||||||
try:
|
try:
|
||||||
result = fn(*args, **kwargs)
|
result = fn(*args, **kwargs)
|
||||||
except:
|
except:
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (_name, str(args), str(kwargs))
|
||||||
|
logger.error(subject)
|
||||||
|
logger.error(trace)
|
||||||
state.state = states.FAILURE
|
state.state = states.FAILURE
|
||||||
state.traceback = trace
|
state.traceback = trace
|
||||||
state.runtime = (timezone.now()-now).total_seconds()
|
state.runtime = (timezone.now()-now).total_seconds()
|
||||||
state.save()
|
state.save()
|
||||||
subject = 'EXCEPTION executing task %s(args=%s, kwargs=%s)' % (name, str(args), str(kwargs))
|
|
||||||
trace = traceback.format_exc()
|
|
||||||
logger.error(subject)
|
|
||||||
logger.error(trace)
|
|
||||||
mail_admins(subject, trace)
|
mail_admins(subject, trace)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -6,6 +6,7 @@ import re
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
|
|
||||||
|
@ -165,6 +166,10 @@ def touch(fname, mode=0o666, dir_fd=None, **kwargs):
|
||||||
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
|
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OperationLocked(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LockFile(object):
|
class LockFile(object):
|
||||||
""" File-based lock mechanism used for preventing concurrency problems """
|
""" File-based lock mechanism used for preventing concurrency problems """
|
||||||
def __init__(self, lockfile, expire=5*60, unlocked=False):
|
def __init__(self, lockfile, expire=5*60, unlocked=False):
|
||||||
|
@ -188,8 +193,8 @@ class LockFile(object):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if not self.unlocked:
|
if not self.unlocked:
|
||||||
if not self.acquire():
|
if not self.acquire():
|
||||||
raise OperationLocked('%s lock file exists and its mtime is less '
|
raise OperationLocked("%s lock file exists and its mtime is less than %s seconds" %
|
||||||
'than %s seconds' % (self.lockfile, self.expire))
|
(self.lockfile, self.expire))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
|
|
Loading…
Reference in New Issue