dynalite
Version: 
An implementation of Amazon's DynamoDB built on LevelDB
180 lines (151 loc) • 8.13 kB
JavaScript
var db = require('../db')
module.exports = function updateTable (store, data, cb) {
  var key = data.TableName, tableDb = store.tableDb
  tableDb.lock(key, function (release) {
    cb = release(cb)
    store.getTable(key, false, function (err, table) {
      if (err) return cb(err)
      var tableBillingMode = (table.BillingModeSummary || {}).BillingMode || 'PROVISIONED'
      if (data.ProvisionedThroughput && (data.BillingMode || tableBillingMode) == 'PAY_PER_REQUEST') {
        return cb(db.validationError('One or more parameter values were invalid: ' +
          'Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST'))
      }
      var updates, i, update, dataThroughput, tableThroughput, readDiff, writeDiff
      try {
        updates = getThroughputUpdates(data, table)
      }
      catch (err) {
        return cb(err)
      }
      for (i = 0; i < updates.length; i++) {
        update = updates[i]
        dataThroughput = update.dataThroughput
        tableThroughput = update.tableThroughput
        readDiff = dataThroughput.ReadCapacityUnits - tableThroughput.ReadCapacityUnits
        writeDiff = dataThroughput.WriteCapacityUnits - tableThroughput.WriteCapacityUnits
        if (!readDiff && !writeDiff)
          return cb(db.validationError(
            'The provisioned throughput for the table will not change. The requested value equals the current value. ' +
            'Current ReadCapacityUnits provisioned for the table: ' + tableThroughput.ReadCapacityUnits +
            '. Requested ReadCapacityUnits: ' + dataThroughput.ReadCapacityUnits + '. ' +
            'Current WriteCapacityUnits provisioned for the table: ' + tableThroughput.WriteCapacityUnits +
            '. Requested WriteCapacityUnits: ' + dataThroughput.WriteCapacityUnits + '. ' +
            'Refer to the Amazon DynamoDB Developer Guide for current limits and how to request higher limits.'))
        update.setStatus('UPDATING')
        if (readDiff > 0 || writeDiff > 0) tableThroughput.LastIncreaseDateTime = Date.now() / 1000
        if (readDiff < 0 || writeDiff < 0) tableThroughput.LastDecreaseDateTime = Date.now() / 1000
        update.readDiff = readDiff
        update.writeDiff = writeDiff
        if (data.BillingMode == 'PROVISIONED' && tableBillingMode != 'PROVISIONED') {
          tableThroughput.ReadCapacityUnits = dataThroughput.ReadCapacityUnits
          tableThroughput.WriteCapacityUnits = dataThroughput.WriteCapacityUnits
        }
      }
      if (data.BillingMode == 'PAY_PER_REQUEST' && tableBillingMode != 'PAY_PER_REQUEST') {
        table.TableStatus = 'UPDATING'
        table.BillingModeSummary = table.BillingModeSummary || {}
        table.BillingModeSummary.BillingMode = 'PAY_PER_REQUEST'
        table.TableThroughputModeSummary = table.TableThroughputModeSummary || {}
        table.TableThroughputModeSummary.TableThroughputMode = 'PAY_PER_REQUEST'
        table.ProvisionedThroughput = table.ProvisionedThroughput || {}
        table.ProvisionedThroughput.LastDecreaseDateTime = Date.now() / 1000
        table.ProvisionedThroughput.NumberOfDecreasesToday = table.ProvisionedThroughput.NumberOfDecreasesToday || 0
        table.ProvisionedThroughput.ReadCapacityUnits = 0
        table.ProvisionedThroughput.WriteCapacityUnits = 0
        if (table.GlobalSecondaryIndexes) {
          table.GlobalSecondaryIndexes.forEach(function (index) {
            index.IndexStatus = 'UPDATING'
            index.ProvisionedThroughput = index.ProvisionedThroughput || {}
            index.ProvisionedThroughput.NumberOfDecreasesToday = index.ProvisionedThroughput.NumberOfDecreasesToday || 0
            index.ProvisionedThroughput.ReadCapacityUnits = 0
            index.ProvisionedThroughput.WriteCapacityUnits = 0
          })
        }
      }
      else if (data.BillingMode == 'PROVISIONED' && tableBillingMode != 'PROVISIONED') {
        table.BillingModeSummary = table.BillingModeSummary || {}
        table.BillingModeSummary.BillingMode = 'PROVISIONED'
        table.TableThroughputModeSummary = table.TableThroughputModeSummary || {}
        table.TableThroughputModeSummary.TableThroughputMode = 'PROVISIONED'
      }
      tableDb.put(key, table, function (err) {
        if (err) return cb(err)
        setTimeout(function () {
          // Shouldn't need to lock/fetch as nothing should have changed
          updates.forEach(function (update) {
            dataThroughput = update.dataThroughput
            tableThroughput = update.tableThroughput
            update.setStatus('ACTIVE')
            if (update.readDiff > 0 || update.writeDiff > 0) {
              tableThroughput.LastIncreaseDateTime = Date.now() / 1000
            }
            else if (update.readDiff < 0 || update.writeDiff < 0) {
              tableThroughput.LastDecreaseDateTime = Date.now() / 1000
              tableThroughput.NumberOfDecreasesToday++
            }
            tableThroughput.ReadCapacityUnits = dataThroughput.ReadCapacityUnits
            tableThroughput.WriteCapacityUnits = dataThroughput.WriteCapacityUnits
          })
          if (data.BillingMode == 'PAY_PER_REQUEST' && tableBillingMode != 'PAY_PER_REQUEST') {
            table.TableStatus = 'ACTIVE'
            table.BillingModeSummary.LastUpdateToPayPerRequestDateTime = Date.now() / 1000
            table.TableThroughputModeSummary.LastUpdateToPayPerRequestDateTime = Date.now() / 1000
            delete table.ProvisionedThroughput.LastDecreaseDateTime
            if (table.GlobalSecondaryIndexes) {
              table.GlobalSecondaryIndexes.forEach(function (index) {
                index.IndexStatus = 'ACTIVE'
                index.ProvisionedThroughput.NumberOfDecreasesToday++
                index.ProvisionedThroughput.LastDecreaseDateTime = Date.now() / 1000
              })
            }
          }
          tableDb.put(key, table, function (err) {
            if (err && !/Database is (not open|closed)/.test(err)) console.error(err.stack || err)
          })
        }, store.options.updateTableMs)
        cb(null, { TableDescription: table })
      })
    })
  })
}
function getThroughputUpdates (data, table) {
  var tableBillingMode = (table.BillingModeSummary || {}).BillingMode || 'PROVISIONED'
  var remainingIndexes = (table.GlobalSecondaryIndexes || []).reduce(function (map, index) {
    map[index.IndexName] = true
    return map
  }, Object.create(null))
  var updates = []
  if (data.ProvisionedThroughput) {
    updates.push({
      dataThroughput: data.ProvisionedThroughput,
      tableThroughput: table.ProvisionedThroughput,
      setStatus: function (status) { table.TableStatus = status },
    })
  }
  var globalUpdates = data.GlobalSecondaryIndexUpdates || []
  if (globalUpdates.length > 5) throw db.limitError('Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per table')
  globalUpdates.forEach(function (update) {
    var dataThroughput = update.Update && update.Update.ProvisionedThroughput
    if (!dataThroughput) {
      return
    }
    if (dataThroughput.ReadCapacityUnits > 1000000000000 || dataThroughput.WriteCapacityUnits > 1000000000000) {
      throw db.validationError('This operation cannot be performed with given input values. Please contact DynamoDB service team for more info: Action Blocked: IndexUpdate')
    }
    (table.GlobalSecondaryIndexes || []).forEach(function (index) {
      if (index.IndexName == update.Update.IndexName) {
        delete remainingIndexes[index.IndexName]
        updates.push({
          dataThroughput: dataThroughput,
          tableThroughput: index.ProvisionedThroughput,
          setStatus: function (status) { index.IndexStatus = status },
        })
      }
    })
  })
  if (data.BillingMode == 'PROVISIONED' && tableBillingMode != 'PROVISIONED' && Object.keys(remainingIndexes).length) {
    throw db.validationError('One or more parameter values were invalid: ' +
      'ProvisionedThroughput must be specified for index: ' + Object.keys(remainingIndexes).join(','))
  }
  return updates
}