gitscope
← marzneshin / v0.6.2 / app/tasks/record_usages.py
https://dev.kronoz.win/r/marzneshin.git
app/tasks/record_usages.py · Python
Blame Raw
  1import asyncio
  2from collections import defaultdict
  3from datetime import datetime
  4
  5from sqlalchemy import and_, select, insert, update, bindparam
  6
  7from app import marznode
  8from app.db import GetDB
  9from app.db.models import NodeUsage, NodeUserUsage, User
 10from app.marznode import MarzNodeBase
 11
 12
 13def record_user_usage_logs(
 14    params: list, node_id: int, consumption_factor: int = 1
 15):
 16    if not params:
 17        return
 18
 19    created_at = datetime.fromisoformat(
 20        datetime.utcnow().strftime("%Y-%m-%dT%H:00:00")
 21    )
 22
 23    with GetDB() as db:
 24        # make user usage row if doesn't exist
 25        select_stmt = select(NodeUserUsage.user_id).where(
 26            and_(
 27                NodeUserUsage.node_id == node_id,
 28                NodeUserUsage.created_at == created_at,
 29            )
 30        )
 31        existings = [r[0] for r in db.execute(select_stmt).fetchall()]
 32        uids_to_insert = set()
 33
 34        for p in params:
 35            uid = p["uid"]
 36            if uid in existings:
 37                continue
 38            uids_to_insert.add(uid)
 39
 40        if uids_to_insert:
 41            stmt = insert(NodeUserUsage).values(
 42                user_id=bindparam("uid"),
 43                created_at=created_at,
 44                node_id=node_id,
 45                used_traffic=0,
 46            )
 47            db.execute(stmt, [{"uid": uid} for uid in uids_to_insert])
 48
 49        # record
 50        stmt = (
 51            update(NodeUserUsage)
 52            .values(
 53                used_traffic=NodeUserUsage.used_traffic
 54                + bindparam("value") * consumption_factor
 55            )
 56            .where(
 57                and_(
 58                    NodeUserUsage.user_id == bindparam("uid"),
 59                    NodeUserUsage.node_id == node_id,
 60                    NodeUserUsage.created_at == created_at,
 61                )
 62            )
 63        )
 64        db.connection().execute(
 65            stmt, params, execution_options={"synchronize_session": None}
 66        )
 67        db.commit()
 68
 69
 70def record_node_stats(node_id: int, usage: int):
 71    if not usage:
 72        return
 73
 74    created_at = datetime.fromisoformat(
 75        datetime.utcnow().strftime("%Y-%m-%dT%H:00:00")
 76    )
 77
 78    with GetDB() as db:
 79
 80        # make node usage row if doesn't exist
 81        select_stmt = select(NodeUsage.node_id).where(
 82            and_(
 83                NodeUsage.node_id == node_id,
 84                NodeUsage.created_at == created_at,
 85            )
 86        )
 87        notfound = db.execute(select_stmt).first() is None
 88        if notfound:
 89            stmt = insert(NodeUsage).values(
 90                created_at=created_at, node_id=node_id, uplink=0, downlink=0
 91            )
 92            db.execute(stmt)
 93
 94        # record
 95        stmt = (
 96            update(NodeUsage)
 97            .values(
 98                downlink=NodeUsage.downlink + usage,
 99            )
100            .where(
101                and_(
102                    NodeUsage.node_id == node_id,
103                    NodeUsage.created_at == created_at,
104                )
105            )
106        )
107
108        db.execute(stmt)
109        db.commit()
110
111
112async def get_users_stats(
113    node_id: int, node: MarzNodeBase
114) -> tuple[int, list[dict]]:
115    try:
116        params = list()
117        for stat in await asyncio.wait_for(node.fetch_users_stats(), 10):
118            if stat.usage:
119                params.append({"uid": stat.uid, "value": stat.usage})
120        return node_id, params
121    except:
122        return node_id, []
123
124
125async def record_user_usages():
126    # usage_coefficient = {None: 1}  # default usage coefficient for the main api instance
127
128    results = await asyncio.gather(
129        *[
130            get_users_stats(node_id, node)
131            for node_id, node in marznode.nodes.items()
132        ]
133    )
134    api_params = {node_id: params for node_id, params in list(results)}
135
136    users_usage = defaultdict(int)
137    for node_id, params in api_params.items():
138        coefficient = (
139            node.usage_coefficient
140            if (node := marznode.nodes.get(node_id))
141            else 1
142        )
143        node_usage = 0
144        for param in params:
145            users_usage[param["uid"]] += (
146                param["value"] * coefficient
147            )  # apply the usage coefficient
148            node_usage += param["value"]
149        record_node_stats(node_id, node_usage)
150
151    users_usage = list(
152        {"id": uid, "value": value} for uid, value in users_usage.items()
153    )
154    if not users_usage:
155        return
156
157    # record users usage
158    with GetDB() as db:
159        stmt = update(User).values(
160            used_traffic=User.used_traffic + bindparam("value"),
161            lifetime_used_traffic=User.lifetime_used_traffic
162            + bindparam("value"),
163            online_at=datetime.utcnow(),
164        )
165
166        db.execute(
167            stmt, users_usage, execution_options={"synchronize_session": None}
168        )
169        db.commit()
170
171    for node_id, params in api_params.items():
172        record_user_usage_logs(
173            params,
174            node_id,
175            (
176                node.usage_coefficient
177                if (node := marznode.nodes.get(node_id))
178                else 1
179            ),
180        )