import { IScopedClusterClient } from 'kibana/server' ; import { CombinedJob , Detector } from '../../../common/types/anomaly_detection_jobs' ; import { ModelMemoryEstimateResponse } from '../calculate_model_memory_limit/calculate_model_memory_limit' ; import { validateModelMemoryLimit } from './validate_model_memory_limit' ; import type { MlClient } from '../../lib/ml_client' ; describe ( 'ML - validateModelMemoryLimit' , ( ) => { const mlInfoResponse = { defaults : { anomaly_detectors : { model_memory_limit : '1gb' , categorization_examples_limit : 4 , model_snapshot_retention_days : 1 , } , datafeeds : { scroll_size : 1000 , } , } , limits : { max_model_memory_limit : '30mb' , effective_max_model_memory_limit : '40mb' , } , } ; const fieldCapsResponse = { indices : [ 'cloudwatch' ] , fields : { instance : { keyword : { type : 'keyword' , searchable : true , aggregatable : true , } , } , } , } ; const cardinalitySearchResponse = { took : 8 , timed_out : false , _shards : { total : 15 , successful : 15 , skipped : 0 , failed : 0 , } , hits : { total : 1793481 , max_score : 0 , hits : [ ] , } , aggregations : { instance : { value : 77 , } , } , } ; const modelMemoryEstimateResponse = { model_memory_estimate : '40mb' , } ; interface MockAPICallResponse { estimateModelMemory ? : ModelMemoryEstimateResponse ; } const getMockMlClusterClient = ( ) : IScopedClusterClient => { const callAs = { search : ( ) => Promise . resolve ( { body : cardinalitySearchResponse } ) , fieldCaps : ( ) => Promise . resolve ( { body : fieldCapsResponse } ) , } ; return { asCurrentUser : callAs , asInternalUser : callAs , } as unknown as IScopedClusterClient ; } ; const getMockMlClient = ( { estimateModelMemory : estimateModelMemory , } = { } ) : MlClient => { const callAs = { info : ( ) => Promise . resolve ( { body : mlInfoResponse } ) , estimateModelMemory : ( ) => Promise . resolve ( { body : estimateModelMemory || modelMemoryEstimateResponse } ) , } ; return callAs as MlClient ; } ; function getJobConfig ( influencers = [ ] , detectors = [ ] ) { return { analysis_config : { detectors , influencers } , data_description : { time_field : '@timestamp' } , datafeed_config : { indices : [ ] , } , analysis_limits : { model_memory_limit : '20mb' , } , } as unknown as CombinedJob ; } function createDetectors ( numberOfDetectors ) { const dtrs = [ ] ; for ( let i = 0 ; i < numberOfDetectors ; i ++ ) { dtrs . push ( { function : 'mean' , field_name : `foo${ i }` , partition_field_name : 'instance' , } ) ; } return dtrs as Detector [ ] ; } it ( 'Called with no duration or split and mml within limit' , ( ) => { const job = getJobConfig ( ) ; const duration = undefined ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ ] ) ; } ) ; } ) ; it ( 'Called with no duration or split and mml above limit' , ( ) => { const job = getJobConfig ( ) ; const duration = undefined ; job . analysis_limits ! . model_memory_limit = '31mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_greater_than_max_mml' ] ) ; } ) ; } ) ; it ( 'Called large number of detectors, causing estimated mml to be over the max' , ( ) => { const dtrs = createDetectors ( 10 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '20mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( { estimateModelMemory : { model_memory_estimate : '66mb' } } ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'estimated_mml_greater_than_max_mml' ] ) ; } ) ; } ) ; it ( 'Called with small number of detectors, so estimated mml is under max' , ( ) => { const dtrs = createDetectors ( 2 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '30mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( { estimateModelMemory : { model_memory_estimate : '24mb' } } ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'success_mml' ] ) ; } ) ; } ) ; it ( 'Called enough detectors to cause estimated mml to be over specified mml' , ( ) => { const dtrs = createDetectors ( 2 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '10mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( { estimateModelMemory : { model_memory_estimate : '22mb' } } ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'half_estimated_mml_greater_than_mml' ] ) ; } ) ; } ) ; it ( 'Called with enough detectors to cause estimated mml to be over specified mml, no max setting' , ( ) => { const dtrs = createDetectors ( 2 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; delete mlInfoResponse . limits . max_model_memory_limit ; job . analysis_limits ! . model_memory_limit = '10mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'half_estimated_mml_greater_than_mml' ] ) ; } ) ; } ) ; it ( 'Called with no duration or split and mml above limit, no max setting' , ( ) => { const job = getJobConfig ( ) ; const duration = undefined ; job . analysis_limits ! . model_memory_limit = '31mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ ] ) ; } ) ; } ) ; it ( 'Called with no duration or split and mml above limit, no max setting, above effective max mml' , ( ) => { const job = getJobConfig ( ) ; const duration = undefined ; job . analysis_limits ! . model_memory_limit = '41mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_greater_than_effective_max_mml' ] ) ; } ) ; } ) ; it ( 'Called with small number of detectors, so estimated mml is under specified mml, no max setting' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '20mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( { estimateModelMemory : { model_memory_estimate : '19mb' } } ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'success_mml' ] ) ; } ) ; } ) ; it ( 'Called with specified invalid mml of "0mb"' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '0mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_value_invalid' ] ) ; } ) ; } ) ; it ( 'Called with specified invalid mml of "10mbananas"' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '10mbananas' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_value_invalid' ] ) ; } ) ; } ) ; it ( 'Called with specified invalid mml of "10"' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '10' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_value_invalid' ] ) ; } ) ; } ) ; it ( 'Called with specified invalid mml of "mb"' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = 'mb' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_value_invalid' ] ) ; } ) ; } ) ; it ( 'Called with specified invalid mml of "asdf"' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = 'asdf' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_value_invalid' ] ) ; } ) ; } ) ; it ( 'Called with specified invalid mml of "1023KB"' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '1023KB' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'mml_value_invalid' ] ) ; } ) ; } ) ; it ( 'Called with specified valid mml of "1024KB", still triggers a warning' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '1024KB' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'half_estimated_mml_greater_than_mml' ] ) ; } ) ; } ) ; it ( 'Called with specified valid mml of "6MB", still triggers info' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '6MB' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'half_estimated_mml_greater_than_mml' ] ) ; } ) ; } ) ; it ( 'Called with specified valid mml of "20MB", triggers success message' , ( ) => { const dtrs = createDetectors ( 1 ) ; const job = getJobConfig ( [ 'instance' ] , dtrs ) ; const duration = { start : 0 , end : 1 } ; job . analysis_limits ! . model_memory_limit = '20MB' ; return validateModelMemoryLimit ( getMockMlClusterClient ( ) , getMockMlClient ( { estimateModelMemory : { model_memory_estimate : '20mb' } } ) , job , duration ) . then ( ( messages ) => { const ids = messages . map ( ( m ) => m . id ) ; expect ( ids ) . toEqual ( [ 'success_mml' ] ) ; } ) ; } ) ; } ) ;