diff options
Diffstat (limited to 'raddb/mods-config/sql/main/postgresql/process-radacct.sql')
-rw-r--r-- | raddb/mods-config/sql/main/postgresql/process-radacct.sql | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/raddb/mods-config/sql/main/postgresql/process-radacct.sql b/raddb/mods-config/sql/main/postgresql/process-radacct.sql new file mode 100644 index 0000000..7a70724 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/process-radacct.sql @@ -0,0 +1,278 @@ +# -*- text -*- +# +# main/postgresql/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- fr_new_data_usage_period stored procedure. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the fr_new_data_usage_period SP is +-- invoked one per day just after midnight, to produce usage data with daily +-- granularity, then a reasonably accurate monthly bandwidth summary for a +-- given user could be obtained by queriing this table with: +-- +-- SELECT +-- TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-Month') AS month, +-- TRUNC(SUM(acctinputoctets)/1000/1000/1000,9) AS gb_in, +-- TRUNC(SUM(acctoutputoctets)/1000/1000/1000,9) AS gb_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end IS NOT NULL +-- GROUP BY +-- month; +-- +-- month | gb_in | gb_out +-- ----------------+-------------+-------------- +-- 2019-July | 5.782279231 | 50.545664824 +-- 2019-August | 4.230543344 | 48.523096424 +-- 2019-September | 4.847360599 | 48.631835488 +-- 2019-October | 6.456763254 | 51.686231937 +-- 2019-November | 6.362537735 | 52.385710572 +-- 2019-December | 4.301524442 | 50.762240277 +-- 2020-January | 5.436280545 | 49.067775286 +-- (7 rows) +-- +CREATE TABLE data_usage_by_period ( + username text, + period_start timestamp with time zone, + period_end timestamp with time zone, + acctinputoctets bigint, + acctoutputoctets bigint +); +ALTER TABLE data_usage_by_period ADD CONSTRAINT data_usage_by_period_pkey PRIMARY KEY (username, period_start); +CREATE INDEX data_usage_by_period_pkey_period_end ON data_usage_by_period(period_end); + + +-- +-- Stored procedure that when run with some arbitrary frequency, say +-- once per day by cron, will process the recent radacct entries to extract +-- time-windowed data containing acct{input,output}octets ("data usage") per +-- username, per period. +-- +-- Each invocation will create new rows in the data_usage_by_period tables +-- containing the data used by each user since the procedure was last invoked. +-- The intervals do not need to be identical but care should be taken to +-- ensure that the start/end of each period aligns well with any intended +-- reporting intervals. +-- +-- It can be invoked by running: +-- +-- SELECT fr_new_data_usage_period(); +-- +-- +CREATE OR REPLACE FUNCTION fr_new_data_usage_period () +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE v_start timestamp; +DECLARE v_end timestamp; +BEGIN + + SELECT COALESCE(MAX(period_end) + INTERVAL '1 SECOND', TO_TIMESTAMP(0)) INTO v_start FROM data_usage_by_period; + SELECT DATE_TRUNC('second',CURRENT_TIMESTAMP) INTO v_end; + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_start, + v_end, + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime > v_start OR + acctstoptime IS NULL + GROUP BY + username + ) AS s + ON CONFLICT ON CONSTRAINT data_usage_by_period_pkey + DO UPDATE + SET + acctinputoctets = data_usage_by_period.acctinputoctets + EXCLUDED.acctinputoctets, + acctoutputoctets = data_usage_by_period.acctoutputoctets + EXCLUDED.acctoutputoctets, + period_end = v_end; + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_end + INTERVAL '1 SECOND', + NULL::timestamp, + 0 - SUM(acctinputoctets), + 0 - SUM(acctoutputoctets) + FROM + radacct + WHERE + acctstoptime IS NULL + GROUP BY + username + ) AS s; + +END +$$; + + +-- ------------------------------------------------------ +-- - "Lightweight" Accounting-On/Off strategy resources - +-- ------------------------------------------------------ +-- +-- The following resources are for use only when the "lightweight" +-- Accounting-On/Off strategy is enabled in queries.conf. +-- +-- Instead of bulk closing the radacct sessions belonging to a reloaded NAS, +-- this strategy leaves them open and records the NAS reload time in the +-- nasreload table. +-- +-- Where applicable, the onus is on the administator to: +-- +-- * Consider the nas reload times when deriving a list of +-- active/inactive sessions, and when determining the duration of sessions +-- interrupted by a NAS reload. (Refer to the view below.) +-- +-- * Close the affected sessions out of band. (Refer to the SP below.) +-- +-- +-- The radacct_with_reloads view presents the radacct table with two additional +-- columns: acctstoptime_with_reloads and acctsessiontime_with_reloads +-- +-- Where the session isn't closed (acctstoptime IS NULL), yet it started before +-- the last reload of the NAS (radacct.acctstarttime < nasreload.reloadtime), +-- the derived columns are set based on the reload time of the NAS (effectively +-- the point in time that the session was interrupted.) +-- +CREATE VIEW radacct_with_reloads AS +SELECT + a.*, + COALESCE(a.AcctStopTime, + CASE WHEN a.AcctStartTime < n.ReloadTime THEN n.ReloadTime END + ) AS AcctStopTime_With_Reloads, + COALESCE(a.AcctSessionTime, + CASE WHEN a.AcctStopTime IS NULL AND a.AcctStartTime < n.ReloadTime THEN + EXTRACT(EPOCH FROM (n.ReloadTime - a.AcctStartTime)) + END + ) AS AcctSessionTime_With_Reloads +FROM radacct a +LEFT OUTER JOIN nasreload n USING (nasipaddress); + + +-- +-- It may be desirable to periodically "close" radacct sessions belonging to a +-- reloaded NAS, replicating the "bulk close" Accounting-On/Off behaviour, +-- just not in real time. +-- +-- The fr_radacct_close_after_reload SP will set radacct.acctstoptime to +-- nasreload.reloadtime, calculate the corresponding radacct.acctsessiontime, +-- and set acctterminatecause to "NAS reboot" for interrupted sessions. It +-- does so in batches, which avoids long-lived locks on the affected rows. +-- +-- It can be invoked as follows: +-- +-- CALL fr_radacct_close_after_reload(); +-- +-- Note: This SP requires PostgreSQL >= 11 which was the first version to +-- introduce PROCEDUREs which permit transaction control. This allows COMMIT +-- to be called to incrementally apply successive batch updates prior to the +-- end of the procedure. Prior to version 11 there exists only FUNCTIONs that +-- execute atomically. You can convert this procedure to a function, but by +-- doing so you are really no better off than performing a single, +-- long-running bulk update. +-- +-- Note: This SP walks radacct in strides of v_batch_size. It will typically +-- skip closed and ongoing sessions at a rate significantly faster than +-- 500,000 rows per second and process batched updates faster than 25,000 +-- orphaned sessions per second. If this isn't fast enough then you should +-- really consider using a custom schema that includes partitioning by +-- nasipaddress or acct{start,stop}time. +-- +CREATE OR REPLACE PROCEDURE fr_radacct_close_after_reload () +LANGUAGE plpgsql +AS $$ + +DECLARE v_a bigint; +DECLARE v_z bigint; +DECLARE v_updated bigint DEFAULT 0; +DECLARE v_last_report bigint DEFAULT 0; +DECLARE v_now bigint; +DECLARE v_last boolean DEFAULT false; +DECLARE v_rowcount integer; + +-- +-- This works for many circumstances +-- +DECLARE v_batch_size CONSTANT integer := 2500; + +BEGIN + + SELECT MIN(RadAcctId) INTO v_a FROM radacct WHERE AcctStopTime IS NULL; + + LOOP + + v_z := NULL; + SELECT RadAcctId INTO v_z FROM radacct WHERE RadAcctId > v_a ORDER BY RadAcctId OFFSET v_batch_size LIMIT 1; + + IF v_z IS NULL THEN + SELECT MAX(RadAcctId) INTO v_z FROM radacct; + v_last := true; + END IF; + + UPDATE radacct a + SET + AcctStopTime = n.reloadtime, + AcctSessionTime = EXTRACT(EPOCH FROM (n.ReloadTime - a.AcctStartTime)), + AcctTerminateCause = 'NAS reboot' + FROM nasreload n + WHERE + a.NASIPAddress = n.NASIPAddress + AND RadAcctId BETWEEN v_a AND v_z + AND AcctStopTime IS NULL + AND AcctStartTime < n.ReloadTime; + + GET DIAGNOSTICS v_rowcount := ROW_COUNT; + v_updated := v_updated + v_rowcount; + + COMMIT; -- Make the update visible + + v_a := v_z + 1; + + -- + -- Periodically report how far we've got + -- + SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) INTO v_now; + IF v_last_report != v_now OR v_last THEN + RAISE NOTICE 'RadAcctID: %; Sessions closed: %', v_z, v_updated; + v_last_report := v_now; + END IF; + + EXIT WHEN v_last; + + END LOOP; + +END +$$; + |