Improve MessageDigest and Mac instance creation to solve lock contention [DPP-956] (#13221)

* Improve MessageDigest and Mac instance creation to solve lock contention problem [DPP-956]

changelog_begin
Scalability bottleneck in regard to hashing has been fixed in multiple places.
changelog_end
This commit is contained in:
Sergey Kisel 2022-03-09 22:15:32 +01:00 committed by GitHub
parent 1a66bb998b
commit 9a27edd656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 329 additions and 42 deletions

View File

@ -29,8 +29,8 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar")
name = "daml_lf_%s_archive_proto_srcs" % version,
srcs = glob([
"src/{directory}/protobuf/com/daml/daml_lf_{version}/*.proto".format(
version = mangled_version,
directory = directory,
version = mangled_version,
),
]),
),
@ -105,6 +105,7 @@ da_scala_library(
":daml_lf_1.dev_archive_proto_java",
"//daml-lf/data",
"//daml-lf/language",
"//libs-scala/crypto",
"//libs-scala/nameof",
"//libs-scala/scala-utils",
"@maven//:com_google_protobuf_protobuf_java",

View File

@ -3,12 +3,11 @@
package com.daml.lf.archive
import com.daml.crypto.MessageDigestPrototype
import com.daml.daml_lf_dev.DamlLf
import com.daml.lf.data.Ref.PackageId
import com.daml.lf.language.{LanguageMajorVersion, LanguageVersion}
import java.security.MessageDigest
case class ArchivePayload(
pkgId: PackageId,
proto: DamlLf.ArchivePayload,
@ -27,8 +26,7 @@ object Reader {
.fromString(lf.getHash)
.left
.map(err => Error.Parsing("Invalid hash: " + err))
ourHash = MessageDigest
.getInstance("SHA-256")
ourHash = MessageDigestPrototype.SHA_256.newDigest
.digest(lf.getPayload.toByteArray)
.map("%02x" format _)
.mkString

View File

@ -26,6 +26,7 @@ da_scala_library(
"//visibility:public",
],
deps = [
"//libs-scala/crypto",
"//libs-scala/logging-entries",
"//libs-scala/scala-utils",
"@maven//:com_google_guava_guava",

View File

@ -4,10 +4,8 @@
package com.daml.lf
package data
import java.security.MessageDigest
import com.daml.crypto.MessageDigestPrototype
import com.daml.scalautil.Statement.discard
import com.google.common.io.BaseEncoding
import com.google.protobuf.ByteString
import scalaz.Order
@ -40,7 +38,7 @@ object Utf8 {
Bytes.fromByteString(ByteString.copyFromUtf8(s))
def sha256(s: String): String = {
val digest = MessageDigest.getInstance("SHA-256")
val digest = MessageDigestPrototype.SHA_256.newDigest
digest.update(getBytes(s).toByteBuffer)
BaseEncoding.base16().lowerCase().encode(digest.digest())
}

View File

@ -31,6 +31,7 @@ da_scala_library(
"//daml-lf/archive:daml_lf_archive_reader",
"//daml-lf/data",
"//daml-lf/language",
"//libs-scala/crypto",
"//libs-scala/safe-proto",
"@maven//:com_google_protobuf_protobuf_java",
],

View File

@ -5,12 +5,11 @@ package com.daml.lf
package archive.testing
import com.daml.SafeProto
import java.security.MessageDigest
import com.daml.crypto.MessageDigestPrototype
import com.daml.daml_lf_dev.{DamlLf => PLF}
import com.daml.lf.data.Ref.PackageId
import com.daml.lf.language.Ast.Package
import com.daml.lf.language.{LanguageMajorVersion, LanguageVersion}
import com.daml.daml_lf_dev.{DamlLf => PLF}
// Important: do not use this in production code. It is designed for testing only.
object Encode {
@ -39,7 +38,10 @@ object Encode {
val payload = data.assertRight(SafeProto.toByteString(encodePayloadOfVersion(pkg, version)))
val hash = PackageId.assertFromString(
MessageDigest.getInstance("SHA-256").digest(payload.toByteArray).map("%02x" format _).mkString
MessageDigestPrototype.SHA_256.newDigest
.digest(payload.toByteArray)
.map("%02x" format _)
.mkString
)
PLF.Archive

View File

@ -57,6 +57,7 @@ da_scala_library(
":value_proto_java",
"//daml-lf/data",
"//daml-lf/language",
"//libs-scala/crypto",
"//libs-scala/nameof",
"//libs-scala/safe-proto",
"//libs-scala/scala-utils",

View File

@ -4,17 +4,17 @@
package com.daml.lf
package crypto
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.concurrent.atomic.AtomicLong
import com.daml.crypto.{MacPrototype, MessageDigestPrototype}
import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicLong
import com.daml.lf.data.{Bytes, ImmArray, Ref, Time, Utf8}
import com.daml.lf.value.Value
import com.daml.scalautil.Statement.discard
import scalaz.Order
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import scala.util.control.NoStackTrace
final class Hash private (val bytes: Bytes) {
@ -239,7 +239,7 @@ object Hash {
cid2Bytes: Value.ContractId => Bytes,
): Builder = new Builder(cid2Bytes) {
private val md = MessageDigest.getInstance("SHA-256")
private val md = MessageDigestPrototype.SHA_256.newDigest
override protected def update(a: ByteBuffer): Unit =
md.update(a)
@ -255,13 +255,12 @@ object Hash {
}
private val hMacAlgorithm = "HmacSHA256"
private[crypto] def hMacBuilder(key: Hash): Builder = new Builder(noCid2String) {
private val mac: Mac = Mac.getInstance(hMacAlgorithm)
private val macPrototype: MacPrototype = MacPrototype.HmacSHA_256
private val mac: Mac = macPrototype.newMac
mac.init(new SecretKeySpec(key.bytes.toByteArray, hMacAlgorithm))
mac.init(new SecretKeySpec(key.bytes.toByteArray, macPrototype.algorithm))
override protected def update(a: ByteBuffer): Unit =
mac.update(a)

View File

@ -313,6 +313,7 @@ alias(
"//ledger/ledger-api-common",
"//ledger/test-common:dar-files-default-lib",
"//libs-scala/contextualized-logging",
"//libs-scala/crypto",
"//libs-scala/db-utils",
"//libs-scala/doobie-slf4j",
"//libs-scala/ports",

View File

@ -13,6 +13,7 @@ import akka.stream.scaladsl.{Source, StreamConverters}
import akka.util.ByteString
import com.daml.api.util.TimestampConversion
import com.daml.bazeltools.BazelRunfiles.requiredResource
import com.daml.crypto.MessageDigestPrototype
import com.daml.lf.data.Ref
import com.daml.http.dbbackend.JdbcConfig
import com.daml.http.domain.ContractId
@ -71,9 +72,7 @@ object AbstractHttpServiceIntegrationTestFuns {
def sha256(source: Source[ByteString, Any])(implicit mat: Materializer): Try[String] = Try {
import com.google.common.io.BaseEncoding
import java.security.MessageDigest
val md = MessageDigest.getInstance("SHA-256")
val md = MessageDigestPrototype.SHA_256.newDigest
val is = source.runWith(StreamConverters.asInputStream())
val dis = new DigestInputStream(is, md)

View File

@ -42,6 +42,7 @@ da_scala_library(
"//ledger/ledger-resources",
"//ledger/metrics",
"//libs-scala/contextualized-logging",
"//libs-scala/crypto",
"//libs-scala/logging-entries",
"//libs-scala/resources",
"//libs-scala/resources-akka",

View File

@ -3,9 +3,10 @@
package com.daml.platform.store.serialization
import com.daml.crypto.MessageDigestPrototype
import java.nio.ByteBuffer
import java.security.MessageDigest
import com.daml.lf.data.{Numeric, Utf8}
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.value.Value
@ -154,7 +155,7 @@ object KeyHasher extends KeyHasher {
/** @deprecated in favor of [[GlobalKey.hash]]
*/
override def hashKey(key: GlobalKey): Array[Byte] = {
val digest = MessageDigest.getInstance("SHA-256")
val digest = MessageDigestPrototype.SHA_256.newDigest
// First, write the template ID
putString(digest, key.templateId.packageId)

View File

@ -287,6 +287,7 @@ da_scala_test_suite(
"//ledger/test-common:dar-files-default-lib",
"//libs-scala/concurrent",
"//libs-scala/contextualized-logging",
"//libs-scala/crypto",
"//libs-scala/grpc-utils",
"//libs-scala/logging-entries",
"//libs-scala/ports",

View File

@ -3,10 +3,7 @@
package com.daml.platform.store
import java.math.BigInteger
import java.nio.charset.Charset
import java.security.MessageDigest
import com.daml.crypto.MessageDigestPrototype
import com.daml.platform.store.FlywayMigrationsSpec._
import org.apache.commons.io.IOUtils
import org.flywaydb.core.Flyway
@ -17,6 +14,8 @@ import org.flywaydb.core.internal.scanner.{LocationScannerCache, ResourceNameCac
import org.scalatest.matchers.should.Matchers._
import org.scalatest.wordspec.AnyWordSpec
import java.math.BigInteger
import java.nio.charset.Charset
import scala.jdk.CollectionConverters._
// SQL MIGRATION AND THEIR DIGEST FILES SHOULD BE CREATED ONLY ONCE AND NEVER CHANGED AGAIN,
@ -44,7 +43,7 @@ class FlywayMigrationsSpec extends AnyWordSpec {
object FlywayMigrationsSpec {
private val digester = MessageDigest.getInstance("SHA-256")
private val digester = MessageDigestPrototype.SHA_256.newDigest
private def assertFlywayMigrationFileHashes(
dbType: DbType,

View File

@ -58,6 +58,7 @@ da_scala_library(
"//ledger/participant-state-index",
"//libs-scala/concurrent",
"//libs-scala/contextualized-logging",
"//libs-scala/crypto",
"//libs-scala/grpc-utils",
"//libs-scala/logging-entries",
"//libs-scala/resources",

View File

@ -3,7 +3,7 @@
package com.daml.ledger.validator
import java.security.MessageDigest
import com.daml.crypto.MessageDigestPrototype
import com.daml.ledger.participant.state.kvutils.Raw
import com.daml.ledger.participant.state.kvutils.store.DamlLogEntryId
@ -30,8 +30,7 @@ object HashingLogEntryIdComputationStrategy extends LogEntryIdComputationStrateg
.build
private def hash(rawEnvelope: Raw.Envelope): ByteString = {
val messageDigest = MessageDigest
.getInstance("SHA-256")
val messageDigest = MessageDigestPrototype.SHA_256.newDigest
messageDigest.update(rawEnvelope.bytes.asReadOnlyByteBuffer())
val hash = messageDigest
.digest()

View File

@ -0,0 +1,35 @@
# Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
load("//bazel_tools:scala.bzl", "da_scala_benchmark_jmh", "da_scala_library", "da_scala_test")
load("@scala_version//:index.bzl", "scala_major_version")
da_scala_library(
name = "crypto",
srcs = glob(["src/main/scala/**/*.scala"]),
tags = ["maven_coordinates=com.daml:crypto:__VERSION__"],
visibility = [
"//visibility:public",
],
deps = [
"//libs-scala/scala-utils",
],
)
da_scala_test(
name = "crypto-tests",
size = "small",
srcs = glob(["src/test/scala/**/*.scala"]),
deps = [
":crypto",
],
)
da_scala_benchmark_jmh(
name = "crypto-perf",
srcs = glob(["src/jmh/scala/**/*.scala"]),
visibility = ["//visibility:public"],
deps = [
":crypto",
],
)

View File

@ -0,0 +1,50 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.crypto
import org.openjdk.jmh.annotations._
import java.nio.charset.StandardCharsets
import javax.crypto.spec.SecretKeySpec
import scala.util.Random
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 5)
class MacPrototypeBenchmark {
var key = ""
var value = ""
val charset = StandardCharsets.UTF_8
@Setup
def setup(): Unit = {
key = Random.nextString(32)
value = Random.nextString(32)
}
private def encode(): Array[Byte] = {
val prototype = MacPrototype.HmacSHA_256
val mac = prototype.newMac
mac.init(new SecretKeySpec(key.getBytes(charset), prototype.algorithm))
mac.doFinal(value.getBytes(charset))
}
@Benchmark
@Threads(1)
def encodeHmacSHA_256_1(): Array[Byte] = {
encode()
}
@Benchmark
@Threads(4)
def encodeHmacSHA_256_4(): Array[Byte] = {
encode()
}
@Benchmark
@Threads(16)
def encodeHmacSHA_256_16(): Array[Byte] = {
encode()
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.crypto
import org.openjdk.jmh.annotations._
import java.nio.charset.StandardCharsets
import scala.util.Random
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 5)
class MessageDigestPrototypeBenchmark {
var value = ""
val charset = StandardCharsets.UTF_8
@Setup
def setup(): Unit = {
value = Random.nextString(32)
}
@Benchmark
@Threads(1)
def newDigest(): Array[Byte] = {
MessageDigestPrototype.SHA_256.newDigest.digest(value.getBytes(charset))
}
@Benchmark
@Threads(4)
def newDigest4(): Array[Byte] = {
MessageDigestPrototype.SHA_256.newDigest.digest(value.getBytes(charset))
}
@Benchmark
@Threads(16)
def newDigest16(): Array[Byte] = {
MessageDigestPrototype.SHA_256.newDigest.digest(value.getBytes(charset))
}
@Benchmark
@Threads(32)
def newDigest32(): Array[Byte] = {
MessageDigestPrototype.SHA_256.newDigest.digest(value.getBytes(charset))
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.crypto
import com.daml.scalautil.Statement.discard
import javax.crypto.Mac
/*
* Use Mac prototypes as a workaround for
* https://bugs.openjdk.java.net/browse/JDK-7092821, similar to Guava's
* workaround https://github.com/google/guava/issues/1197
*/
final class MacPrototype(val algorithm: String) {
private val prototype = createMac
private val supportsClone: Boolean =
try {
discard(prototype.clone())
true
} catch {
case _: CloneNotSupportedException =>
false
}
private def createMac: Mac = Mac.getInstance(algorithm)
def newMac: Mac = {
if (supportsClone)
prototype.clone().asInstanceOf[Mac]
else
createMac
}
}
object MacPrototype {
val HmacSHA_256 = new MacPrototype("HmacSHA256")
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.crypto
import com.daml.scalautil.Statement.discard
import java.security.MessageDigest
/*
* Use MessageDigest prototypes as a workaround for
* https://bugs.openjdk.java.net/browse/JDK-7092821, similar to Guava's
* workaround https://github.com/google/guava/issues/1197
*/
final class MessageDigestPrototype(val algorithm: String) {
private def createDigest: MessageDigest = MessageDigest.getInstance(algorithm)
private val prototype = createDigest
private val supportsClone: Boolean =
try {
discard(prototype.clone())
true
} catch {
case _: CloneNotSupportedException =>
false
}
def newDigest: MessageDigest = {
if (supportsClone)
prototype.clone().asInstanceOf[MessageDigest]
else
createDigest
}
}
object MessageDigestPrototype {
final val SHA_256 = new MessageDigestPrototype("SHA-256")
}

View File

@ -0,0 +1,38 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.crypto
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.spec.SecretKeySpec
class MacPrototypeSpec extends AnyFlatSpec with Matchers {
behavior of MacPrototype.getClass.getSimpleName
it should "provide new instance of digest for HmacSHA256" in {
val mac = MacPrototype.HmacSHA_256.newMac
val mac2 = MacPrototype.HmacSHA_256.newMac
mac should not be theSameInstanceAs(mac2)
}
it should "expose algorithm" in {
MacPrototype.HmacSHA_256.algorithm shouldBe "HmacSHA256"
}
it should "perform encoding for the `HmacSHA256` algorithm" in {
val key = "Hello"
val prototype = MacPrototype.HmacSHA_256
val mac = prototype.newMac
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), prototype.algorithm))
val sha = mac.doFinal("Hello World".getBytes(StandardCharsets.UTF_8))
new String(
Base64.getEncoder.encode(sha),
StandardCharsets.UTF_8,
) shouldBe "Y0PTLXgtpY9zSmzT6w2U48JcDGx7G7pRyHTRCIE/Pm0="
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.crypto
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import java.nio.charset.StandardCharsets
import java.util.Base64
class MessageDigestPrototypeSpec extends AnyFlatSpec with Matchers {
behavior of MessageDigestPrototype.getClass.getSimpleName
it should "provide new instance of digest for SHA-256" in {
val digest = MessageDigestPrototype.SHA_256.newDigest
val digest2 = MessageDigestPrototype.SHA_256.newDigest
digest should not be theSameInstanceAs(digest2)
}
it should "expose algorithm" in {
MessageDigestPrototype.SHA_256.algorithm shouldBe "SHA-256"
}
it should "perform a digest for the SHA-256 algorithm" in {
val digest = MessageDigestPrototype.SHA_256.newDigest
val sha = digest.digest("Hello World".getBytes(StandardCharsets.UTF_8))
new String(
Base64.getEncoder.encode(sha),
StandardCharsets.UTF_8,
) shouldBe "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4="
}
}

View File

@ -16,6 +16,7 @@ da_scala_library(
tags = ["maven_coordinates=com.daml:flyway-testing:__VERSION__"],
visibility = ["//visibility:public"],
deps = [
"//libs-scala/crypto",
"@maven//:org_flywaydb_flyway_core",
"@maven//:org_scalatest_scalatest_compatible",
],

View File

@ -3,12 +3,7 @@
package com.daml.flyway
import java.io.{BufferedReader, FileNotFoundException}
import java.math.BigInteger
import java.nio.charset.Charset
import java.security.MessageDigest
import java.util
import com.daml.crypto.MessageDigestPrototype
import org.flywaydb.core.Flyway
import org.flywaydb.core.api.configuration.FluentConfiguration
import org.flywaydb.core.api.resource.LoadableResource
@ -16,6 +11,10 @@ import org.flywaydb.core.internal.scanner.{LocationScannerCache, ResourceNameCac
import org.scalatest.matchers.should.Matchers._
import org.scalatest.wordspec.AnyWordSpec
import java.io.{BufferedReader, FileNotFoundException}
import java.math.BigInteger
import java.nio.charset.Charset
import java.util
import scala.jdk.CollectionConverters._
abstract class AbstractImmutableMigrationsSpec extends AnyWordSpec {
@ -50,7 +49,7 @@ abstract class AbstractImmutableMigrationsSpec extends AnyWordSpec {
}
private def computeCurrentDigest(resource: LoadableResource, encoding: Charset): String = {
val sha256 = MessageDigest.getInstance("SHA-256")
val sha256 = MessageDigestPrototype.SHA_256.newDigest
new BufferedReader(resource.read())
.lines()
.forEach(line => sha256.update((line + "\n").getBytes(encoding)))

View File

@ -217,6 +217,8 @@
type: jar-scala
- target: //libs-scala/contextualized-logging:contextualized-logging
type: jar-scala
- target: //libs-scala/crypto:crypto
type: jar-scala
- target: //libs-scala/doobie-slf4j:doobie-slf4j
type: jar-scala
- target: //libs-scala/grpc-utils:grpc-utils