Replace float with decimal

Floating point calculations are inaccurate, often lose precision even
when storing into database. So we replace float with decimal.

Change-Id: I3782264015e0b0e260f0c046dc7c3ea94eab0b52
This commit is contained in:
lvdongbing 2016-06-22 21:11:09 -04:00
parent 5025328bc1
commit ff26ed98e7
9 changed files with 139 additions and 121 deletions

View File

@ -15,7 +15,9 @@ Utilities module.
''' '''
import datetime import datetime
import decimal
import random import random
import six
import string import string
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
@ -27,6 +29,7 @@ from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import strutils from oslo_utils import strutils
from oslo_utils import timeutils
from bilean.common import exception from bilean.common import exception
from bilean.common.i18n import _ from bilean.common.i18n import _
@ -156,3 +159,38 @@ def format_time(value):
value = value.replace(microsecond=0) value = value.replace(microsecond=0)
value = value.isoformat() value = value.isoformat()
return value return value
def format_time_to_seconds(t):
"""Format datetime to seconds from 1970-01-01 00:00:00 UTC."""
epoch = datetime.datetime.utcfromtimestamp(0)
if isinstance(t, datetime.datetime):
return (t - epoch).total_seconds()
if isinstance(t, six.string_types):
dt = timeutils.parse_strtime(t)
return (dt - epoch).total_seconds()
return t
def make_decimal(value):
"""Format float to decimal."""
if isinstance(value, decimal.Decimal):
return value
if isinstance(value, float):
return decimal.Decimal.from_float(value)
return decimal.Decimal(str(value))
def format_decimal(value, num=8):
"""Format decimal and keep num decimals."""
if not isinstance(value, decimal.Decimal):
value = make_decimal(value)
dec = "0.%s" % ('0' * num)
return value.quantize(decimal.Decimal(dec))
def dec2str(value):
"""Decimal to str and keep 2 decimals."""
if not isinstance(value, decimal.Decimal):
value = make_decimal(value)
return str(value.quantize(decimal.Decimal('0.00')))

View File

@ -29,10 +29,10 @@ def upgrade(migrate_engine):
sqlalchemy.String(36), sqlalchemy.String(36),
sqlalchemy.ForeignKey('policy.id'), sqlalchemy.ForeignKey('policy.id'),
nullable=True), nullable=True),
sqlalchemy.Column('balance', sqlalchemy.Float), sqlalchemy.Column('balance', sqlalchemy.Numeric(20, 8)),
sqlalchemy.Column('rate', sqlalchemy.Float), sqlalchemy.Column('rate', sqlalchemy.Numeric(20, 8)),
sqlalchemy.Column('credit', sqlalchemy.Integer), sqlalchemy.Column('credit', sqlalchemy.Integer),
sqlalchemy.Column('last_bill', sqlalchemy.DateTime), sqlalchemy.Column('last_bill', sqlalchemy.Numeric(24, 8)),
sqlalchemy.Column('status', sqlalchemy.String(255)), sqlalchemy.Column('status', sqlalchemy.String(255)),
sqlalchemy.Column('status_reason', sqlalchemy.Text), sqlalchemy.Column('status_reason', sqlalchemy.Text),
sqlalchemy.Column('created_at', sqlalchemy.DateTime), sqlalchemy.Column('created_at', sqlalchemy.DateTime),
@ -83,9 +83,9 @@ def upgrade(migrate_engine):
sqlalchemy.Column('rule_id', sqlalchemy.String(36), nullable=False), sqlalchemy.Column('rule_id', sqlalchemy.String(36), nullable=False),
sqlalchemy.Column('resource_type', sqlalchemy.String(36), sqlalchemy.Column('resource_type', sqlalchemy.String(36),
nullable=False), nullable=False),
sqlalchemy.Column('last_bill', sqlalchemy.DateTime), sqlalchemy.Column('last_bill', sqlalchemy.Numeric(24, 8)),
sqlalchemy.Column('properties', types.Dict), sqlalchemy.Column('properties', types.Dict),
sqlalchemy.Column('rate', sqlalchemy.Float, nullable=False), sqlalchemy.Column('rate', sqlalchemy.Numeric(20, 8), nullable=False),
sqlalchemy.Column('created_at', sqlalchemy.DateTime), sqlalchemy.Column('created_at', sqlalchemy.DateTime),
sqlalchemy.Column('updated_at', sqlalchemy.DateTime), sqlalchemy.Column('updated_at', sqlalchemy.DateTime),
sqlalchemy.Column('deleted_at', sqlalchemy.DateTime), sqlalchemy.Column('deleted_at', sqlalchemy.DateTime),
@ -118,10 +118,10 @@ def upgrade(migrate_engine):
sqlalchemy.Column('user_id', sqlalchemy.String(36)), sqlalchemy.Column('user_id', sqlalchemy.String(36)),
sqlalchemy.Column('resource_id', sqlalchemy.String(36)), sqlalchemy.Column('resource_id', sqlalchemy.String(36)),
sqlalchemy.Column('resource_type', sqlalchemy.String(255)), sqlalchemy.Column('resource_type', sqlalchemy.String(255)),
sqlalchemy.Column('start_time', sqlalchemy.DateTime), sqlalchemy.Column('start_time', sqlalchemy.Numeric(24, 8)),
sqlalchemy.Column('end_time', sqlalchemy.DateTime), sqlalchemy.Column('end_time', sqlalchemy.Numeric(24, 8)),
sqlalchemy.Column('rate', sqlalchemy.Float), sqlalchemy.Column('rate', sqlalchemy.Numeric(20, 8)),
sqlalchemy.Column('cost', sqlalchemy.Float), sqlalchemy.Column('cost', sqlalchemy.Numeric(20, 8)),
sqlalchemy.Column('meta_data', types.Dict), sqlalchemy.Column('meta_data', types.Dict),
mysql_engine='InnoDB', mysql_engine='InnoDB',
mysql_charset='utf8' mysql_charset='utf8'
@ -134,7 +134,7 @@ def upgrade(migrate_engine):
sqlalchemy.Column('user_id', sqlalchemy.String(36)), sqlalchemy.Column('user_id', sqlalchemy.String(36)),
sqlalchemy.Column('type', sqlalchemy.String(255)), sqlalchemy.Column('type', sqlalchemy.String(255)),
sqlalchemy.Column('timestamp', sqlalchemy.DateTime), sqlalchemy.Column('timestamp', sqlalchemy.DateTime),
sqlalchemy.Column('value', sqlalchemy.Float), sqlalchemy.Column('value', sqlalchemy.Numeric(20, 8)),
sqlalchemy.Column('meta_data', types.Dict), sqlalchemy.Column('meta_data', types.Dict),
mysql_engine='InnoDB', mysql_engine='InnoDB',
mysql_charset='utf8' mysql_charset='utf8'
@ -150,8 +150,8 @@ def upgrade(migrate_engine):
sqlalchemy.Column('action', sqlalchemy.String(255)), sqlalchemy.Column('action', sqlalchemy.String(255)),
sqlalchemy.Column('cause', sqlalchemy.String(255)), sqlalchemy.Column('cause', sqlalchemy.String(255)),
sqlalchemy.Column('owner', sqlalchemy.String(36)), sqlalchemy.Column('owner', sqlalchemy.String(36)),
sqlalchemy.Column('start_time', sqlalchemy.Float(precision='24,8')), sqlalchemy.Column('start_time', sqlalchemy.Numeric(24, 8)),
sqlalchemy.Column('end_time', sqlalchemy.Float(precision='24,8')), sqlalchemy.Column('end_time', sqlalchemy.Numeric(24, 8)),
sqlalchemy.Column('timeout', sqlalchemy.Integer), sqlalchemy.Column('timeout', sqlalchemy.Integer),
sqlalchemy.Column('inputs', types.Dict), sqlalchemy.Column('inputs', types.Dict),
sqlalchemy.Column('outputs', types.Dict), sqlalchemy.Column('outputs', types.Dict),

View File

@ -102,11 +102,10 @@ class User(BASE, BileanBase, SoftDelete, StateAware, models.TimestampMixin):
sqlalchemy.String(36), sqlalchemy.String(36),
sqlalchemy.ForeignKey('policy.id'), sqlalchemy.ForeignKey('policy.id'),
nullable=True) nullable=True)
balance = sqlalchemy.Column(sqlalchemy.Float, default=0.0) balance = sqlalchemy.Column(sqlalchemy.Numeric(20, 8), default=0.0)
rate = sqlalchemy.Column(sqlalchemy.Float, default=0.0) rate = sqlalchemy.Column(sqlalchemy.Numeric(20, 8), default=0.0)
credit = sqlalchemy.Column(sqlalchemy.Integer, default=0) credit = sqlalchemy.Column(sqlalchemy.Integer, default=0)
last_bill = sqlalchemy.Column( last_bill = sqlalchemy.Column(sqlalchemy.Numeric(24, 8))
sqlalchemy.DateTime, default=timeutils.utcnow())
class Policy(BASE, BileanBase, SoftDelete, models.TimestampMixin): class Policy(BASE, BileanBase, SoftDelete, models.TimestampMixin):
@ -146,8 +145,8 @@ class Resource(BASE, BileanBase, SoftDelete, models.TimestampMixin):
rule_id = sqlalchemy.Column(sqlalchemy.String(36), nullable=True) rule_id = sqlalchemy.Column(sqlalchemy.String(36), nullable=True)
user = relationship(User, backref=backref('resources')) user = relationship(User, backref=backref('resources'))
resource_type = sqlalchemy.Column(sqlalchemy.String(36), nullable=False) resource_type = sqlalchemy.Column(sqlalchemy.String(36), nullable=False)
rate = sqlalchemy.Column(sqlalchemy.Float, nullable=False) rate = sqlalchemy.Column(sqlalchemy.Numeric(20, 8), nullable=False)
last_bill = sqlalchemy.Column(sqlalchemy.DateTime) last_bill = sqlalchemy.Column(sqlalchemy.Numeric(24, 8))
properties = sqlalchemy.Column(types.Dict) properties = sqlalchemy.Column(types.Dict)
@ -163,8 +162,8 @@ class Action(BASE, BileanBase, StateAware, models.TimestampMixin):
action = sqlalchemy.Column(sqlalchemy.String(255)) action = sqlalchemy.Column(sqlalchemy.String(255))
cause = sqlalchemy.Column(sqlalchemy.String(255)) cause = sqlalchemy.Column(sqlalchemy.String(255))
owner = sqlalchemy.Column(sqlalchemy.String(36)) owner = sqlalchemy.Column(sqlalchemy.String(36))
start_time = sqlalchemy.Column(sqlalchemy.Float(precision='24,8')) start_time = sqlalchemy.Column(sqlalchemy.Numeric(24, 8))
end_time = sqlalchemy.Column(sqlalchemy.Float(precision='24,8')) end_time = sqlalchemy.Column(sqlalchemy.Numeric(24, 8))
timeout = sqlalchemy.Column(sqlalchemy.Integer) timeout = sqlalchemy.Column(sqlalchemy.Integer)
inputs = sqlalchemy.Column(types.Dict) inputs = sqlalchemy.Column(types.Dict)
outputs = sqlalchemy.Column(types.Dict) outputs = sqlalchemy.Column(types.Dict)
@ -212,10 +211,10 @@ class Consumption(BASE, BileanBase):
user_id = sqlalchemy.Column(sqlalchemy.String(36)) user_id = sqlalchemy.Column(sqlalchemy.String(36))
resource_id = sqlalchemy.Column(sqlalchemy.String(36)) resource_id = sqlalchemy.Column(sqlalchemy.String(36))
resource_type = sqlalchemy.Column(sqlalchemy.String(255)) resource_type = sqlalchemy.Column(sqlalchemy.String(255))
start_time = sqlalchemy.Column(sqlalchemy.DateTime) start_time = sqlalchemy.Column(sqlalchemy.Numeric(24, 8))
end_time = sqlalchemy.Column(sqlalchemy.DateTime) end_time = sqlalchemy.Column(sqlalchemy.Numeric(24, 8))
rate = sqlalchemy.Column(sqlalchemy.Float) rate = sqlalchemy.Column(sqlalchemy.Numeric(20, 8))
cost = sqlalchemy.Column(sqlalchemy.Float) cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8))
meta_data = sqlalchemy.Column(types.Dict) meta_data = sqlalchemy.Column(types.Dict)
@ -229,7 +228,7 @@ class Recharge(BASE, BileanBase):
user_id = sqlalchemy.Column(sqlalchemy.String(36)) user_id = sqlalchemy.Column(sqlalchemy.String(36))
type = sqlalchemy.Column(sqlalchemy.String(255)) type = sqlalchemy.Column(sqlalchemy.String(255))
timestamp = sqlalchemy.Column(sqlalchemy.DateTime) timestamp = sqlalchemy.Column(sqlalchemy.DateTime)
value = sqlalchemy.Column(sqlalchemy.Float) value = sqlalchemy.Column(sqlalchemy.Numeric(20, 8))
meta_data = sqlalchemy.Column(types.Dict) meta_data = sqlalchemy.Column(types.Dict)

View File

@ -21,6 +21,7 @@ from bilean.common import context as req_context
from bilean.common import exception from bilean.common import exception
from bilean.common.i18n import _ from bilean.common.i18n import _
from bilean.common.i18n import _LE from bilean.common.i18n import _LE
from bilean.common import utils
from bilean.db import api as db_api from bilean.db import api as db_api
from bilean.engine import event as EVENT from bilean.engine import event as EVENT
@ -104,8 +105,8 @@ class Action(object):
# working on the action. It also serves as a lock. # working on the action. It also serves as a lock.
self.owner = kwargs.get('owner', None) self.owner = kwargs.get('owner', None)
self.start_time = kwargs.get('start_time', None) self.start_time = utils.make_decimal(kwargs.get('start_time', 0))
self.end_time = kwargs.get('end_time', None) self.end_time = utils.make_decimal(kwargs.get('end_time', 0))
# Timeout is a placeholder in case some actions may linger too long # Timeout is a placeholder in case some actions may linger too long
self.timeout = kwargs.get('timeout', cfg.CONF.default_action_timeout) self.timeout = kwargs.get('timeout', cfg.CONF.default_action_timeout)
@ -141,8 +142,8 @@ class Action(object):
'action': self.action, 'action': self.action,
'cause': self.cause, 'cause': self.cause,
'owner': self.owner, 'owner': self.owner,
'start_time': self.start_time, 'start_time': utils.format_decimal(self.start_time),
'end_time': self.end_time, 'end_time': utils.format_decimal(self.end_time),
'timeout': self.timeout, 'timeout': self.timeout,
'status': self.status, 'status': self.status,
'status_reason': self.status_reason, 'status_reason': self.status_reason,
@ -364,8 +365,8 @@ class Action(object):
'target': self.target, 'target': self.target,
'cause': self.cause, 'cause': self.cause,
'owner': self.owner, 'owner': self.owner,
'start_time': self.start_time, 'start_time': utils.dec2str(self.start_time),
'end_time': self.end_time, 'end_time': utils.dec2str(self.end_time),
'timeout': self.timeout, 'timeout': self.timeout,
'status': self.status, 'status': self.status,
'status_reason': self.status_reason, 'status_reason': self.status_reason,

View File

@ -30,10 +30,10 @@ class Consumption(object):
self.resource_id = kwargs.get('resource_id') self.resource_id = kwargs.get('resource_id')
self.resource_type = kwargs.get('resource_type') self.resource_type = kwargs.get('resource_type')
self.start_time = kwargs.get('start_time') self.start_time = utils.make_decimal(kwargs.get('start_time', 0))
self.end_time = kwargs.get('end_time') self.end_time = utils.make_decimal(kwargs.get('end_time', 0))
self.rate = kwargs.get('rate') self.rate = utils.make_decimal(kwargs.get('rate', 0))
self.cost = kwargs.get('cost') self.cost = utils.make_decimal(kwargs.get('cost', 0))
self.metadata = kwargs.get('metadata') self.metadata = kwargs.get('metadata')
@classmethod @classmethod
@ -87,10 +87,10 @@ class Consumption(object):
'user_id': self.user_id, 'user_id': self.user_id,
'resource_id': self.resource_id, 'resource_id': self.resource_id,
'resource_type': self.resource_type, 'resource_type': self.resource_type,
'start_time': self.start_time, 'start_time': utils.format_decimal(self.start_time),
'end_time': self.end_time, 'end_time': utils.format_decimal(self.end_time),
'rate': self.rate, 'rate': utils.format_decimal(self.rate),
'cost': self.cost, 'cost': utils.format_decimal(self.cost),
'meta_data': self.metadata, 'meta_data': self.metadata,
} }
@ -109,10 +109,10 @@ class Consumption(object):
'user_id': self.user_id, 'user_id': self.user_id,
'resource_id': self.resource_id, 'resource_id': self.resource_id,
'resource_type': self.resource_type, 'resource_type': self.resource_type,
'start_time': utils.format_time(self.start_time), 'start_time': utils.dec2str(self.start_time),
'end_time': utils.format_time(self.end_time), 'end_time': utils.dec2str(self.end_time),
'rate': self.rate, 'rate': utils.dec2str(self.rate),
'cost': self.cost, 'cost': utils.dec2str(self.cost),
'metadata': self.metadata, 'metadata': self.metadata,
} }
return consumption return consumption

View File

@ -22,6 +22,7 @@ from taskflow.types import failure as ft
from bilean.common import exception from bilean.common import exception
from bilean.common.i18n import _LE from bilean.common.i18n import _LE
from bilean.common import utils
from bilean.engine import policy as policy_mod from bilean.engine import policy as policy_mod
from bilean.engine import user as user_mod from bilean.engine import user as user_mod
from bilean.plugins import base as plugin_base from bilean.plugins import base as plugin_base
@ -77,7 +78,7 @@ class CreateResourceTask(task.Task):
# Update resource with rule_id and rate # Update resource with rule_id and rate
resource.rule_id = rule.id resource.rule_id = rule.id
resource.rate = rule.get_price(resource) resource.rate = utils.make_decimal(rule.get_price(resource))
resource.store(context) resource.store(context)
def revert(self, context, resource, result, **kwargs): def revert(self, context, resource, result, **kwargs):
@ -96,7 +97,7 @@ class UpdateResourceTask(task.Task):
old_rate = resource.rate old_rate = resource.rate
resource.properties = values.get('properties') resource.properties = values.get('properties')
rule = plugin_base.Rule.load(context, rule_id=resource.rule_id) rule = plugin_base.Rule.load(context, rule_id=resource.rule_id)
resource.rate = rule.get_price(resource) resource.rate = utils.make_decimal(rule.get_price(resource))
resource.delta_rate = resource.rate - old_rate resource.delta_rate = resource.rate - old_rate
resource.store(context) resource.store(context)
@ -177,8 +178,7 @@ class UpdateUserRateTask(task.Task):
def execute(self, context, user_obj, user_bak, resource, *args, **kwargs): def execute(self, context, user_obj, user_bak, resource, *args, **kwargs):
user_obj.update_rate(context, resource.delta_rate, user_obj.update_rate(context, resource.delta_rate,
timestamp=resource.last_bill, timestamp=resource.last_bill)
delayed_cost=resource.delayed_cost)
def revert(self, context, user_obj, user_bak, resource, result, def revert(self, context, user_obj, user_bak, resource, result,
*args, **kwargs): *args, **kwargs):

View File

@ -12,6 +12,7 @@
# under the License. # under the License.
import six import six
import time
from bilean.common import exception from bilean.common import exception
from bilean.common.i18n import _ from bilean.common.i18n import _
@ -24,8 +25,8 @@ from bilean.plugins import base as plugin_base
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import timeutils
wallclock = time.time
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -38,16 +39,14 @@ class User(object):
'INIT', 'FREE', 'ACTIVE', 'WARNING', 'FREEZE', 'INIT', 'FREE', 'ACTIVE', 'WARNING', 'FREEZE',
) )
ALLOW_DELAY_TIME = 10
def __init__(self, user_id, **kwargs): def __init__(self, user_id, **kwargs):
self.id = user_id self.id = user_id
self.name = kwargs.get('name') self.name = kwargs.get('name')
self.policy_id = kwargs.get('policy_id') self.policy_id = kwargs.get('policy_id')
self.balance = kwargs.get('balance', 0) self.balance = utils.make_decimal(kwargs.get('balance', 0))
self.rate = kwargs.get('rate', 0.0) self.rate = utils.make_decimal(kwargs.get('rate', 0))
self.credit = kwargs.get('credit', 0) self.credit = kwargs.get('credit', 0)
self.last_bill = kwargs.get('last_bill') self.last_bill = utils.make_decimal(kwargs.get('last_bill', 0))
self.status = kwargs.get('status', self.INIT) self.status = kwargs.get('status', self.INIT)
self.status_reason = kwargs.get('status_reason', 'Init user') self.status_reason = kwargs.get('status_reason', 'Init user')
@ -65,10 +64,10 @@ class User(object):
values = { values = {
'name': self.name, 'name': self.name,
'policy_id': self.policy_id, 'policy_id': self.policy_id,
'balance': self.balance, 'balance': utils.format_decimal(self.balance),
'rate': self.rate, 'rate': utils.format_decimal(self.rate),
'credit': self.credit, 'credit': self.credit,
'last_bill': self.last_bill, 'last_bill': utils.format_decimal(self.last_bill),
'status': self.status, 'status': self.status,
'status_reason': self.status_reason, 'status_reason': self.status_reason,
'created_at': self.created_at, 'created_at': self.created_at,
@ -156,7 +155,7 @@ class User(object):
if not realtime: if not realtime:
return u return u
if u.rate > 0 and u.status != u.FREEZE: if u.rate > 0 and u.status != u.FREEZE:
seconds = (timeutils.utcnow() - u.last_bill).total_seconds() seconds = utils.make_decimal(wallclock()) - u.last_bill
u.balance -= u.rate * seconds u.balance -= u.rate * seconds
return u return u
@ -194,10 +193,10 @@ class User(object):
'id': self.id, 'id': self.id,
'name': self.name, 'name': self.name,
'policy_id': self.policy_id, 'policy_id': self.policy_id,
'balance': self.balance, 'balance': utils.dec2str(self.balance),
'rate': self.rate, 'rate': utils.dec2str(self.rate),
'credit': self.credit, 'credit': self.credit,
'last_bill': utils.format_time(self.last_bill), 'last_bill': utils.dec2str(self.last_bill),
'status': self.status, 'status': self.status,
'status_reason': self.status_reason, 'status_reason': self.status_reason,
'created_at': utils.format_time(self.created_at), 'created_at': utils.format_time(self.created_at),
@ -213,7 +212,7 @@ class User(object):
self.status_reason = reason self.status_reason = reason
self.store(context) self.store(context)
def update_rate(self, context, delta_rate, timestamp=None, delayed_cost=0): def update_rate(self, context, delta_rate, timestamp=None):
"""Update user's rate and update user status. """Update user's rate and update user status.
:param context: The request context. :param context: The request context.
@ -223,18 +222,15 @@ class User(object):
adjust balance by delayed_cost. adjust balance by delayed_cost.
""" """
if delta_rate == 0 and delayed_cost == 0:
return
# Settle account before update rate # Settle account before update rate
self._settle_account(context, timestamp=timestamp, self._settle_account(context, delta_rate=delta_rate,
delayed_cost=delayed_cost) timestamp=timestamp)
old_rate = self.rate old_rate = self.rate
new_rate = old_rate + delta_rate new_rate = old_rate + delta_rate
if old_rate == 0 and new_rate > 0: if old_rate == 0 and new_rate > 0:
# Set last_bill when status change to 'ACTIVE' from 'FREE' # Set last_bill when status change to 'ACTIVE' from 'FREE'
self.last_bill = timeutils.utcnow() self.last_bill = timestamp or wallclock()
reason = _("Status change to 'ACTIVE' cause resource creation.") reason = _("Status change to 'ACTIVE' cause resource creation.")
self.status = self.ACTIVE self.status = self.ACTIVE
self.status_reason = reason self.status_reason = reason
@ -262,7 +258,7 @@ class User(object):
param timestamp: Record when recharge action occurs. param timestamp: Record when recharge action occurs.
param metadata: Some other keyword. param metadata: Some other keyword.
""" """
self.balance += value self.balance += utils.make_decimal(value)
if self.status == self.INIT and self.balance > 0: if self.status == self.INIT and self.balance > 0:
self.status = self.FREE self.status = self.FREE
self.status_reason = "Recharged" self.status_reason = "Recharged"
@ -293,31 +289,31 @@ class User(object):
'bilean.scheduler.cron_scheduler', 'bilean.scheduler.cron_scheduler',
group='scheduler') group='scheduler')
prior_notify_time = cfg.CONF.scheduler.prior_notify_time * 3600 prior_notify_time = cfg.CONF.scheduler.prior_notify_time * 3600
rest_usage = prior_notify_time * self.rate rest_usage = utils.make_decimal(prior_notify_time) * self.rate
if self.balance > rest_usage: return self.balance < rest_usage
return False
return True
def do_delete(self, context): def do_delete(self, context):
db_api.user_delete(context, self.id) db_api.user_delete(context, self.id)
return True return True
def _settle_account(self, context, timestamp=None, delayed_cost=0): def _settle_account(self, context, delta_rate=0, timestamp=None):
if self.rate == 0 and delayed_cost == 0: if self.rate == 0:
LOG.info(_LI("Ignore settlement action because user is in '%s' " LOG.info(_LI("Ignore settlement action because user is in '%s' "
"status."), self.status) "status."), self.status)
return return
# Calculate user's cost before last_bill and now # Calculate user's cost between last_bill and now
cost = 0 now = utils.make_decimal(wallclock())
if self.rate > 0 and self.last_bill: delayed_cost = 0
timestamp = timestamp or timeutils.utcnow() if delta_rate != 0:
total_seconds = (timestamp - self.last_bill).total_seconds() delayed_seconds = now - timestamp
cost = self.rate * total_seconds delayed_cost = delayed_seconds * utils.make_decimal(delta_rate)
usage_seconds = now - self.last_bill
cost = self.rate * usage_seconds
total_cost = cost + delayed_cost total_cost = cost + delayed_cost
self.balance -= total_cost self.balance -= total_cost
self.last_bill = timestamp self.last_bill = now
def settle_account(self, context, task=None): def settle_account(self, context, task=None):
'''Settle account for user.''' '''Settle account for user.'''

View File

@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import time
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import timeutils from oslo_utils import timeutils
@ -22,6 +24,7 @@ from bilean.db import api as db_api
from bilean.engine import consumption as consumption_mod from bilean.engine import consumption as consumption_mod
from bilean.engine import environment from bilean.engine import environment
wallclock = time.time
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -233,8 +236,6 @@ class Resource(object):
something else. something else.
""" """
ALLOW_DELAY_TIME = 10
def __new__(cls, id, user_id, res_type, properties, **kwargs): def __new__(cls, id, user_id, res_type, properties, **kwargs):
"""Create a new resource of the appropriate class. """Create a new resource of the appropriate class.
@ -258,8 +259,8 @@ class Resource(object):
self.properties = properties self.properties = properties
self.rule_id = kwargs.get('rule_id') self.rule_id = kwargs.get('rule_id')
self.rate = kwargs.get('rate', 0) self.rate = utils.make_decimal(kwargs.get('rate', 0))
self.last_bill = kwargs.get('last_bill') self.last_bill = utils.make_decimal(kwargs.get('last_bill', 0))
self.created_at = kwargs.get('created_at') self.created_at = kwargs.get('created_at')
self.updated_at = kwargs.get('updated_at') self.updated_at = kwargs.get('updated_at')
@ -267,7 +268,6 @@ class Resource(object):
# Properties pass to user to help settle account, not store to db # Properties pass to user to help settle account, not store to db
self.delta_rate = 0 self.delta_rate = 0
self.delayed_cost = 0
self.consumption = None self.consumption = None
def store(self, context): def store(self, context):
@ -278,8 +278,8 @@ class Resource(object):
'resource_type': self.resource_type, 'resource_type': self.resource_type,
'properties': self.properties, 'properties': self.properties,
'rule_id': self.rule_id, 'rule_id': self.rule_id,
'rate': self.rate, 'rate': utils.format_decimal(self.rate),
'last_bill': self.last_bill, 'last_bill': utils.format_decimal(self.last_bill),
'created_at': self.created_at, 'created_at': self.created_at,
'updated_at': self.updated_at, 'updated_at': self.updated_at,
'deleted_at': self.deleted_at, 'deleted_at': self.deleted_at,
@ -304,19 +304,13 @@ class Resource(object):
self.created_at = resource.created_at self.created_at = resource.created_at
return return
now = timeutils.utcnow() self.last_bill = utils.make_decimal(wallclock())
self.last_bill = now
create_time = self.properties.get('created_at') create_time = self.properties.get('created_at')
if create_time is not None: if create_time is not None:
created_at = timeutils.parse_strtime(create_time) sec = utils.format_time_to_seconds(create_time)
delayed_seconds = (now - created_at).total_seconds() self.last_bill = utils.make_decimal(sec)
# Engine handle resource creation is delayed because of something,
# we suppose less than ALLOW_DELAY_TIME is acceptable.
if delayed_seconds > self.ALLOW_DELAY_TIME:
self.delayed_cost = self.delta_rate * delayed_seconds
self.last_bill = created_at
values.update(last_bill=self.last_bill) values.update(last_bill=utils.format_decimal(self.last_bill))
resource = db_api.resource_create(context, values) resource = db_api.resource_create(context, values)
self.created_at = resource.created_at self.created_at = resource.created_at
@ -326,19 +320,14 @@ class Resource(object):
return return
update_time = self.properties.get('updated_at') update_time = self.properties.get('updated_at')
now = timeutils.utcnow() updated_at = utils.make_decimal(wallclock())
updated_at = now
if update_time is not None: if update_time is not None:
updated_at = timeutils.parse_strtime(update_time) sec = utils.format_time_to_seconds(update_time)
delayed_seconds = (now - updated_at).total_seconds() updated_at = utils.make_decimal(sec)
# Engine handle resource update is delayed because of something,
# we suppose less than ALLOW_DELAY_TIME is acceptable.
if delayed_seconds > self.ALLOW_DELAY_TIME:
self.delayed_cost = self.delta_rate * delayed_seconds
# Generate consumption between lass bill and update time # Generate consumption between lass bill and update time
old_rate = self.rate - self.delta_rate old_rate = self.rate - self.delta_rate
cost = (updated_at - self.last_bill).total_seconds() * old_rate cost = (updated_at - self.last_bill) * old_rate
params = {'resource_id': self.id, params = {'resource_id': self.id,
'resource_type': self.resource_type, 'resource_type': self.resource_type,
'start_time': self.last_bill, 'start_time': self.last_bill,
@ -349,7 +338,7 @@ class Resource(object):
self.consumption = consumption_mod.Consumption(self.user_id, **params) self.consumption = consumption_mod.Consumption(self.user_id, **params)
self.last_bill = updated_at self.last_bill = updated_at
values.update(last_bill=updated_at) values.update(last_bill=utils.format_decimal(updated_at))
db_api.resource_update(context, self.id, values) db_api.resource_update(context, self.id, values)
def _delete(self, context, soft_delete=True): def _delete(self, context, soft_delete=True):
@ -359,18 +348,13 @@ class Resource(object):
return return
delete_time = self.properties.get('deleted_at') delete_time = self.properties.get('deleted_at')
now = timeutils.utcnow() deleted_at = utils.make_decimal(wallclock())
deleted_at = now
if delete_time is not None: if delete_time is not None:
deleted_at = timeutils.parse_strtime(delete_time) sec = utils.format_time_to_seconds(delete_time)
delayed_seconds = (now - deleted_at).total_seconds() deleted_at = utils.make_decimal(sec)
# Engine handle resource deletion is delayed because of something,
# we suppose less than ALLOW_DELAY_TIME is acceptable.
if delayed_seconds > self.ALLOW_DELAY_TIME:
self.delayed_cost = self.delta_rate * delayed_seconds
# Generate consumption between lass bill and delete time # Generate consumption between lass bill and delete time
cost = (deleted_at - self.last_bill).total_seconds() * self.rate cost = (deleted_at - self.last_bill) * self.rate
params = {'resource_id': self.id, params = {'resource_id': self.id,
'resource_type': self.resource_type, 'resource_type': self.resource_type,
'start_time': self.last_bill, 'start_time': self.last_bill,
@ -447,8 +431,8 @@ class Resource(object):
'resource_type': self.resource_type, 'resource_type': self.resource_type,
'properties': self.properties, 'properties': self.properties,
'rule_id': self.rule_id, 'rule_id': self.rule_id,
'rate': self.rate, 'rate': utils.dec2str(self.rate),
'last_bill': utils.format_time(self.last_bill), 'last_bill': utils.dec2str(self.last_bill),
'created_at': utils.format_time(self.created_at), 'created_at': utils.format_time(self.created_at),
'updated_at': utils.format_time(self.updated_at), 'updated_at': utils.format_time(self.updated_at),
'deleted_at': utils.format_time(self.deleted_at), 'deleted_at': utils.format_time(self.deleted_at),

View File

@ -179,7 +179,7 @@ class CronScheduler(object):
def _add_notify_job(self, user): def _add_notify_job(self, user):
if user.rate == 0: if user.rate == 0:
return False return False
total_seconds = user.balance / user.rate total_seconds = float(user.balance / user.rate)
prior_notify_time = cfg.CONF.scheduler.prior_notify_time * 3600 prior_notify_time = cfg.CONF.scheduler.prior_notify_time * 3600
notify_seconds = total_seconds - prior_notify_time notify_seconds = total_seconds - prior_notify_time
notify_seconds = notify_seconds if notify_seconds > 0 else 0 notify_seconds = notify_seconds if notify_seconds > 0 else 0
@ -198,7 +198,7 @@ class CronScheduler(object):
def _add_freeze_job(self, user): def _add_freeze_job(self, user):
if user.rate == 0: if user.rate == 0:
return False return False
total_seconds = user.balance / user.rate total_seconds = float(user.balance / user.rate)
run_date = timeutils.utcnow() + timedelta(seconds=total_seconds) run_date = timeutils.utcnow() + timedelta(seconds=total_seconds)
job_params = {'run_date': run_date} job_params = {'run_date': run_date}
job_id = self._generate_job_id(user.id, self.FREEZE) job_id = self._generate_job_id(user.id, self.FREEZE)