Add debounce.cancel, cancel session cleanup

This commit is contained in:
Jerko Steiner 2019-08-27 14:54:43 +07:00
parent 2256fbc1e8
commit 22f0b15e3a
5 changed files with 44 additions and 20 deletions

View File

@ -6,6 +6,7 @@ import {ITransactionManager} from '../database/ITransactionManager'
import {Session as SessionEntity} from '../entities/Session' import {Session as SessionEntity} from '../entities/Session'
import {SessionStore} from '../session/SessionStore' import {SessionStore} from '../session/SessionStore'
import {UrlWithStringQuery} from 'url' import {UrlWithStringQuery} from 'url'
import {apiLogger} from '../logger'
export interface ISessionOptions { export interface ISessionOptions {
transactionManager: ITransactionManager, transactionManager: ITransactionManager,
@ -32,7 +33,8 @@ export class SessionMiddleware implements IMiddleware {
path: params.baseUrl.path, path: params.baseUrl.path,
}, },
store: new SessionStore({ store: new SessionStore({
cleanup: 1, cleanupDelay: 60 * 1000,
logger: apiLogger,
getRepository: this.getRepository, getRepository: this.getRepository,
ttl: 1, ttl: 1,
buildSession: this.buildSession, buildSession: this.buildSession,

View File

@ -3,6 +3,7 @@ import request from 'supertest'
import {SessionStore} from './SessionStore' import {SessionStore} from './SessionStore'
import {ISession} from './ISession' import {ISession} from './ISession'
import ExpressSession from 'express-session' import ExpressSession from 'express-session'
import loggerFactory from '@rondo.dev/logger'
import { import {
createConnection, Column, Connection, Entity, Index, PrimaryColumn, createConnection, Column, Connection, Entity, Index, PrimaryColumn,
Repository, Repository,
@ -51,7 +52,8 @@ describe('SessionStore', () => {
maxAge: 10, maxAge: 10,
}, },
store: new SessionStore({ store: new SessionStore({
cleanup: 1, logger: loggerFactory.getLogger('api'),
cleanupDelay: 60 * 1000,
getRepository: () => repository, getRepository: () => repository,
ttl: 1, ttl: 1,
buildSession: (sd, s) => ({...s, extraData: 'test'}), buildSession: (sd, s) => ({...s, extraData: 'test'}),

View File

@ -2,6 +2,7 @@ import {Store} from 'express-session'
import {ISession} from './ISession' import {ISession} from './ISession'
import {Repository, LessThan} from 'typeorm' import {Repository, LessThan} from 'typeorm'
import {debounce} from '@rondo.dev/tasq' import {debounce} from '@rondo.dev/tasq'
import { ILogger } from '@rondo.dev/logger'
type SessionData = Express.SessionData type SessionData = Express.SessionData
type Callback = (err?: any, session?: SessionData) => void type Callback = (err?: any, session?: SessionData) => void
@ -9,8 +10,9 @@ type CallbackErr = (err?: any) => void
export interface ISessionStoreOptions<S extends ISession> { export interface ISessionStoreOptions<S extends ISession> {
readonly ttl: number readonly ttl: number
readonly cleanup: number readonly cleanupDelay: number
readonly getRepository: TRepositoryFactory<S> readonly getRepository: TRepositoryFactory<S>
readonly logger: ILogger,
buildSession(sessionData: SessionData, session: ISession): S buildSession(sessionData: SessionData, session: ISession): S
} }
@ -24,25 +26,27 @@ export type TRepositoryFactory<T> = () => Repository<T>
export class SessionStore<S extends ISession> extends Store { export class SessionStore<S extends ISession> extends Store {
protected readonly getRepository: TRepositoryFactory<S> protected readonly getRepository: TRepositoryFactory<S>
protected readonly cleanup: (...args: never[]) => void
readonly cleanup = debounce(async () => {
try {
const now = Date.now()
// this method is debounced because is caused deadlock errors in tests.
// Be wary of future problems. Debounce should fix it but this still
// needs to be thorughly tested. The problem is a the delete statement
// which locks the whole table.
await this.getRepository().delete({
expiredAt: LessThan(now),
} as any)
} catch (err) {
this.options.logger.error('Error cleaning sessions: %s', err.stack)
}
}, 1000)
constructor( constructor(
protected readonly options: ISessionStoreOptions<S>, protected readonly options: ISessionStoreOptions<S>,
) { ) {
super() super()
this.getRepository = options.getRepository this.getRepository = options.getRepository
this.cleanup = debounce(async () => {
try {
const now = Date.now()
// FIXME causes deadlocks in tests
await this.getRepository().delete({
expiredAt: LessThan(now),
} as any)
} catch (err) {
console.log('error cleaning sessions', err)
}
}, 1000)
} }
protected async promiseToCallback<T>( protected async promiseToCallback<T>(
@ -75,6 +79,8 @@ export class SessionStore<S extends ISession> extends Store {
} }
set = (sid: string, session: SessionData, callback?: CallbackErr) => { set = (sid: string, session: SessionData, callback?: CallbackErr) => {
this.cleanup.cancel()
const promise = Promise.resolve() const promise = Promise.resolve()
.then(() => this.saveSession( .then(() => this.saveSession(
this.options.buildSession(session, { this.options.buildSession(session, {

View File

@ -38,10 +38,13 @@ export class TestUtils<T extends IRoutes> {
let context: any let context: any
beforeAll(async () => {
connection = await database.connect()
})
beforeEach(async () => { beforeEach(async () => {
context = namespace.createContext(); context = namespace.createContext();
(namespace as any).enter(context) (namespace as any).enter(context)
connection = await database.connect()
queryRunner = connection.createQueryRunner() queryRunner = connection.createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
namespace.set(TRANSACTION_ID, shortid()) namespace.set(TRANSACTION_ID, shortid())
@ -56,10 +59,13 @@ export class TestUtils<T extends IRoutes> {
} }
await queryRunner.release() await queryRunner.release()
namespace.set(TRANSACTION_ID, undefined) namespace.set(TRANSACTION_ID, undefined)
namespace.set(ENTITY_MANAGER, undefined) namespace.set(ENTITY_MANAGER, undefined);
await connection.close();
(namespace as any).exit(context) (namespace as any).exit(context)
}) })
afterAll(async () => {
await database.close()
})
} }
async createRole(name: string) { async createRole(name: string) {

View File

@ -1,13 +1,21 @@
export function debounce<A, R>(fn: (...args: A[]) => R, delay: number) { export function debounce<A, R>(fn: (...args: A[]) => R, delay: number) {
let timeout: NodeJS.Timeout | null = null let timeout: NodeJS.Timeout | null = null
return function debounceImpl(...args: A[]): void { const cancel = () => {
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
} }
}
function debounceImpl(...args: A[]): void {
cancel()
timeout = setTimeout(() => { timeout = setTimeout(() => {
fn(...args) fn(...args)
}, delay) }, delay)
} }
debounceImpl.cancel = cancel
return debounceImpl
} }