From b5848012a98b4fe32bd279ed3fdc54dda0ce9417 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 4 Apr 2025 17:18:02 -0400 Subject: [PATCH] delete: monthly usage limiting --- src/backend/src/CoreModule.js | 2 - src/backend/src/api/APIError.js | 10 - src/backend/src/routers/drivers/usage.js | 127 --------- .../src/services/drivers/DriverService.js | 36 --- .../src/services/sla/MonthlyUsageService.js | 247 ------------------ src/backend/src/services/sla/SLAService.js | 7 - 6 files changed, 429 deletions(-) delete mode 100644 src/backend/src/services/sla/MonthlyUsageService.js diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index b4d37e8de..0847124c1 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -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); diff --git a/src/backend/src/api/APIError.js b/src/backend/src/api/APIError.js index 195516662..afd1a22fe 100644 --- a/src/backend/src/api/APIError.js +++ b/src/backend/src/api/APIError.js @@ -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.', diff --git a/src/backend/src/routers/drivers/usage.js b/src/backend/src/routers/drivers/usage.js index 71b69c8ca..22d98eb39 100644 --- a/src/backend/src/routers/drivers/usage.js +++ b/src/backend/src/routers/drivers/usage.js @@ -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]); } diff --git a/src/backend/src/services/drivers/DriverService.js b/src/backend/src/services/drivers/DriverService.js index 2a15bcfbc..cf3a15eca 100644 --- a/src/backend/src/services/drivers/DriverService.js +++ b/src/backend/src/services/drivers/DriverService.js @@ -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 => { diff --git a/src/backend/src/services/sla/MonthlyUsageService.js b/src/backend/src/services/sla/MonthlyUsageService.js deleted file mode 100644 index 39f66fd46..000000000 --- a/src/backend/src/services/sla/MonthlyUsageService.js +++ /dev/null @@ -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 . - */ -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} - 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} 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} 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} 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} 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} - 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} - 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, -}; diff --git a/src/backend/src/services/sla/SLAService.js b/src/backend/src/services/sla/SLAService.js index 07339bfce..3453dfb4a 100644 --- a/src/backend/src/services/sla/SLAService.js +++ b/src/backend/src/services/sla/SLAService.js @@ -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, }, } };