delete: monthly usage limiting

This commit is contained in:
KernelDeimos
2025-04-04 17:18:02 -04:00
parent fc677ef61f
commit b5848012a9
6 changed files with 0 additions and 429 deletions
-2
View File
@@ -110,7 +110,6 @@ const install = async ({ services, app, useapi, modapi }) => {
const { NAPIThumbnailService } = require('./services/thumbnails/NAPIThumbnailService');
const { DevConsoleService } = require('./services/DevConsoleService');
const { RateLimitService } = require('./services/sla/RateLimitService');
const { MonthlyUsageService } = require('./services/sla/MonthlyUsageService');
const { AuthService } = require('./services/auth/AuthService');
const { SLAService } = require('./services/sla/SLAService');
const { PermissionService } = require('./services/auth/PermissionService');
@@ -221,7 +220,6 @@ const install = async ({ services, app, useapi, modapi }) => {
]),
})
services.registerService('rate-limit', RateLimitService);
services.registerService('monthly-usage', MonthlyUsageService);
services.registerService('auth', AuthService);
services.registerService('permission', PermissionService);
services.registerService('sla', SLAService);
-10
View File
@@ -336,16 +336,6 @@ module.exports = class APIError {
message: ({ method_name, rate_limit }) =>
`Rate limit exceeded for method ${quot(method_name)}: ${rate_limit.max} requests per ${rate_limit.period}ms.`,
},
'monthly_limit_exceeded': {
status: 429,
message: ({ method_key, limit }) =>
`Monthly limit exceeded for method ${quot(method_key)}: ${limit} requests per month.`,
},
'monthly_usage_exceeded': {
status: 429,
message: ({ limit, unit }) =>
`Monthly limit exceeded: ${limit} ${unit} per month.`,
},
'server_rate_exceeded': {
status: 503,
message: 'System-wide rate limit exceeded. Please try again later.',
-127
View File
@@ -55,136 +55,9 @@ module.exports = eggspress('/drivers/usage', {
await svc_event.emit('usages.query', event);
usages.usages = event.usages;
const rows = await db.read(
'SELECT * FROM `service_usage_monthly` WHERE user_id=? ' +
'AND `year` = ? AND `month` = ?',
[
actor.type.user.id,
new Date().getUTCFullYear(),
new Date().getUTCMonth() + 1,
]
);
const user_is_verified = actor.type.user.email_confirmed;
for ( const row of rows ) {
const app = await get_app({ id: row.app_id });
let extra_parsed;
try {
extra_parsed = db.case({
mysql: () => row.extra,
otherwise: () => JSON.parse(row.extra),
})();
} catch ( e ) {
console.log(
'\x1B[31;1m error parsing monthly usage extra',
row.extra, e,
);
continue;
}
const identifying_fields = {
service: extra_parsed,
year: row.year,
month: row.month,
};
// EntityStorage identifiers weren't tracked properly. We don't realy need
// to track or show them, so this isn't a huge deal, but we need to make
// sure they don't populate garbage data into the usage report.
if ( ! identifying_fields.service['driver.implementation'] ) {
continue;
}
if ( identifying_fields.service['driver.interface'] === 'puter-es' ) {
continue;
}
if ( identifying_fields.service['driver.interface'] === 'puter-kvstore' ) {
continue;
}
const svc_driverUsage = req.services.get('driver-usage-policy');
const policy = await svc_driverUsage.get_effective_policy({
actor,
service_name: identifying_fields.service['driver.implementation'],
trait_name: identifying_fields.service['driver.interface'],
});
// console.log(`POLICY FOR ${identifying_fields.service['driver.implementation']} ${identifying_fields.service['driver.interface']}`, policy);
const user_usage_key = hash_serializable_object(identifying_fields);
if ( ! usages.user[user_usage_key] ) {
usages.user[user_usage_key] = {
...identifying_fields,
policy,
};
usages.user[user_usage_key].monthly_limit =
policy?.['monthly-limit'] ??
policy?.['monthy-limit'] ??
null;
if ( ! policy ) {
usages.user[user_usage_key].monthly_limit = 0;
}
}
usages.user[user_usage_key].monthly_usage =
(usages.user[user_usage_key].monthly_usage || 0) + row.count;
// const user_method_usage = usages.user.find(
// u => u.key === row.key
// );
// This will be
if ( ! app ) continue;
const app_usages = usages.apps[app.uid]
?? (usages.apps[app.uid] = {});
const id_plus_app = {
...identifying_fields,
app_uid: app.uid,
};
usages.app_objects[app.uid] = app;
const app_usage_key = hash_serializable_object(id_plus_app);
// DISABLED FOR NOW: need to rework this for the new policy system
if ( false ) if ( ! app_usages[app_usage_key] ) {
app_usages[app_usage_key] = {
...identifying_fields,
};
const method_key = row.extra['driver.implementation'] +
':' + row.extra['driver.method'];
const sla_key = `driver:impl:${method_key}`;
const svc_sla = x.get('services').get('sla');
const sla = await svc_sla.get('app_default', sla_key);
app_usages[app_usage_key].monthly_limit =
sla?.monthly_limit || null;
}
// TODO: DRY
// remove some privileged information
delete app.id;
delete app.approved_for_listing;
delete app.approved_for_opening_items;
delete app.godmode;
delete app.owner_user_id;
if ( ! app_usages[app_usage_key] ) {
app_usages[app_usage_key] = {};
}
app_usages[app_usage_key].monthly_usage =
(app_usages[app_usage_key].monthly_usage || 0) + row.count;
// usages.apps.push(usage);
}
for ( const k in usages.apps ) {
usages.apps[k] = Object.values(usages.apps[k]);
}
@@ -429,42 +429,6 @@ class DriverService extends BaseService {
return args;
},
},
{
name: 'enforce monthly usage limit',
on_call: async args => {
if ( skip_usage ) return args;
// Typo-Tolerance
if ( effective_policy?.['monthy-limit'] ) {
effective_policy['monthly-limit'] = effective_policy['monthy-limit'];
}
if ( ! effective_policy?.['monthly-limit'] ) return args;
const svc_monthlyUsage = services.get('monthly-usage');
const count = await svc_monthlyUsage.check_2(
actor, method_key, 0
);
if ( count >= effective_policy['monthly-limit'] ) {
throw APIError.create('monthly_limit_exceeded', null, {
method_key,
limit: effective_policy['monthly-limit'],
});
}
return args;
},
on_return: async result => {
if ( skip_usage ) return result;
const svc_monthlyUsage = services.get('monthly-usage');
const extra = {
'driver.interface': iface,
'driver.implementation': service_name,
'driver.method': method,
};
await svc_monthlyUsage.increment(actor, method_key, extra);
return result;
},
},
{
name: 'add metadata',
on_return: async result => {
@@ -1,247 +0,0 @@
// METADATA // {"ai-commented":{"service":"xai"}}
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const BaseService = require("../BaseService");
const { UserActorType, AppUnderUserActorType } = require("../auth/Actor");
const { DB_WRITE } = require("../database/consts");
/**
* MonthlyUsageService - A service class for managing and tracking monthly usage statistics.
*
* This class extends BaseService to provide functionalities related to:
* - Incrementing usage counts for actors (users or applications under users).
* - Checking current usage against specified criteria for both users and applications.
* - Handling different types of actors (UserActorType, AppUnderUserActorType) to ensure
* appropriate data segregation and usage limits enforcement.
*
* @extends BaseService
*/
class MonthlyUsageService extends BaseService {
/**
* Initializes the MonthlyUsageService by setting up the database connection.
*
* @note This method sets the `db` property to a write-enabled database connection for usage data.
*/
async _init () {
this.db = this.services.get('database').get(DB_WRITE, 'usage');
}
/**
* Increments the usage count for a specific actor and key.
*
* @param {Object} actor - The actor object whose usage is being tracked.
* @param {string} key - The usage key to increment.
* @param {Object} extra - Additional metadata to store with the usage record.
*
* @note This method generates a unique key based on the actor's UID and the provided key.
* @note The method performs an UPSERT operation, ensuring the count is incremented or set to 1 if new.
* @note The `extra` parameter is stringified before being stored in the database.
* @returns {Promise<void>} - A promise that resolves when the increment operation is complete.
*/
async increment (actor, key, extra) {
key = `${actor.uid}:${key}`;
const year = new Date().getUTCFullYear();
// months are zero-indexed by getUTCMonth, which could be confusing
const month = new Date().getUTCMonth() + 1;
const maybe_app_id = actor.type.app?.id;
const stringified = JSON.stringify(extra);
// UPSERT increment count
await this.db.write(
'INSERT INTO `service_usage_monthly` (`year`, `month`, `key`, `count`, `user_id`, `app_id`, `extra`) ' +
'VALUES (?, ?, ?, 1, ?, ?, ?) ' +
this.db.case({
mysql: 'ON DUPLICATE KEY UPDATE `count` = `count` + 1, `extra` = ?',
// sqlite: ' ',
otherwise: 'ON CONFLICT(`year`, `month`, `key`) ' +
'DO UPDATE SET `count` = `count` + 1, `extra` = ?',
}),
[
year, month, key, actor.type.user.id, maybe_app_id ?? null, stringified,
stringified,
]
);
}
/**
* Checks the monthly usage for the given actor based on specific criteria.
*
* This method determines the type of actor and delegates the check to the appropriate
* method for further processing. It supports both user and app-under-user actors.
*
* @param {Object} actor - The actor whose usage needs to be checked.
* @param {Object} specifiers - JSON object specifying conditions for the usage check.
* @returns {Promise<number>} The total usage count or 0 if no matching records found.
*/
async check (actor, specifiers) {
if ( actor.type instanceof UserActorType ) {
return await this._user_check(actor, specifiers);
}
if ( actor.type instanceof AppUnderUserActorType ) {
return await this._app_under_user_check(actor, specifiers);
}
}
/**
* Checks usage for an actor, routing to specific check methods based on actor type.
* @param {Object} actor - The actor to check usage for.
* @param {Object} specifiers - Additional specifiers for the usage check.
* @returns {Promise<Number>} The usage count or 0 if no usage is found.
*/
async check_2 (actor, key, ver) {
// TODO: get 'ver' working here for future updates
key = `${actor.uid}:${key}`;
if ( actor.type instanceof UserActorType ) {
return await this._user_check_2(actor, key);
}
if ( actor.type instanceof AppUnderUserActorType ) {
return await this._app_under_user_check_2(actor, key);
}
}
/**
* Performs a secondary check on usage for either user or app under user actors.
*
* @param {Object} actor - The actor performing the action.
* @param {string} key - The usage key to check.
* @param {string} ver - The version, currently not implemented for future updates.
* @returns {Promise<number>} A promise that resolves to the count of usage, or 0 if not found.
* @note The 'ver' parameter is planned for future use to handle version-specific checks.
*/
async _user_check (actor, specifiers) {
const year = new Date().getUTCFullYear();
// months are zero-indexed by getUTCMonth, which could be confusing
const month = new Date().getUTCMonth() + 1;
const rows = await this.db.read(
'SELECT SUM(`count`) AS sum FROM `service_usage_monthly` ' +
'WHERE `year` = ? AND `month` = ? AND `user_id` = ? ' +
'AND JSON_CONTAINS(`extra`, ?)',
[
year, month, actor.type.user.id,
JSON.stringify(specifiers),
]
);
return rows[0]?.sum || 0;
}
/**
* Performs a usage check for a user based on a specific key.
*
* @param {Object} actor - The actor object representing the user or app.
* @param {string} key - The unique key to check usage for.
* @returns {Promise<number>} The sum of usage count for the specified key, or 0 if not found.
* @note This method is intended for future updates where version control might be implemented.
*/
async _user_check_2 (actor, key) {
const year = new Date().getUTCFullYear();
// months are zero-indexed by getUTCMonth, which could be confusing
const month = new Date().getUTCMonth() + 1;
const rows = await this.db.read(
'SELECT SUM(`count`) AS sum FROM `service_usage_monthly` ' +
'WHERE `year` = ? AND `month` = ? AND `user_id` = ? ' +
'AND `key` = ?',
[
year, month, actor.type.user.id,
key,
]
);
return rows[0]?.sum || 0;
}
/**
* Checks the monthly usage for an app under a user account.
*
* @param {Object} actor - The actor object representing the user and app context.
* @param {Object} specifiers - An object containing usage specifiers to filter the query.
* @returns {Promise<number>} - The count of usage for the specified criteria or 0 if not found.
* @note This method queries the database for usage data specific to an app within a user's account.
* It uses JSON_CONTAINS to match specifiers within the extra field of the database entry.
*/
async _app_under_user_check (actor, specifiers) {
const year = new Date().getUTCFullYear();
// months are zero-indexed by getUTCMonth, which could be confusing
const month = new Date().getUTCMonth() + 1;
// SELECT count
const rows = await this.db.read(
'SELECT `count` FROM `service_usage_monthly` ' +
'WHERE `year` = ? AND `month` = ? AND `user_id` = ? ' +
'AND `app_id` = ? ' +
'AND JSON_CONTAINS(`extra`, ?)',
[
year, month, actor.type.user.id,
actor.type.app.id,
specifiers,
]
);
return rows[0]?.count || 0;
}
/**
* Performs a check for usage under an app, identified by a specific key.
* This method queries the database to retrieve the usage count for a given user, app, and key.
*
* @param {Actor} actor - The actor object containing user and app information.
* @param {string} key - The usage key to check against.
* @returns {Promise<number>} - A promise that resolves to the usage count or 0 if no record exists.
*/
async _app_under_user_check_2 (actor, key) {
const year = new Date().getUTCFullYear();
// months are zero-indexed by getUTCMonth, which could be confusing
const month = new Date().getUTCMonth() + 1;
// SELECT count
const rows = await this.db.read(
'SELECT `count` FROM `service_usage_monthly` ' +
'WHERE `year` = ? AND `month` = ? AND `user_id` = ? ' +
'AND `app_id` = ? ' +
'AND `key` = ?',
[
year, month, actor.type.user.id,
actor.type.app.id,
key,
]
);
return rows[0]?.count || 0;
}
}
module.exports = {
MonthlyUsageService,
};
@@ -57,7 +57,6 @@ class SLAService extends BaseService {
max: 10,
period: 30000,
},
monthly_limit: 80 * 1000,
},
},
// app_default: {
@@ -89,21 +88,18 @@ class SLAService extends BaseService {
max: 40,
period: 30000,
},
monthly_limit: 20,
},
'driver:impl:public-openai-chat-completion:complete': {
rate_limit: {
max: 40,
period: 30000,
},
monthly_limit: 100,
},
'driver:impl:public-openai-image-generation:generate': {
rate_limit: {
max: 40,
period: 30000,
},
monthly_limit: 4,
},
},
user_verified: {
@@ -112,21 +108,18 @@ class SLAService extends BaseService {
max: 40,
period: 30000,
},
monthly_limit: 100,
},
'driver:impl:public-openai-chat-completion:complete': {
rate_limit: {
max: 40,
period: 30000,
},
monthly_limit: 3000,
},
'driver:impl:public-openai-image-generation:generate': {
rate_limit: {
max: 40,
period: 30000,
},
monthly_limit: 5,
},
}
};