Trigger timing out (>90000ms) when function itself only takes 2000-4000s

Hi,
We have an ordered Database trigger in realm on insert and replace to a collection that holds products. It runs a function, validating a product, to then send it to aws via kinesis.PutRecord. Everything works fine and on inserts of ~100 products the trigger takes max 1000ms per product.

Recently we added more functionality to the function that the trigger runs. This functionality searches in other collections for data connected to the product, in order to find if any data is missing.
After adding this functionality, the function itself takes max 4s (so 4000ms) to run per product (using the UI with the exact same data that the triggers receive).
However, when the trigger now runs the function it times out, taking longer than 90000ms.
What I find really strange, (except for the fact that the function should only take about 4000ms not 90000ms to run) is that the first 3 of 100 triggers seems to run as expected, until then all the remaining triggers time out


Any idea of what could cause this issue?

Worth mentioning is that it has worked sometimes. The exact same 100 products have been inserted and the triggers have been able to finish (with the same function) without reaching the time limit. The times have however, also in that case, escalated from ~6000ms to ~60000ms after the first few trigger runs.

Very thankful for any help you could provide.

Hi @clakken, are you able to share the function and your trigger definition?

Yes, sure!
There is quite alot of code, pasting the functions in the order they are called.

Trigger definition:

{
   "id": "5fa9016e8591bc0b25ca4207",
  "name": "productCollectionTrigger",
  "type": "DATABASE",
  "config": {
      "service_name": "mongodb-atlas",
      "database": "custom-made",
      "collection": "validatedProduct",
      "operation_types": [
          "INSERT",
          "REPLACE"
      ],
      "full_document": true
  },
  "function_name": "handleProductsCustomMadeAvailability",
  "disabled": false
}

handeProductsCustomMadeAvailability:

exports = async (changeEvent, testDatabase) => {
    const { fullDocument } = changeEvent
    const { artnocol, validation } = fullDocument

    const database = testDatabase || 'custom-made'
    const cluster = context.services.get('mongodb-atlas')

    const validatedCollection = cluster
        .db(database)
        .collection('validatedProduct')
   
    let missingMeasurements = []
    // ===> START: the following is what seems to affect the execution time (but only when called through the trigger)
    try {
        const { rulegroup, lectrabody: fit, lectracuff: cufflectra } = fullDocument
        missingMeasurements = await context.functions.execute(
            'findMissingMeasurements',
            { rulegroup, fit, cufflectra },
            testDatabase
        )
    } catch (error) {
        console.error('findMissingMeasurements:', error, 'for product')
    }
    // <==== END

    const newAvailableCustomMade = validation ?
        validation.isValid &&
        validation.missingComponentTypes.length === 1 &&
        validation.missingComponentTypes.includes('size') &&
        validation.isInStock.result && 
        missingMeasurements && 
        missingMeasurements.length < 1
        : false 

    try {
        await validatedCollection.updateOne({ artnocol }, { $set: { availableCustomMade: newAvailableCustomMade, missingData: missingMeasurements} }) 
    } catch (error) {
        throw `Could not update document for product ${artnocol}: ${error}`  
    }

    try {
        var fullUpdatedDocument = await validatedCollection.findOne({ artnocol })
    } catch (error) {
        throw `Could not find updated document for product ${artnocol}: ${error}`  
    }

    // send updated availability to Kinesis -> Product service
    try {
        const {
            artnocol,
            availableCustomMade,
            lastValidatedAt,
            priceGroup,
            stock,
        } = fullUpdatedDocument

        const type = 'custom-made'
        const message = {
            sku: artnocol,
            isAvailableCustomMade: availableCustomMade,
            lastValidatedAt,
            priceGroup: priceGroup || '',
            stock: stock || 0,
        }
    
        const awsService = context.services.get('aws')
    
        const response = await awsService
            .kinesis('eu-west-1')
            .PutRecord({
                Data: JSON.stringify({ type, message }),
                StreamName: context.values.get('aws-kinesis-stream'),
                PartitionKey: '1',
            })

        if (response) {
            console.log(`Succesfully sent custom made availability to kinesis for product ${artnocol}`)
        }
    } catch (error) {
        throw `Could not send custom made availability to kinesis for product ${artnocol}: ${error}`
    }
}

findMissingMeasurements:

exports = async (input, testDatabase) => {
    const { rulegroup, fit, cufflectra }  = input

    const cluster = context.services.get('mongodb-atlas')
    const database = 'custom-made-config'

    let sizesDocument = ''
    try {
        sizesDocument = await cluster.db(database).collection('sizesPerFit').findOne({ fitno: fit })
    } catch (error) {
        throw `findMissingMeasurements: ${error}`
    }

    if (!sizesDocument) {
        throw `findMissingMeasurements: could not find sizePerFit document for fit ${fit}`
    }
    
    const measurementPromiseArray = []

    for (const size of sizesDocument.sizes) {
        measurementPromiseArray.push(context.functions.execute(
            'getDefaultMeasurements',
            {
                rulegroup,
                fitno: fit,
                size,
                cufflectra,
            },
            testDatabase
        ))
    }
    try {
        var measurementsPerSize = await Promise.all(measurementPromiseArray)
    } catch (error) {
        throw `findMissingMeasurements: getDefaultMeasurements: ${error}`
    }

    const measurementPoints = ['neck','waist','chest','length','cuffright','cuffleft', 'sleeveright', 'sleeveleft']
    const missingMeasurements = []

    for (let i = 0; i < sizesDocument.sizes.length; i++) {
        const missingMeasurementsPerSize = { size: sizesDocument.sizes[i], missingMeasurements: [] }

        for (const measurementPoint of measurementPoints) {
            if (!(measurementPoint in measurementsPerSize[i])) {
                missingMeasurementsPerSize.missingMeasurements.push(measurementPoint)
            }
        }
        if (missingMeasurementsPerSize.missingMeasurements.length > 0) {
            missingMeasurements.push(missingMeasurementsPerSize)
        }
    }
    return missingMeasurements
}

getDefaultMeasurements:

exports = async (input, testDatabase) => {
  const { rulegroup, fitno, size, cufflectra } = input

  const expectedInput = {
    rulegroup,
    fitno,
    size,
    cufflectra,
  }

  const missingInformation = Object.keys(expectedInput).reduce((acc, key) => {
    return !expectedInput[key] ? [...acc, `${key}: ${expectedInput[key]}`] : acc
  }, [])

  if (missingInformation.length) {
    throw `getDefaultMeasurements: Missing the following input: ${missingInformation.join(
      ', '
    )}`
  }

  const database = testDatabase || 'custom-made-component'
  const db = context.services.get('mongodb-atlas').db(database)
  const collection = db.collection('measurements')

  let validMeasurements = {}

  const measurementPoints = {
    NECK: 'neck',
    WAIST: 'waist',
    CHEST: 'chest',
    LENGTH: 'length',
    CUFF_RIGHT: 'cuff right',
    CUFF_LEFT: 'cuff left',
    SLEEVE_RIGHT: 'right sleeve',
    SLEEVE_LEFT: 'left sleeve',
    SHORT_SLEEVE: 'short sleeve',
  }

  // Function to remove the ugly "Cuff <LECTRA> Left/Right" from the measurementpoint.
  const removeLectraFromPoint = (measurementPoint) => {
    let pointWords = measurementPoint.split(' ')
    if (
      pointWords[0] === 'cuff' &&
      (pointWords[2] === 'right' || pointWords[2] === 'left')
    ) {
      pointWords.splice(1, 1)
    } else if (pointWords[0] === 'short') {
      pointWords.splice(2, 1)
    }
    return pointWords.join(' ')
  }

  const setMeasurementByPoint = (measurement) => {
    const actualPointName = removeLectraFromPoint(measurement.measuementpoint)
    switch (actualPointName) {
      case measurementPoints.NECK:
        validMeasurements = {
          ...validMeasurements,
          neck: measurement.defaultvalue,
        }
        break
      case measurementPoints.WAIST:
        validMeasurements = {
          ...validMeasurements,
          waist: measurement.defaultvalue,
        }
        break
      case measurementPoints.CHEST:
        validMeasurements = {
          ...validMeasurements,
          chest: measurement.defaultvalue,
        }
        break
      case measurementPoints.LENGTH:
        validMeasurements = {
          ...validMeasurements,
          length: measurement.defaultvalue,
        }
        break
      case measurementPoints.CUFF_RIGHT:
        validMeasurements = {
          ...validMeasurements,
          cuffright: measurement.defaultvalue,
        }
        break
      case measurementPoints.CUFF_LEFT:
        validMeasurements = {
          ...validMeasurements,
          cuffleft: measurement.defaultvalue,
        }
        break
      case measurementPoints.SLEEVE_RIGHT:
        validMeasurements = {
          ...validMeasurements,
          sleeveright: measurement.defaultvalue,
        }
        break
      case measurementPoints.SLEEVE_LEFT:
        validMeasurements = {
          ...validMeasurements,
          sleeveleft: measurement.defaultvalue,
        }
        break
      case measurementPoints.SHORT_SLEEVE:
        validMeasurements = {
          ...validMeasurements,
          sleeveright: measurement.defaultvalue,
          sleeveleft: measurement.defaultvalue,
        }
        break
      default:
        return
    }
  }

  try {
    const measurements = await collection
      .find({
        rulegroup,
        fitno,
        size,
      })
      .toArray()

    if (measurements && measurements.length > 0) {
      const isValidForCuffPromiseArray = []

      measurements.forEach(measurement => {
        isValidForCuffPromiseArray.push(context.functions.execute(
          'getMeasurementPointValidForCuff',
          { cufflectra },
          { measuementpoint: measurement.measuementpoint },
          testDatabase
        ))
      })

      const isValidForCuffArray = await Promise.all(isValidForCuffPromiseArray)
      
      isValidForCuffArray.forEach((isValidForCuff, index) => {
        if (isValidForCuff && isValidForCuff.result) {
          setMeasurementByPoint(measurements[index])
        }
      })
      
    } else {
      throw `getDefaultMeasurements: Cannot find any measurements matching: { rulegroup: ${rulegroup}, fitno: ${fitno}, size: ${size} }`
    }

    return validMeasurements
  } catch (error) {
    throw error
  }
}

getMeasurementPointValidForCuff:

exports = async (input, source, testDatabase) => {
  const { cufflectra, cuffgroup, fitno, rulegroup } = input
  const { measuementpoint } = source

  if (!measuementpoint) {
    return {
      error: `getMeasurementPointValidForCuff: No measurementpoint was included in the source argument`
    }
  }

  const database = testDatabase || 'custom-made-component'

  const cluster = context.services.get('mongodb-atlas')
  const db = cluster.db(database)
  const configCollection = cluster
    .db('custom-made-config')
    .collection('componentDetailPossibilities')

  const chosenCuff =
    (cufflectra || cuffgroup) &&
    (await db.collection('cuffs').findOne({
      ...(cuffgroup && { cuffgroup }),
      ...(rulegroup && { rulegroup }),
      ...(fitno && { fitno }),
      ...(cufflectra && { cufflectra }),
    }))

  // Find digits in measuementpoint
  const lectra = measuementpoint.match(/(\d+)/)

  var lectraMatch = true
  if (lectra) {
    if (chosenCuff && (cufflectra || (cuffgroup && fitno && rulegroup)))
      lectraMatch = chosenCuff.cufflectra === lectra[0]
    else
      return {
        error: `Cuff measurement can not be determined 'valid for cuff' without cuffgroup, rulegroup and fit, or cufflectra`,
      }
  }

  if (chosenCuff) {
    const cuffDetailPossibilities = await configCollection.findOne({
      componentname: 'cuff',
    })
    const componentvalue = chosenCuff[cuffDetailPossibilities.componentkey] // sleevelength  = "Short" || "Long"
    const detailoptionobjects = cuffDetailPossibilities.detailoptionobjects

    for (const object of detailoptionobjects) {
      if (object.stringValue === componentvalue) {
        var measurementPointsValidForCuff = object.measurementpoints
      }
    }
  }
  const pointMatch = measurementPointsValidForCuff?.filter((point) => {
    // Find spaces in point
    if (/\s/.test(point)) {
      const words = point.split(' ')
      return (
        measuementpoint.includes(words[0]) && measuementpoint.includes(words[1])
      )
    }
    return measuementpoint.includes(point)
  })

  return {
    result: pointMatch ? pointMatch.length > 0 && lectraMatch : false,
  }
}

Hi again,
I made a try run with the code that previously seemed to cause the time out, and without having made any changes to the code whatsoever from the previously failed runs, it now works as expected.
It does however make me feel quite unsure of realm, seeing as the same implementation can result in different outcomes…

1 Like