mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 12:50:59 +00:00
delete: monthly usage limiting
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user