All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.spinnaker.front50.model.SqlStorageService.kt Maven / Gradle / Ivy

/*
 * Copyright 2019 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.netflix.spinnaker.front50.model

import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.front50.api.model.Timestamped
import com.netflix.spinnaker.front50.model.ObjectType.APPLICATION
import com.netflix.spinnaker.front50.model.ObjectType.APPLICATION_PERMISSION
import com.netflix.spinnaker.front50.model.ObjectType.DELIVERY
import com.netflix.spinnaker.front50.model.ObjectType.ENTITY_TAGS
import com.netflix.spinnaker.front50.model.ObjectType.NOTIFICATION
import com.netflix.spinnaker.front50.model.ObjectType.PIPELINE
import com.netflix.spinnaker.front50.model.ObjectType.PIPELINE_TEMPLATE
import com.netflix.spinnaker.front50.model.ObjectType.PLUGIN_INFO
import com.netflix.spinnaker.front50.model.ObjectType.PLUGIN_VERSIONS
import com.netflix.spinnaker.front50.model.ObjectType.PROJECT
import com.netflix.spinnaker.front50.model.ObjectType.SERVICE_ACCOUNT
import com.netflix.spinnaker.front50.model.ObjectType.SNAPSHOT
import com.netflix.spinnaker.front50.model.ObjectType.STRATEGY
import com.netflix.spinnaker.front50.model.sql.DefaultTableDefinition
import com.netflix.spinnaker.front50.model.sql.DeliveryTableDefinition
import com.netflix.spinnaker.front50.model.sql.PipelineStrategyTableDefinition
import com.netflix.spinnaker.front50.model.sql.PipelineTableDefinition
import com.netflix.spinnaker.front50.model.sql.ProjectTableDefinition
import com.netflix.spinnaker.front50.model.sql.transactional
import com.netflix.spinnaker.front50.model.sql.withRetry
import com.netflix.spinnaker.kork.sql.config.SqlRetryProperties
import com.netflix.spinnaker.kork.sql.routing.withPool
import com.netflix.spinnaker.kork.web.exceptions.NotFoundException
import com.netflix.spinnaker.security.AuthenticatedRequest
import java.time.Clock
import kotlin.system.measureTimeMillis
import org.jooq.DSLContext
import org.jooq.exception.SQLDialectNotSupportedException
import org.jooq.impl.DSL
import org.jooq.impl.DSL.field
import org.jooq.impl.DSL.max
import org.jooq.impl.DSL.table
import org.slf4j.LoggerFactory

class SqlStorageService(
  private val objectMapper: ObjectMapper,
  private val registry: Registry,
  private val jooq: DSLContext,
  private val clock: Clock,
  private val sqlRetryProperties: SqlRetryProperties,
  private val chunkSize: Int,
  private val poolName: String
) : StorageService, BulkStorageService, AdminOperations {

  companion object {
    private val log = LoggerFactory.getLogger(SqlStorageService::class.java)

    private val definitionsByType = mutableMapOf(
      PROJECT to ProjectTableDefinition(),
      PIPELINE to PipelineTableDefinition(),
      STRATEGY to PipelineStrategyTableDefinition(),
      PIPELINE_TEMPLATE to DefaultTableDefinition(PIPELINE_TEMPLATE, "pipeline_templates", true),
      NOTIFICATION to DefaultTableDefinition(NOTIFICATION, "notifications", true),
      SERVICE_ACCOUNT to DefaultTableDefinition(SERVICE_ACCOUNT, "service_accounts", true),
      APPLICATION to DefaultTableDefinition(APPLICATION, "applications", true),
      APPLICATION_PERMISSION to DefaultTableDefinition(APPLICATION_PERMISSION, "application_permissions", true),
      SNAPSHOT to DefaultTableDefinition(SNAPSHOT, "snapshots", false),
      ENTITY_TAGS to DefaultTableDefinition(ENTITY_TAGS, "entity_tags", false),
      DELIVERY to DeliveryTableDefinition(),
      PLUGIN_INFO to DefaultTableDefinition(PLUGIN_INFO, "plugin_info", false),
      PLUGIN_VERSIONS to DefaultTableDefinition(PLUGIN_VERSIONS, "plugin_versions", false)
    )

    private val bodyField = field("body", String::class.java)
    private val lastModifiedField = field("last_modified_at", Long::class.java)
  }

  override fun supportsVersioning(): Boolean {
    return true
  }

  override fun  loadObject(objectType: ObjectType, objectKey: String): T {
    val result = withPool(poolName) {
      jooq.withRetry(sqlRetryProperties.reads) { ctx ->
        ctx
          .select(
            field("body", String::class.java),
            field("created_at", Long::class.java)
          )
          .from(definitionsByType[objectType]!!.tableName)
          .where(
            field("id", String::class.java).eq(objectKey).and(
              DSL.field("is_deleted", Boolean::class.java).eq(false)
            )
          )
          .fetchOne()
      } ?: throw NotFoundException("Object not found (key: $objectKey)")
    }

    return objectMapper.readValue(
      result.get(field("body", String::class.java)),
      objectType.clazz as Class
    ).apply {
      this.createdAt = result.get(field("created_at", Long::class.java))
    }
  }

  override fun  loadObjects(objectType: ObjectType, objectKeys: List): List {
    val objects = mutableListOf()

    val timeToLoadObjects = measureTimeMillis {
      objects.addAll(
        objectKeys.chunked(chunkSize).flatMap { keys ->
          val records = withPool(poolName) {
            jooq.withRetry(sqlRetryProperties.reads) { ctx ->
              ctx
                .select(
                  field("body", String::class.java),
                  field("created_at", Long::class.java),
                  field("last_modified_at", Long::class.java)
                )
                .from(definitionsByType[objectType]!!.tableName)
                .where(
                  field("id", String::class.java).`in`(keys).and(
                    DSL.field("is_deleted", Boolean::class.java).eq(false)
                  )
                )
                .fetch()
            }
          }

          records.map {
            objectMapper.readValue(
              it.getValue(field("body", String::class.java)),
              objectType.clazz as Class
            ).apply {
              this.createdAt = it.getValue(field("created_at", Long::class.java))

              this.lastModified = it.getValue(field("last_modified_at", Long::class.java))
            }
          }
        }
      )
    }

    log.debug(
      "Took {}ms to fetch {} objects for {}",
      timeToLoadObjects,
      objects.size,
      objectType
    )

    return objects
  }

  override fun deleteObject(objectType: ObjectType, objectKey: String) {
    withPool(poolName) {
      jooq.transactional(sqlRetryProperties.transactions) { ctx ->
        if (definitionsByType[objectType]!!.supportsHistory) {
          ctx
            .update(table(definitionsByType[objectType]!!.tableName))
            .set(DSL.field("is_deleted", Boolean::class.java), true)
            .set(DSL.field("last_modified_at", Long::class.java), clock.millis())
            .where(DSL.field("id", String::class.java).eq(objectKey))
            .execute()
        } else {
          ctx
            .delete(table(definitionsByType[objectType]!!.tableName))
            .where(DSL.field("id", String::class.java).eq(objectKey))
            .execute()
        }
      }
    }
  }

  override fun  storeObjects(objectType: ObjectType, allItems: Collection) {
    // using a lower `chunkSize` to avoid exceeding default packet size limits.
    allItems.chunked(100).forEach { items ->
      try {
        withPool(poolName) {
          jooq.transactional(sqlRetryProperties.transactions) { ctx ->
            try {
              ctx.batch(
                items.map { item ->
                  val insertPairs = definitionsByType[objectType]!!.getInsertPairs(
                    objectMapper, item.id.toLowerCase(), item
                  )
                  val updatePairs = definitionsByType[objectType]!!.getUpdatePairs(insertPairs)

                  ctx.insertInto(
                    table(definitionsByType[objectType]!!.tableName),
                    *insertPairs.keys.map { DSL.field(it) }.toTypedArray()
                  )
                    .values(insertPairs.values)
                    .onConflict(DSL.field("id", String::class.java))
                    .doUpdate()
                    .set(updatePairs.mapKeys { DSL.field(it.key) })
                }
              ).execute()
            } catch (e: SQLDialectNotSupportedException) {
              for (item in items) {
                storeSingleObject(objectType, item.id.toLowerCase(), item)
              }
            }

            if (definitionsByType[objectType]!!.supportsHistory) {
              try {
                ctx.batch(
                  items.map { item ->
                    val historyPairs = definitionsByType[objectType]!!.getHistoryPairs(
                      objectMapper, clock, item.id.toLowerCase(), item
                    )

                    ctx
                      .insertInto(
                        table(definitionsByType[objectType]!!.historyTableName),
                        *historyPairs.keys.map { DSL.field(it) }.toTypedArray()
                      )
                      .values(historyPairs.values)
                      .onDuplicateKeyIgnore()
                  }
                ).execute()
              } catch (e: SQLDialectNotSupportedException) {
                for (item in items) {
                  storeSingleObjectHistory(objectType, item.id.toLowerCase(), item)
                }
              }
            }
          }
        }
      } catch (e: Exception) {
        log.error("Unable to store objects (objectType: {}, objectKeys: {})", objectType, items.map { it.id })
        throw e
      }
    }
  }

  override fun  storeObject(objectType: ObjectType, objectKey: String, item: T) {
    item.lastModifiedBy = AuthenticatedRequest.getSpinnakerUser().orElse("anonymous")

    try {
      withPool(poolName) {
        jooq.transactional(sqlRetryProperties.transactions) { ctx ->
          val insertPairs = definitionsByType[objectType]!!.getInsertPairs(objectMapper, objectKey, item)
          val updatePairs = definitionsByType[objectType]!!.getUpdatePairs(insertPairs)

          try {
            ctx
              .insertInto(
                table(definitionsByType[objectType]!!.tableName),
                *insertPairs.keys.map { DSL.field(it) }.toTypedArray()
              )
              .values(insertPairs.values)
              .onConflict(DSL.field("id", String::class.java))
              .doUpdate()
              .set(updatePairs.mapKeys { DSL.field(it.key) })
              .execute()
          } catch (e: SQLDialectNotSupportedException) {
            storeSingleObject(objectType, objectKey, item)
          }

          if (definitionsByType[objectType]!!.supportsHistory) {
            val historyPairs = definitionsByType[objectType]!!.getHistoryPairs(objectMapper, clock, objectKey, item)

            try {
              ctx
                .insertInto(
                  table(definitionsByType[objectType]!!.historyTableName),
                  *historyPairs.keys.map { DSL.field(it) }.toTypedArray()
                )
                .values(historyPairs.values)
                .onDuplicateKeyIgnore()
                .execute()
            } catch (e: SQLDialectNotSupportedException) {
              storeSingleObjectHistory(objectType, objectKey, item)
            }
          }
        }
      }
    } catch (e: Exception) {
      log.error("Unable to store object (objectType: {}, objectKey: {})", objectType, objectKey, e)
      throw e
    }
  }

  override fun listObjectKeys(objectType: ObjectType): Map {
    val startTime = System.currentTimeMillis()
    val resultSet = withPool(poolName) {
      jooq.withRetry(sqlRetryProperties.reads) { ctx ->
        ctx
          .select(
            field("id", String::class.java),
            field("last_modified_at", Long::class.java)
          )
          .from(table(definitionsByType[objectType]!!.tableName))
          .where(DSL.field("is_deleted", Boolean::class.java).eq(false))
          .fetch()
          .intoResultSet()
      }
    }

    val objectKeys = mutableMapOf()

    while (resultSet.next()) {
      objectKeys.put(resultSet.getString(1), resultSet.getLong(2))
    }

    log.debug(
      "Took {}ms to fetch {} object keys for {}",
      System.currentTimeMillis() - startTime,
      objectKeys.size,
      objectType
    )

    return objectKeys
  }

  override fun  listObjectVersions(
    objectType: ObjectType,
    objectKey: String,
    maxResults: Int
  ): List {
    if (maxResults == 1) {
      // will throw NotFoundException if object does not exist
      return mutableListOf(loadObject(objectType, objectKey))
    }

    val result = withPool(poolName) {
      jooq.withRetry(sqlRetryProperties.reads) { ctx ->
        if (definitionsByType[objectType]!!.supportsHistory) {
          ctx
            .select(bodyField, lastModifiedField)
            .from(definitionsByType[objectType]!!.historyTableName)
            .where(DSL.field("id", String::class.java).eq(objectKey))
            .orderBy(DSL.field("recorded_at").desc())
            .limit(maxResults)
            .fetch()
        } else {
          ctx
            .select(bodyField, lastModifiedField)
            .from(definitionsByType[objectType]!!.tableName)
            .where(DSL.field("id", String::class.java).eq(objectKey))
            .fetch()
        }
      }
    }

    return result.map {
      val record = objectMapper.readValue(it.get(bodyField), objectType.clazz as Class)
      record.lastModified = it.get(lastModifiedField)
      record
    }
  }

  override fun getLastModified(objectType: ObjectType): Long {
    val resultSet = withPool(poolName) {
      jooq.withRetry(sqlRetryProperties.reads) { ctx ->
        ctx
          .select(max(field("last_modified_at", Long::class.java)).`as`("last_modified_at"))
          .from(table(definitionsByType[objectType]!!.tableName))
          .fetch()
          .intoResultSet()
      }
    }

    return if (resultSet.next()) {
      return resultSet.getLong(1)
    } else {
      0
    }
  }

  override fun recover(operation: AdminOperations.Recover) {
    val objectType = ObjectType.values().find {
      it.clazz.simpleName.equals(operation.objectType, true)
    } ?: throw NotFoundException("Object type ${operation.objectType} is unsupported")

    withPool(poolName) {
      jooq.transactional(sqlRetryProperties.transactions) { ctx ->
        val updatedCount = ctx
          .update(table(definitionsByType[objectType]!!.tableName))
          .set(DSL.field("is_deleted", Boolean::class.java), false)
          .set(DSL.field("last_modified_at", Long::class.java), clock.millis())
          .where(DSL.field("id", String::class.java).eq(operation.objectId.toLowerCase()))
          .execute()

        if (updatedCount == 0) {
          throw NotFoundException("Object ${operation.objectType}:${operation.objectId} was not found")
        }
      }
    }
    log.info("Object ${operation.objectType}:${operation.objectId} was recovered")
  }

  private fun storeSingleObject(objectType: ObjectType, objectKey: String, item: Timestamped) {
    val insertPairs = definitionsByType[objectType]!!.getInsertPairs(objectMapper, objectKey, item)
    val updatePairs = definitionsByType[objectType]!!.getUpdatePairs(insertPairs)

    val exists = jooq.withRetry(sqlRetryProperties.reads) {
      jooq.fetchExists(
        jooq.select()
          .from(definitionsByType[objectType]!!.tableName)
          .where(field("id").eq(objectKey).and(field("is_deleted").eq(false)))
          .forUpdate()
      )
    }

    if (exists) {
      jooq.withRetry(sqlRetryProperties.transactions) {
        jooq
          .update(table(definitionsByType[objectType]!!.tableName)).apply {
            updatePairs.forEach { k, v ->
              set(field(k), v)
            }
          }
          .set(field("id"), objectKey) // satisfy jooq fluent interface
          .where(field("id").eq(objectKey))
          .execute()
      }
    } else {
      jooq.withRetry(sqlRetryProperties.transactions) {
        jooq
          .insertInto(
            table(definitionsByType[objectType]!!.tableName),
            *insertPairs.keys.map { DSL.field(it) }.toTypedArray()
          )
          .values(insertPairs.values)
          .execute()
      }
    }
  }

  private fun storeSingleObjectHistory(objectType: ObjectType, objectKey: String, item: Timestamped) {
    val historyPairs = definitionsByType[objectType]!!.getHistoryPairs(objectMapper, clock, objectKey, item)

    val exists = jooq.withRetry(sqlRetryProperties.reads) {
      jooq.fetchExists(
        jooq.select()
          .from(definitionsByType[objectType]!!.historyTableName)
          .where(field("id").eq(objectKey).and(field("body_sig").eq(historyPairs.getValue("body_sig"))))
          .forUpdate()
      )
    }

    if (!exists) {
      jooq.withRetry(sqlRetryProperties.transactions) {
        jooq
          .insertInto(
            table(definitionsByType[objectType]!!.historyTableName),
            *historyPairs.keys.map { DSL.field(it) }.toTypedArray()
          )
          .values(historyPairs.values)
          .execute()
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy