@ -10,6 +10,7 @@ import {ActionSummary} from "app/common/ActionSummary";
import {
import {
ApplyUAOptions ,
ApplyUAOptions ,
ApplyUAResult ,
ApplyUAResult ,
DataLimitStatus ,
DataSourceTransformed ,
DataSourceTransformed ,
ForkResult ,
ForkResult ,
ImportOptions ,
ImportOptions ,
@ -34,6 +35,7 @@ import {DocData} from 'app/common/DocData';
import { DocSnapshots } from 'app/common/DocSnapshot' ;
import { DocSnapshots } from 'app/common/DocSnapshot' ;
import { DocumentSettings } from 'app/common/DocumentSettings' ;
import { DocumentSettings } from 'app/common/DocumentSettings' ;
import { normalizeEmail } from 'app/common/emails' ;
import { normalizeEmail } from 'app/common/emails' ;
import { Features } from 'app/common/Features' ;
import { FormulaProperties , getFormulaProperties } from 'app/common/GranularAccessClause' ;
import { FormulaProperties , getFormulaProperties } from 'app/common/GranularAccessClause' ;
import { byteString , countIf , safeJsonParse } from 'app/common/gutil' ;
import { byteString , countIf , safeJsonParse } from 'app/common/gutil' ;
import { InactivityTimer } from 'app/common/InactivityTimer' ;
import { InactivityTimer } from 'app/common/InactivityTimer' ;
@ -51,6 +53,7 @@ import {Authorizer} from 'app/server/lib/Authorizer';
import { checksumFile } from 'app/server/lib/checksumFile' ;
import { checksumFile } from 'app/server/lib/checksumFile' ;
import { Client } from 'app/server/lib/Client' ;
import { Client } from 'app/server/lib/Client' ;
import { DEFAULT_CACHE_TTL , DocManager } from 'app/server/lib/DocManager' ;
import { DEFAULT_CACHE_TTL , DocManager } from 'app/server/lib/DocManager' ;
import { ICreateActiveDocOptions } from 'app/server/lib/ICreate' ;
import { makeForkIds } from 'app/server/lib/idUtils' ;
import { makeForkIds } from 'app/server/lib/idUtils' ;
import { GRIST_DOC_SQL , GRIST_DOC_WITH_TABLE1_SQL } from 'app/server/lib/initialDocSql' ;
import { GRIST_DOC_SQL , GRIST_DOC_WITH_TABLE1_SQL } from 'app/server/lib/initialDocSql' ;
import { ISandbox } from 'app/server/lib/ISandbox' ;
import { ISandbox } from 'app/server/lib/ISandbox' ;
@ -160,18 +163,21 @@ export class ActiveDoc extends EventEmitter {
private _lastMemoryMeasurement : number = 0 ; // Timestamp when memory was last measured.
private _lastMemoryMeasurement : number = 0 ; // Timestamp when memory was last measured.
private _fetchCache = new MapWithTTL < string , Promise < TableDataAction > > ( DEFAULT_CACHE_TTL ) ;
private _fetchCache = new MapWithTTL < string , Promise < TableDataAction > > ( DEFAULT_CACHE_TTL ) ;
private _rowCount? : number ;
private _rowCount? : number ;
private _productFeatures? : Features ;
private _gracePeriodStart : Date | null = null ;
// Timer for shutting down the ActiveDoc a bit after all clients are gone.
// Timer for shutting down the ActiveDoc a bit after all clients are gone.
private _inactivityTimer = new InactivityTimer ( ( ) = > this . shutdown ( ) , Deps . ACTIVEDOC_TIMEOUT * 1000 ) ;
private _inactivityTimer = new InactivityTimer ( ( ) = > this . shutdown ( ) , Deps . ACTIVEDOC_TIMEOUT * 1000 ) ;
private _recoveryMode : boolean = false ;
private _recoveryMode : boolean = false ;
private _shuttingDown : boolean = false ;
private _shuttingDown : boolean = false ;
constructor ( docManager : DocManager , docName : string , private _options ? : {
constructor ( docManager : DocManager , docName : string , private _options? : ICreateActiveDocOptions ) {
safeMode? : boolean ,
docUrl? : string
} ) {
super ( ) ;
super ( ) ;
if ( _options ? . safeMode ) { this . _recoveryMode = true ; }
if ( _options ? . safeMode ) { this . _recoveryMode = true ; }
if ( _options ? . doc ) {
this . _productFeatures = _options . doc . workspace . org . billingAccount ? . product . features ;
this . _gracePeriodStart = _options . doc . gracePeriodStart ;
}
this . _docManager = docManager ;
this . _docManager = docManager ;
this . _docName = docName ;
this . _docName = docName ;
this . docStorage = new DocStorage ( docManager . storageManager , docName ) ;
this . docStorage = new DocStorage ( docManager . storageManager , docName ) ;
@ -219,6 +225,24 @@ export class ActiveDoc extends EventEmitter {
}
}
}
}
public async getDataLimitStatus ( ) : Promise < DataLimitStatus > {
if ( this . _rowLimit && this . _rowCount ) {
const ratio = this . _rowCount / this . _rowLimit ;
if ( ratio > 1 ) {
const start = this . _gracePeriodStart ;
const days = this . _productFeatures ? . gracePeriodDays ;
if ( start && days && moment ( ) . diff ( moment ( start ) , 'days' ) >= days ) {
return 'deleteOnly' ;
} else {
return 'gracePeriod' ;
}
} else if ( ratio > 0.9 ) {
return 'approachingLimit' ;
}
}
return null ;
}
public async getUserOverride ( docSession : OptDocSession ) {
public async getUserOverride ( docSession : OptDocSession ) {
return this . _granularAccess . getUserOverride ( docSession ) ;
return this . _granularAccess . getUserOverride ( docSession ) ;
}
}
@ -900,6 +924,17 @@ export class ActiveDoc extends EventEmitter {
// Be careful not to sneak into user action queue before Calculate action, otherwise
// Be careful not to sneak into user action queue before Calculate action, otherwise
// there'll be a deadlock.
// there'll be a deadlock.
await this . waitForInitialization ( ) ;
await this . waitForInitialization ( ) ;
if (
await this . getDataLimitStatus ( ) === "deleteOnly" &&
! actions . every ( action = > [
'RemoveTable' , 'RemoveColumn' , 'RemoveRecord' , 'BulkRemoveRecord' ,
'RemoveViewSection' , 'RemoveView' , 'ApplyUndoActions' ,
] . includes ( action [ 0 ] as string ) )
) {
throw new Error ( "Document is in delete-only mode" ) ;
}
// Granular access control implemented in _applyUserActions.
// Granular access control implemented in _applyUserActions.
return await this . _applyUserActions ( docSession , actions , options ) ;
return await this . _applyUserActions ( docSession , actions , options ) ;
}
}
@ -1223,7 +1258,7 @@ export class ActiveDoc extends EventEmitter {
. . . this . getLogMeta ( docSession ) ,
. . . this . getLogMeta ( docSession ) ,
rowCount : sandboxActionBundle.rowCount
rowCount : sandboxActionBundle.rowCount
} ) ;
} ) ;
this. _rowCount = sandboxActionBundle . rowCount ;
await this . _updateRowCount ( sandboxActionBundle . rowCount ) ;
await this . _reportDataEngineMemory ( ) ;
await this . _reportDataEngineMemory ( ) ;
} else {
} else {
// Create default SandboxActionBundle to use if the data engine is not called.
// Create default SandboxActionBundle to use if the data engine is not called.
@ -1451,6 +1486,25 @@ export class ActiveDoc extends EventEmitter {
} ;
} ;
}
}
private get _rowLimit ( ) : number | undefined {
return this . _productFeatures ? . baseMaxRowsPerDocument ;
}
private async _updateGracePeriodStart ( gracePeriodStart : Date | null ) {
this . _gracePeriodStart = gracePeriodStart ;
await this . getHomeDbManager ( ) ? . setDocGracePeriodStart ( this . docName , gracePeriodStart ) ;
}
private async _updateRowCount ( rowCount : number ) {
this . _rowCount = rowCount ;
const exceedingRowLimit = this . _rowLimit && rowCount > this . _rowLimit ;
if ( exceedingRowLimit && ! this . _gracePeriodStart ) {
await this . _updateGracePeriodStart ( new Date ( ) ) ;
} else if ( ! exceedingRowLimit && this . _gracePeriodStart ) {
await this . _updateGracePeriodStart ( null ) ;
}
}
/ * *
/ * *
* Prepares a single attachment by adding it DocStorage and returns a UserAction to apply .
* Prepares a single attachment by adding it DocStorage and returns a UserAction to apply .
* /
* /