mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 16:57:40 +03:00
TransactionNodesStatistics.stats to return case class of same name (#12159)
* TransactionNodesStatistics.stats now returns case class of same name changelog_begin changelog_end * Rename TransactionNodesStats to Detail and make inner class * Rename TransactionNodesStatistics.stats to apply * Rename TransactionNodesStatistics to TransactionNodeStatistics * Reformat * Remove rollback from statistic action count * Update with review comments * Update with review comments
This commit is contained in:
parent
0a2c207e93
commit
51b9405f5c
@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.lf
|
||||
package transaction
|
||||
|
||||
object TransactionNodeStatistics {
|
||||
|
||||
/** Container for transaction statistics.
|
||||
*
|
||||
* @param creates number of creates nodes,
|
||||
* @param consumingExercisesByCid number of consuming exercises by contract ID nodes,
|
||||
* @param nonconsumingExercisesByCid number of non-consuming Exercises by contract ID nodes,
|
||||
* @param consumingExercisesByKey number of consuming exercise by contract key nodes,
|
||||
* @param nonconsumingExercisesByKey number of non-consuming exercise by key nodes,
|
||||
* @param fetchesByCid number of fetch by contract ID nodes,
|
||||
* @param fetchesByKey number of fetch by key nodes,
|
||||
* @param lookupsByKey number of lookup by key nodes,
|
||||
*/
|
||||
final case class Actions(
|
||||
creates: Int,
|
||||
consumingExercisesByCid: Int,
|
||||
nonconsumingExercisesByCid: Int,
|
||||
consumingExercisesByKey: Int,
|
||||
nonconsumingExercisesByKey: Int,
|
||||
fetchesByCid: Int,
|
||||
fetchesByKey: Int,
|
||||
lookupsByKey: Int,
|
||||
) {
|
||||
|
||||
def +(that: Actions) =
|
||||
Actions(
|
||||
creates = this.creates + that.creates,
|
||||
consumingExercisesByCid = this.consumingExercisesByCid + that.consumingExercisesByCid,
|
||||
nonconsumingExercisesByCid =
|
||||
this.nonconsumingExercisesByCid + that.nonconsumingExercisesByCid,
|
||||
consumingExercisesByKey = this.consumingExercisesByKey + that.consumingExercisesByKey,
|
||||
nonconsumingExercisesByKey =
|
||||
this.nonconsumingExercisesByKey + that.nonconsumingExercisesByKey,
|
||||
fetchesByCid = this.fetchesByCid + that.fetchesByCid,
|
||||
fetchesByKey = this.fetchesByKey + that.fetchesByKey,
|
||||
lookupsByKey = this.lookupsByKey + that.lookupsByKey,
|
||||
)
|
||||
|
||||
def exercisesByCid: Int = consumingExercisesByCid + nonconsumingExercisesByCid
|
||||
def exercisesByKey: Int = consumingExercisesByKey + nonconsumingExercisesByKey
|
||||
def exercises: Int = exercisesByCid + exercisesByKey
|
||||
def consumingExercises: Int = consumingExercisesByCid + consumingExercisesByKey
|
||||
def nonconsumingExercises: Int = nonconsumingExercisesByCid + nonconsumingExercisesByKey
|
||||
def fetches: Int = fetchesByCid + fetchesByKey
|
||||
def byKeys: Int = exercisesByKey + fetchesByKey + lookupsByKey
|
||||
def actions: Int = creates + exercises + fetches + lookupsByKey
|
||||
}
|
||||
|
||||
val EmptyActions: Actions = Actions(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
val Empty: TransactionNodeStatistics = TransactionNodeStatistics(EmptyActions, EmptyActions)
|
||||
|
||||
private[this] val numberOfFields = EmptyActions.productArity
|
||||
|
||||
private[this] val Seq(
|
||||
createsIdx,
|
||||
consumingExercisesByCidIdx,
|
||||
nonconsumingExerciseCidsIdx,
|
||||
consumingExercisesByKeyIdx,
|
||||
nonconsumingExercisesByKeyIdx,
|
||||
fetchesIdx,
|
||||
fetchesByKeyIdx,
|
||||
lookupsByKeyIdx,
|
||||
) =
|
||||
(0 until numberOfFields)
|
||||
|
||||
private[this] def emptyFields = Array.fill(numberOfFields)(0)
|
||||
|
||||
private[this] def build(stats: Array[Int]) =
|
||||
Actions(
|
||||
creates = stats(createsIdx),
|
||||
consumingExercisesByCid = stats(consumingExercisesByCidIdx),
|
||||
nonconsumingExercisesByCid = stats(nonconsumingExerciseCidsIdx),
|
||||
consumingExercisesByKey = stats(consumingExercisesByKeyIdx),
|
||||
nonconsumingExercisesByKey = stats(nonconsumingExercisesByKeyIdx),
|
||||
fetchesByCid = stats(fetchesIdx),
|
||||
fetchesByKey = stats(fetchesByKeyIdx),
|
||||
lookupsByKey = stats(lookupsByKeyIdx),
|
||||
)
|
||||
|
||||
/** This function produces statistics about the committed nodes (those nodes
|
||||
* that do not appear under a rollback node) on the one hand and
|
||||
* rolled back nodes (those nodes that do appear under a rollback node) on
|
||||
* the other hand within a given transaction `tx`.
|
||||
*/
|
||||
def apply(tx: VersionedTransaction): TransactionNodeStatistics =
|
||||
apply(tx.transaction)
|
||||
|
||||
def apply(tx: Transaction): TransactionNodeStatistics = {
|
||||
val committed = emptyFields
|
||||
val rolledBack = emptyFields
|
||||
var rollbackDepth = 0
|
||||
|
||||
def incr(fieldIdx: Int) =
|
||||
if (rollbackDepth > 0) rolledBack(fieldIdx) += 1 else committed(fieldIdx) += 1
|
||||
|
||||
tx.foreachInExecutionOrder(
|
||||
exerciseBegin = { (_, exe) =>
|
||||
val idx =
|
||||
if (exe.consuming)
|
||||
if (exe.byKey)
|
||||
consumingExercisesByKeyIdx
|
||||
else
|
||||
consumingExercisesByCidIdx
|
||||
else if (exe.byKey)
|
||||
nonconsumingExercisesByKeyIdx
|
||||
else
|
||||
nonconsumingExerciseCidsIdx
|
||||
incr(idx)
|
||||
Transaction.ChildrenRecursion.DoRecurse
|
||||
},
|
||||
rollbackBegin = { (_, _) =>
|
||||
rollbackDepth += 1
|
||||
Transaction.ChildrenRecursion.DoRecurse
|
||||
},
|
||||
leaf = { (_, node) =>
|
||||
val idx = node match {
|
||||
case _: Node.Create =>
|
||||
createsIdx
|
||||
case fetch: Node.Fetch =>
|
||||
if (fetch.byKey)
|
||||
fetchesByKeyIdx
|
||||
else
|
||||
fetchesIdx
|
||||
case _: Node.LookupByKey =>
|
||||
lookupsByKeyIdx
|
||||
}
|
||||
incr(idx)
|
||||
},
|
||||
exerciseEnd = (_, _) => (),
|
||||
rollbackEnd = (_, _) => rollbackDepth -= 1,
|
||||
)
|
||||
|
||||
TransactionNodeStatistics(build(committed), build(rolledBack))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final case class TransactionNodeStatistics(
|
||||
committed: TransactionNodeStatistics.Actions,
|
||||
rolledBack: TransactionNodeStatistics.Actions,
|
||||
) {
|
||||
def +(that: TransactionNodeStatistics) = {
|
||||
TransactionNodeStatistics(this.committed + that.committed, this.rolledBack + that.rolledBack)
|
||||
}
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.lf
|
||||
package transaction
|
||||
|
||||
/** Container for transaction statistics.
|
||||
*
|
||||
* @param creates number of creates nodes,
|
||||
* @param consumingExercisesByCid number of consuming exercises by contract ID nodes,
|
||||
* @param nonconsumingExercisesByCid number of non-consuming Exercises by contract ID nodes,
|
||||
* @param consumingExercisesByKey number of consuming exercise by contract key nodes,
|
||||
* @param nonconsumingExercisesByKey number of non-consuming exercise by key nodes,
|
||||
* @param fetchesByCid number of fetch by contract ID nodes,
|
||||
* @param fetchesByKey number of fetch by key nodes,
|
||||
* @param lookupsByKey number of lookup by key nodes,
|
||||
* @param rollbacks number of rollback nodes.
|
||||
*/
|
||||
final case class TransactionNodesStatistics(
|
||||
creates: Int,
|
||||
consumingExercisesByCid: Int,
|
||||
nonconsumingExercisesByCid: Int,
|
||||
consumingExercisesByKey: Int,
|
||||
nonconsumingExercisesByKey: Int,
|
||||
fetchesByCid: Int,
|
||||
fetchesByKey: Int,
|
||||
lookupsByKey: Int,
|
||||
rollbacks: Int,
|
||||
) {
|
||||
|
||||
def +(that: TransactionNodesStatistics) =
|
||||
TransactionNodesStatistics(
|
||||
creates = this.creates + that.creates,
|
||||
consumingExercisesByCid = this.consumingExercisesByCid + that.consumingExercisesByCid,
|
||||
nonconsumingExercisesByCid =
|
||||
this.nonconsumingExercisesByCid + that.nonconsumingExercisesByCid,
|
||||
consumingExercisesByKey = this.consumingExercisesByKey + that.consumingExercisesByKey,
|
||||
nonconsumingExercisesByKey =
|
||||
this.nonconsumingExercisesByKey + that.nonconsumingExercisesByKey,
|
||||
fetchesByCid = this.fetchesByCid + that.fetchesByCid,
|
||||
fetchesByKey = this.fetchesByKey + that.fetchesByKey,
|
||||
lookupsByKey = this.lookupsByKey + that.lookupsByKey,
|
||||
rollbacks = this.rollbacks + that.rollbacks,
|
||||
)
|
||||
|
||||
def exercisesByCid: Int = consumingExercisesByCid + nonconsumingExercisesByCid
|
||||
def exercisesByKey: Int = consumingExercisesByKey + nonconsumingExercisesByKey
|
||||
def exercises: Int = exercisesByCid + exercisesByKey
|
||||
def consumingExercises: Int = consumingExercisesByCid + consumingExercisesByKey
|
||||
def nonconsumingExercises: Int = nonconsumingExercisesByCid + nonconsumingExercisesByKey
|
||||
def fetches: Int = fetchesByCid + fetchesByKey
|
||||
def byKeys: Int = exercisesByKey + fetchesByKey + lookupsByKey
|
||||
def actions: Int = creates + exercises + fetches + lookupsByKey
|
||||
def nodes: Int = actions + rollbacks
|
||||
}
|
||||
|
||||
object TransactionNodesStatistics {
|
||||
|
||||
val Empty = TransactionNodesStatistics(0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
private[this] val numberOfFields = Empty.productArity
|
||||
|
||||
private[this] val Seq(
|
||||
createsIdx,
|
||||
consumingExercisesByCidIdx,
|
||||
nonconsumingExerciseCidsIdx,
|
||||
consumingExercisesByKeyIdx,
|
||||
nonconsumingExercisesByKeyIdx,
|
||||
fetchesIdx,
|
||||
fetchesByKeyIdx,
|
||||
lookupsByKeyIdx,
|
||||
rollbacksIdx,
|
||||
) =
|
||||
(0 until numberOfFields)
|
||||
|
||||
private[this] def emptyFields = Array.fill(numberOfFields)(0)
|
||||
|
||||
private[this] def build(stats: Array[Int]) =
|
||||
TransactionNodesStatistics(
|
||||
creates = stats(createsIdx),
|
||||
consumingExercisesByCid = stats(consumingExercisesByCidIdx),
|
||||
nonconsumingExercisesByCid = stats(nonconsumingExerciseCidsIdx),
|
||||
consumingExercisesByKey = stats(consumingExercisesByKeyIdx),
|
||||
nonconsumingExercisesByKey = stats(nonconsumingExercisesByKeyIdx),
|
||||
fetchesByCid = stats(fetchesIdx),
|
||||
fetchesByKey = stats(fetchesByKeyIdx),
|
||||
lookupsByKey = stats(lookupsByKeyIdx),
|
||||
rollbacks = stats(rollbacksIdx),
|
||||
)
|
||||
|
||||
/** This function produces statistics about the committed nodes (those nodes
|
||||
* that do not appear under a rollback node) on the one hand and
|
||||
* rolled back nodes (those nodes that do appear under a rollback node) on
|
||||
* the other hand within a given transaction `tx`.
|
||||
*/
|
||||
def stats(tx: VersionedTransaction): (TransactionNodesStatistics, TransactionNodesStatistics) =
|
||||
stats(tx.transaction)
|
||||
|
||||
def stats(tx: Transaction): (TransactionNodesStatistics, TransactionNodesStatistics) = {
|
||||
val committed = emptyFields
|
||||
val rolledBack = emptyFields
|
||||
var rollbackDepth = 0
|
||||
|
||||
def incr(fieldIdx: Int) =
|
||||
if (rollbackDepth > 0) rolledBack(fieldIdx) += 1 else committed(fieldIdx) += 1
|
||||
|
||||
tx.foreachInExecutionOrder(
|
||||
exerciseBegin = { (_, exe) =>
|
||||
val idx =
|
||||
if (exe.consuming)
|
||||
if (exe.byKey)
|
||||
consumingExercisesByKeyIdx
|
||||
else
|
||||
consumingExercisesByCidIdx
|
||||
else if (exe.byKey)
|
||||
nonconsumingExercisesByKeyIdx
|
||||
else
|
||||
nonconsumingExerciseCidsIdx
|
||||
incr(idx)
|
||||
Transaction.ChildrenRecursion.DoRecurse
|
||||
},
|
||||
rollbackBegin = { (_, _) =>
|
||||
incr(rollbacksIdx)
|
||||
rollbackDepth += 1
|
||||
Transaction.ChildrenRecursion.DoRecurse
|
||||
},
|
||||
leaf = { (_, node) =>
|
||||
val idx = node match {
|
||||
case _: Node.Create =>
|
||||
createsIdx
|
||||
case fetch: Node.Fetch =>
|
||||
if (fetch.byKey)
|
||||
fetchesByKeyIdx
|
||||
else
|
||||
fetchesIdx
|
||||
case _: Node.LookupByKey =>
|
||||
lookupsByKeyIdx
|
||||
}
|
||||
incr(idx)
|
||||
},
|
||||
exerciseEnd = (_, _) => (),
|
||||
rollbackEnd = (_, _) => rollbackDepth -= 1,
|
||||
)
|
||||
|
||||
(build(committed), build(rolledBack))
|
||||
}
|
||||
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
package com.daml.lf
|
||||
package transaction
|
||||
|
||||
import com.daml.lf.transaction.TransactionNodeStatistics.Actions
|
||||
import com.daml.lf.transaction.test.{TransactionBuilder => TxBuilder}
|
||||
import com.daml.lf.value.Value
|
||||
import org.scalatest.Inside
|
||||
@ -11,29 +12,44 @@ import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
class TransactionNodesStatisticsSpec
|
||||
class TransactionNodeStatisticsSpec
|
||||
extends AnyWordSpec
|
||||
with Inside
|
||||
with Matchers
|
||||
with TableDrivenPropertyChecks {
|
||||
|
||||
"TransactionNodeStatistics#+" should {
|
||||
"TransactionNodeStatistics.Actions#+" should {
|
||||
|
||||
"add" in {
|
||||
val s1 = TransactionNodesStatistics(1, 1, 1, 1, 1, 1, 1, 1, 1)
|
||||
val s2 = TransactionNodesStatistics(2, 3, 5, 7, 11, 13, 17, 19, 23)
|
||||
val s1s2 = TransactionNodesStatistics(3, 4, 6, 8, 12, 14, 18, 20, 24)
|
||||
val s2s2 = TransactionNodesStatistics(4, 6, 10, 14, 22, 26, 34, 38, 46)
|
||||
val s1 = Actions(1, 1, 1, 1, 1, 1, 1, 1)
|
||||
val s2 = Actions(2, 3, 5, 7, 11, 13, 17, 19)
|
||||
val s1s2 = Actions(3, 4, 6, 8, 12, 14, 18, 20)
|
||||
val s2s2 = Actions(4, 6, 10, 14, 22, 26, 34, 38)
|
||||
|
||||
s2 + TransactionNodesStatistics.Empty shouldBe s2
|
||||
TransactionNodesStatistics.Empty + s2 shouldBe s2
|
||||
s2 + TransactionNodeStatistics.EmptyActions shouldBe s2
|
||||
TransactionNodeStatistics.EmptyActions + s2 shouldBe s2
|
||||
s1 + s2 shouldBe s1s2
|
||||
s2 + s1 shouldBe s1s2
|
||||
s2 + s2 shouldBe s2s2
|
||||
}
|
||||
}
|
||||
|
||||
"TransactionNodeStatistics.stats" should {
|
||||
"TransactionNodeStatistics#+" should {
|
||||
|
||||
"add" in {
|
||||
val d1c = Actions(1, 1, 1, 1, 1, 1, 1, 1)
|
||||
val d1r = Actions(2, 3, 5, 7, 11, 13, 17, 19)
|
||||
val d2c = Actions(3, 5, 7, 11, 13, 17, 19, 23)
|
||||
val d2r = Actions(5, 7, 11, 13, 17, 19, 23, 29)
|
||||
|
||||
val s1 = TransactionNodeStatistics(d1c, d1r)
|
||||
val s2 = TransactionNodeStatistics(d2c, d2r)
|
||||
val expected = TransactionNodeStatistics(d1c + d2c, d1r + d2r)
|
||||
s1 + s2 shouldBe expected
|
||||
}
|
||||
}
|
||||
|
||||
"TransactionNodeStatistics" should {
|
||||
|
||||
def create(b: TxBuilder, withKey: Boolean = false) = {
|
||||
val parties = Set(b.newParty)
|
||||
@ -88,7 +104,7 @@ class TransactionNodesStatisticsSpec
|
||||
|
||||
val testIterations = 3
|
||||
|
||||
type Getter = TransactionNodesStatistics => Int
|
||||
type Getter = Actions => Int
|
||||
|
||||
val testCases = Table[TxBuilder => Node, Getter](
|
||||
"makeNode" -> "getter",
|
||||
@ -100,7 +116,6 @@ class TransactionNodesStatisticsSpec
|
||||
(fetch(byKey = false), _.fetchesByCid),
|
||||
(fetch(byKey = true), _.fetchesByKey),
|
||||
(lookup, _.lookupsByKey),
|
||||
(rollback, _.rollbacks),
|
||||
)
|
||||
|
||||
"count each type of committed nodes properly" in {
|
||||
@ -109,11 +124,11 @@ class TransactionNodesStatisticsSpec
|
||||
|
||||
for (i <- 1 to testIterations) {
|
||||
builder.add(makeNode(builder))
|
||||
inside(TransactionNodesStatistics.stats(builder.build())) {
|
||||
case (committed, rolledBack) =>
|
||||
inside(TransactionNodeStatistics(builder.build())) {
|
||||
case TransactionNodeStatistics(committed, rolledBack) =>
|
||||
getter(committed) shouldBe i
|
||||
committed.nodes shouldBe i
|
||||
rolledBack shouldBe TransactionNodesStatistics.Empty
|
||||
committed.actions shouldBe i
|
||||
rolledBack shouldBe TransactionNodeStatistics.EmptyActions
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,11 +141,11 @@ class TransactionNodesStatisticsSpec
|
||||
|
||||
for (i <- 1 to testIterations) {
|
||||
builder.add(makeNode(builder), rollbackId)
|
||||
inside(TransactionNodesStatistics.stats(builder.build())) {
|
||||
case (committed, rolledBack) =>
|
||||
committed shouldBe TransactionNodesStatistics(0, 0, 0, 0, 0, 0, 0, 0, rollbacks = 1)
|
||||
inside(TransactionNodeStatistics.apply(builder.build())) {
|
||||
case TransactionNodeStatistics(committed, rolledBack) =>
|
||||
committed shouldBe Actions(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
getter(rolledBack) shouldBe i
|
||||
rolledBack.nodes shouldBe i
|
||||
rolledBack.actions shouldBe i
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -142,11 +157,12 @@ class TransactionNodesStatisticsSpec
|
||||
|
||||
for (i <- 1 to testIterations) {
|
||||
addAllNodes(b, exeId) // one additional nodes of each types
|
||||
inside(TransactionNodesStatistics.stats(b.build())) { case (committed, rolledBack) =>
|
||||
// There are twice more nonconsumming exercises by cid are double because
|
||||
// we use a extra one to nest the other node of the next loop
|
||||
committed shouldBe TransactionNodesStatistics(i, i, 2 * i, i, i, i, i, i, i)
|
||||
rolledBack shouldBe TransactionNodesStatistics.Empty
|
||||
inside(TransactionNodeStatistics.apply(b.build())) {
|
||||
case TransactionNodeStatistics(committed, rolledBack) =>
|
||||
// There are twice more nonconsumming exercises by cid are double because
|
||||
// we use a extra one to nest the other node of the next loop
|
||||
committed shouldBe Actions(i, i, 2 * i, i, i, i, i, i)
|
||||
rolledBack shouldBe TransactionNodeStatistics.EmptyActions
|
||||
}
|
||||
exeId = b.add(exe(false, false)(b), exeId) // one nonconsumming exercises
|
||||
}
|
||||
@ -159,11 +175,12 @@ class TransactionNodesStatisticsSpec
|
||||
for (i <- 1 to testIterations) {
|
||||
rbId = b.add(rollback(b), rbId) // one additional rolled Back rollback node
|
||||
addAllNodes(b, rbId) // one additional rolled Back nodes of each type
|
||||
inside(TransactionNodesStatistics.stats(b.build())) { case (committed, rolledBack) =>
|
||||
committed shouldBe TransactionNodesStatistics(0, 0, 0, 0, 0, 0, 0, 0, 1)
|
||||
// There are twice more rollback nodes, since we use an extra one to
|
||||
// nest the other nodes in each loop
|
||||
rolledBack shouldBe TransactionNodesStatistics(i, i, i, i, i, i, i, i, 2 * i)
|
||||
inside(TransactionNodeStatistics.apply(b.build())) {
|
||||
case TransactionNodeStatistics(committed, rolledBack) =>
|
||||
committed shouldBe Actions(0, 0, 0, 0, 0, 0, 0, 0)
|
||||
// There are twice more rollback nodes, since we use an extra one to
|
||||
// nest the other nodes in each loop
|
||||
rolledBack shouldBe Actions(i, i, i, i, i, i, i, i)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ package com.daml.ledger.participant.state.v2
|
||||
|
||||
import com.daml.ledger.api.DeduplicationPeriod
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.lf.transaction.TransactionNodesStatistics
|
||||
import com.daml.lf.transaction.TransactionNodeStatistics
|
||||
import com.daml.logging.entries.{LoggingValue, ToLoggingValue}
|
||||
|
||||
/** Information about a completion for a submission.
|
||||
@ -43,7 +43,7 @@ case class CompletionInfo(
|
||||
commandId: Ref.CommandId,
|
||||
optDeduplicationPeriod: Option[DeduplicationPeriod],
|
||||
submissionId: Option[Ref.SubmissionId],
|
||||
statistics: Option[TransactionNodesStatistics],
|
||||
statistics: Option[TransactionNodeStatistics],
|
||||
) {
|
||||
def changeId: ChangeId = ChangeId(applicationId, commandId, actAs.toSet)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user