packages/fxa-auth-server/scripts/delete-inactive-accounts/get-inactive-account-uids.ts (229 lines of code) (raw):

#!/usr/bin/env node -r esbuild-register /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * A script to get a list of inactive account UIDs. * * For expediency, this script has very few parameters. It will rely on the * same set of enivronment variables as the auth server. */ import fs from 'fs'; import os from 'os'; import { performance } from 'perf_hooks'; import { Command } from 'commander'; import { Container } from 'typedi'; import PQueue from 'p-queue'; import { parseBooleanArg } from '../lib/args'; import { AppConfig, AuthLogger } from '../../lib/types'; import appConfig from '../../config'; import initLog from '../../lib/log'; import initRedis from '../../lib/redis'; import Token from '../../lib/tokens'; import * as random from '../../lib/crypto/random'; import { createDB } from '../../lib/db'; import oauthDb from '../../lib/oauth/db'; import { accountWhereAndOrderByQueryBuilder, hasAccessToken, hasActiveRefreshToken, hasActiveSessionToken, IsActiveFnBuilder, setDateToUTC, } from './lib'; const createFilepath = (endDate: Date) => `inactive-account-uids-${endDate.toISOString().substring(0, 10)}.csv`; const _collectPerfStatsOn = (statsMap: Map<string, number[]>) => { const sm = statsMap; return <T extends (...args) => ReturnType<T>>(name: string, fn: T) => { const stats: number[] = []; sm.set(name, stats); return async (...args: Parameters<T>) => { const start = performance.now(); const result = await fn(...args); stats.push(performance.now() - start); return result; }; }; }; const init = async () => { const program = new Command(); program .description( 'Get a list of UIDs of accounts that are considered inactive.\n\n' + 'For example, to get a list of inactive account UIDs for accounts \n' + 'created between 2015-01-01 and 2015-01-31 where the account is not \n' + 'active after 2024-10-31:\n' + ' get-inactive-account-uids.ts \\\n' + ' --start-date 2015-01-01 \\\n' + ' --end-date 2015-12-31 \\\n' + ' --active-by-date 2024-10-31' ) .option( '--dry-run [true|false]', 'Print out the number of account to be processed with the given arguments. Defaults to true.', true ) .option( '--active-by-date [date]', 'An account is considered active if it has any activity at or after this date. Optional. Defaults to the value of --end-date.', Date.parse ) .option( '--start-date [date]', 'Start of date range of account creation date, inclusive. Optional. Defaults to 2012-03-12.', Date.parse, '2012-03-12' ) .option( '--end-date [date]', 'End of date range of account creation date, inclusive.', Date.parse ) .option( '--output-path [path]', 'File path to write the list of UIDs to. Optional. Defaults to CWD and filename based on the end date.' ) .option( '--results-limit [number]', 'The number of results per accounts DB query. Defaults to 100000.', parseInt ) .option( '--concurrency [number]', 'The number inflight active checks. Defaults to 6.', parseInt ) .option('--perf-stats [true|false]', 'Print out performance stats.', false); program.parse(process.argv); const isDryRun = parseBooleanArg(program.dryRun); const startDate = setDateToUTC(program.startDate); const endDate = setDateToUTC(program.endDate); const startDateTimestamp = startDate.valueOf(); const endDateTimestamp = endDate.valueOf() + 86400000; // next day for < comparisons const activeByDateTimestamp = setDateToUTC( program.activeByDate || endDate ).valueOf(); const filepath = program.outputPath || createFilepath(endDate); const perfStats = program.perfStats ? new Map() : null; const collectPerfStatsOn = perfStats ? _collectPerfStatsOn(perfStats) : <T extends (...args) => ReturnType<T>>(_, fn: T) => fn; const config = appConfig.getProperties(); const log = initLog({ ...config.log, }); const redis = initRedis( { ...config.redis, ...config.redis.sessionTokens }, log ); const db = createDB( config, log, Token(log, config), random.base32(config.signinUnblock.codeLength) ); const fxaDb = await db.connect(config, redis); Container.set(AppConfig, config); Container.set(AuthLogger, log); const accountWhereAndOrderBy = () => accountWhereAndOrderByQueryBuilder( startDateTimestamp, endDateTimestamp, activeByDateTimestamp ); const accountQueryBuilder = () => accountWhereAndOrderBy() .select('accounts.uid') .limit(program.resultsLimit || 100000); const sessionTokensFn = fxaDb.sessions.bind(fxaDb); const refreshTokensFn = oauthDb.getRefreshTokensByUid.bind(oauthDb); const accessTokensFn = oauthDb.getAccessTokensByUid.bind(oauthDb); const checkActiveSessionToken = collectPerfStatsOn( 'Session Token Check', async (uid: string) => await hasActiveSessionToken(sessionTokensFn, uid, activeByDateTimestamp) ); const checkRefreshToken = collectPerfStatsOn( 'Refresh Token Check', async (uid: string) => await hasActiveRefreshToken(refreshTokensFn, uid, activeByDateTimestamp) ); const checkAccessToken = collectPerfStatsOn( 'Access Token Check', async (uid: string) => await hasAccessToken(accessTokensFn, uid) ); const _isActive = new IsActiveFnBuilder() .setActiveSessionTokenFn(checkActiveSessionToken) .setRefreshTokenFn(checkRefreshToken) .setAccessTokenFn(checkAccessToken) .build(); const isActive = collectPerfStatsOn('Active Status Check', _isActive); if (isDryRun) { const countQuery = accountWhereAndOrderBy().count({ total: 'accounts.uid', }); console.log(`Count query used: ${countQuery.toKnexQuery().toQuery()}`); const acctsCount: any = await countQuery.first(); console.log(`Number of accounts to be processed: ${acctsCount.total}`); return 0; } const fd = fs.openSync(filepath, 'a'); const concurrency = program.concurrency || 6; const queue = new PQueue({ concurrency, }); let hasMaxResultsCount = true; let totalRowsReturned = 0; let totalInactiveAccounts = 0; while (hasMaxResultsCount) { const accountsQuery = accountQueryBuilder(); accountsQuery.offset(totalRowsReturned); const accounts = await accountsQuery; if (!accounts.length) { hasMaxResultsCount = false; break; } const inactiveUids: string[] = []; for (const accountRecord of accounts) { await queue.onSizeLessThan(concurrency * 5); queue.add(async () => { if (!(await isActive(accountRecord.uid))) { inactiveUids.push(accountRecord.uid); } }); } await queue.onIdle(); if (inactiveUids.length) { totalInactiveAccounts += inactiveUids.length; inactiveUids.push(''); fs.writeSync(fd, inactiveUids.join(os.EOL)); } hasMaxResultsCount = accounts.length === program.resultsLimit; totalRowsReturned += accounts.length; } fs.closeSync(fd); console.log( `Processed account created during: ${startDate .toISOString() .substring(0, 10)} - ${endDate.toISOString().substring(0, 10)}` ); console.log( `Account is considered active if activity at or after: ${new Date( activeByDateTimestamp ) .toISOString() .substring(0, 10)}` ); console.log(`Total accounts processed: ${totalRowsReturned}`); console.log(`Number of inactive accounts: ${totalInactiveAccounts}`); console.log(`Inactive account UIDs written to: ${filepath}`); if (perfStats) { const stats = {}; perfStats.forEach((xs, k) => { const cols = {}; const sorted = xs.sort((a, b) => a - b); cols['Total Calls'] = sorted.length; cols['Duration (ms)'] = sorted.reduce((a, b) => a + b, 0); cols['Avg'] = sorted.length === 0 ? 0 : cols['Duration (ms)'] / cols['Total Calls']; cols['Median'] = sorted.length === 0 ? 0 : sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)]; stats[k] = cols; }); console.log('Performance Stats:'); console.table(stats); } return 0; }; if (require.main === module) { init() .catch((err: Error) => { console.error(err); process.exit(1); }) .then((exitCode: number) => process.exit(exitCode)); }