Skip to content

Commit

Permalink
DEX-344 A documentation generator for DEX errors (#7)
Browse files Browse the repository at this point in the history
MatcherErrorDoc is a generator for a error's documentation in Markdown format. Contains:
* All error entities and classes;
* Samples for each error and how to deconstruct it's code to object, entity and class;
* Unit tests to check there is no two equal error's code for different error classes;

Also see the [description](https://github.com/wavesplatform/dex/wiki/The-structure-of-errors) and [the list of errors](https://github.com/wavesplatform/dex/wiki/List-of-all-errors).

MatcherError
* Now it is an "sealed abstract" class (was just "sealed");
* Descendants now are outside of the companion object due to scala macros limitations;
* Improved error messages;
* Fixed an error code duplication;

ErrorInterpolator:
* Now it's a typesafe. Compiler won't generate an MatcherErrorMessage if there is no Writes and Show for a used argument;
* Now it supports a multiline messages;
* Moved to com.wavesplatform.dex.error.Implicits;
* All instances for all type classes required for ErrorInterpolator are in the com.wavesplatform.dex.error.Implicits;

New com.wavesplatform.dex.meta package with meta-programming utilities:
* Sample is an utility class to generate a sample for any class;
* DescendantSamples is an utility class to generate samples for all sealed trait/class descendants;

README.md:
* Added instructions to generate documentation;
* Added new required VM option: "-Xss";
* Added VM parameters brief description;
* Added a section about required VM options for SBT running through IntelliJ IDEA;
* Fixed wrong config's section (waves.matcher vs waves.dex);
  • Loading branch information
vsuharnikov authored and koloale committed Jul 8, 2019
1 parent 98e4767 commit 205d243
Show file tree
Hide file tree
Showing 23 changed files with 771 additions and 453 deletions.
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,21 @@ brew cask install java sbt@1
1. Please follow the SBT installation instructions depending on your operating system ([Mac](https://www.scala-sbt.org/1.0/docs/Installing-sbt-on-Mac.html), [Windows](https://www.scala-sbt.org/1.0/docs/Installing-sbt-on-Windows.html), [Linux](https://www.scala-sbt.org/1.0/docs/Installing-sbt-on-Linux.html)).
2. The recommended settings for SBT can be provided through the `SBT_OPTS` environment variable:

* During each SBT run: `SBT_OPTS="-Xmx2048M -XX:MaxMetaspaceExpansion=0 -XX:MetaspaceSize=256m"`
* Or once in _~/.bash_profile_: `export SBT_OPTS="-Xmx2048M -XX:MaxMetaspaceExpansion=0 -XX:MetaspaceSize=256m"`.
Restart the terminal after this change.
Options are: `-Xmx2048M -Xss256m -XX:MetaspaceSize=256m -XX:MaxMetaspaceExpansion=0`

* During each SBT run: `SBT_OPTS="<paste options here>" `
* Or once in _~/.bash_profile_: `export SBT_OPTS="<paste options here>"`.
Restart the terminal after this change;
* Or for IntelliJ IDEA `VM Parameters` in `Preferences...` > `Build, Execution, Deployment` > `Build Tools` > `sbt`.

3. If you want to run tests from terminal, it's recommended to provide `SBT_THREAD_NUMBER=4` in a same way.
You can increase or decrease number of parallel running tests by changing this environment variable.

About options:
* `-Xmx` to limit memory consumption by SBT;
* `-Xss` to allow compiler use a huge stack. Requred for `shapeless`;
* `-XX:MaxMetaspaceExpansion` to force garbage

## 2. Obtaining Source Codes

```
Expand Down Expand Up @@ -136,7 +144,7 @@ waves.extensions = [
# ... here may be other extensions
]
waves.matcher {
waves.dex {
account = "3Q5GKPLkxXcEwGv6d57v8aksTjh1igHNNDd" # This account must be known at the Node, e.g. created through POST /addresses
# bind-address = "0.0.0.0" # uncomment this line to accept connections from any host
}
Expand Down Expand Up @@ -166,7 +174,17 @@ sbt "dex/run /path/to/configuration"

All files will be stored in `_local/runtime/mainnet`, including logs in the `log/` directory.

## 9. Known issues
## 9. Useful commands

In SBT.

### Generate documentation

```
sbt "dex/runMain com.wavesplatform.dex.MatcherTool /path/to/config gen-docs /path/to/output/docs/dir"
```

## 10. Known issues

### IntelliJ IDEA

Expand Down
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import sbt.Keys._
import sbt._
import sbt.internal.inc.ReflectUtilities

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

def nodeVersionTag: String = "5468da2b49078a82d1208fadd1523deed251feb1"

lazy val node = ProjectRef(uri(s"git://github.com/wavesplatform/Waves.git#$nodeVersionTag"), "node")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class OrdersFromScriptedAccTestSuite extends MatcherSuiteBase {
"trading is deprecated" in {
assertBadRequestAndResponse(
node.placeOrder(bob, aliceWavesPair, OrderType.BUY, 500, 2.waves * Order.PriceConstant, smartTradeFee, version = 1, 10.minutes),
"The trading on scripted account isn't yet supported"
"An account's feature isn't yet supported"
)
}

Expand Down
20 changes: 9 additions & 11 deletions dex/src/main/scala/com/wavesplatform/dex/AddressActor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.wavesplatform.dex.Matcher.StoreEvent
import com.wavesplatform.dex.api.NotImplemented
import com.wavesplatform.dex.db.OrderDB
import com.wavesplatform.dex.db.OrderDB.orderInfoOrdering
import com.wavesplatform.dex.error.MatcherError
import com.wavesplatform.dex.model.Events.{OrderAdded, OrderCanceled, OrderExecuted}
import com.wavesplatform.dex.model.{LimitOrder, OrderInfo, OrderStatus, OrderValidator}
import com.wavesplatform.dex.queue.QueueEvent
Expand Down Expand Up @@ -106,9 +105,9 @@ class AddressActor(
case Some(lo) => storeCanceled(lo.order.assetPair, lo.order.id())
case None =>
val reason = orderDB.status(id) match {
case OrderStatus.NotFound => MatcherError.OrderNotFound(id)
case OrderStatus.Cancelled(_) => MatcherError.OrderCanceled(id)
case OrderStatus.Filled(_) => MatcherError.OrderFull(id)
case OrderStatus.NotFound => error.OrderNotFound(id)
case OrderStatus.Cancelled(_) => error.OrderCanceled(id)
case OrderStatus.Filled(_) => error.OrderFull(id)
}

Future.successful(api.OrderCancelRejected(reason))
Expand Down Expand Up @@ -144,16 +143,16 @@ class AddressActor(
}
}

private def store(id: ByteStr, event: QueueEvent, eventCache: MutableMap[ByteStr, Promise[Resp]], error: Resp): Future[Resp] = {
private def store(id: ByteStr, event: QueueEvent, eventCache: MutableMap[ByteStr, Promise[Resp]], storeError: Resp): Future[Resp] = {
val promisedResponse = Promise[api.WrappedMatcherResponse]
eventCache += id -> promisedResponse
storeEvent(event).transformWith {
case Failure(e) =>
log.error(s"Error persisting $event", e)
Future.successful(error)
Future.successful(storeError)
case Success(r) =>
r match {
case None => Future.successful(NotImplemented(MatcherError.FeatureDisabled))
case None => Future.successful(NotImplemented(error.FeatureDisabled))
case Some(x) =>
log.info(s"Stored $x")
promisedResponse.future
Expand All @@ -162,10 +161,9 @@ class AddressActor(
}

private def storeCanceled(assetPair: AssetPair, id: ByteStr): Future[Resp] =
store(id, QueueEvent.Canceled(assetPair, id), pendingCancellation, api.OrderCancelRejected(MatcherError.CanNotPersistEvent))
store(id, QueueEvent.Canceled(assetPair, id), pendingCancellation, api.OrderCancelRejected(error.CanNotPersistEvent))
private def storePlaced(order: Order): Future[Resp] = {
import com.wavesplatform.dex.error._
store(order.id(), QueueEvent.Placed(order), pendingPlacement, api.OrderRejected(MatcherError.CanNotPersistEvent))
store(order.id(), QueueEvent.Placed(order), pendingPlacement, api.OrderRejected(error.CanNotPersistEvent))
}

private def confirmPlacement(order: Order): Unit = for (p <- pendingPlacement.remove(order.id())) {
Expand Down Expand Up @@ -246,7 +244,7 @@ class AddressActor(
private def handleOrderTerminated(lo: LimitOrder, status: OrderStatus.Final): Unit = {
log.trace(s"Order ${lo.order.id()} terminated: $status")
orderDB.saveOrder(lo.order)
pendingCancellation.remove(lo.order.id()).foreach(_.success(api.OrderCancelRejected(MatcherError.OrderFinalized(lo.order.id()))))
pendingCancellation.remove(lo.order.id()).foreach(_.success(api.OrderCancelRejected(error.OrderFinalized(lo.order.id()))))
expiration.remove(lo.order.id()).foreach(_.cancel())
activeOrders.remove(lo.order.id())
orderDB.saveOrderInfo(
Expand Down
18 changes: 9 additions & 9 deletions dex/src/main/scala/com/wavesplatform/dex/AssetPairBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ class AssetPairBuilder(settings: MatcherSettings, blockchain: Blockchain, blackl

private def validateAssetId(asset: Asset, side: AssetSide): Result[Asset] =
asset.fold[Result[Asset]](Right(Waves)) { asset =>
blockchain.assetDescription(asset).fold[Result[Asset]](Left(MatcherError.AssetNotFound(asset))) { desc =>
blockchain.assetDescription(asset).fold[Result[Asset]](Left(error.AssetNotFound(asset))) { desc =>
if (blacklistedAssets.contains(asset) || isBlacklistedByName(asset, desc)) Left(side match {
case AssetSide.Unknown => MatcherError.AssetBlacklisted(asset)
case AssetSide.Amount => MatcherError.AmountAssetBlacklisted(asset)
case AssetSide.Price => MatcherError.PriceAssetBlacklisted(asset)
case AssetSide.Unknown => error.AssetBlacklisted(asset)
case AssetSide.Amount => error.AmountAssetBlacklisted(asset)
case AssetSide.Price => error.PriceAssetBlacklisted(asset)
})
else Right(asset)
}
Expand All @@ -53,20 +53,20 @@ class AssetPairBuilder(settings: MatcherSettings, blockchain: Blockchain, blackl
def validateAssetPair(pair: AssetPair): Result[AssetPair] =
validate.measure {
if (settings.allowedAssetPairs.contains(pair)) pair.asRight
else if (settings.whiteListOnly) Left(MatcherError.AssetPairIsNotAllowed(pair))
else if (settings.whiteListOnly) Left(error.AssetPairIsDenied(pair))
else
for {
_ <- cond(pair.amountAsset != pair.priceAsset, (), MatcherError.AssetPairSameAssets(pair.amountAsset))
_ <- cond(isCorrectlyOrdered(pair), pair, MatcherError.AssetPairReversed(pair))
_ <- cond(pair.amountAsset != pair.priceAsset, (), error.AssetPairSameAssets(pair.amountAsset))
_ <- cond(isCorrectlyOrdered(pair), pair, error.OrderAssetPairReversed(pair))
_ <- validateAssetId(pair.priceAsset, AssetSide.Price)
_ <- validateAssetId(pair.amountAsset, AssetSide.Amount)
} yield pair
}

def createAssetPair(a1: String, a2: String): Result[AssetPair] =
create.measure(for {
a1 <- AssetPair.extractAssetId(a1).toEither.left.map(_ => MatcherError.InvalidAsset(a1))
a2 <- AssetPair.extractAssetId(a2).toEither.left.map(_ => MatcherError.InvalidAsset(a2))
a1 <- AssetPair.extractAssetId(a1).toEither.left.map(_ => error.InvalidAsset(a1))
a2 <- AssetPair.extractAssetId(a2).toEither.left.map(_ => error.InvalidAsset(a2))
p <- validateAssetPair(AssetPair(a1, a2))
} yield p)
}
Expand Down
14 changes: 13 additions & 1 deletion dex/src/main/scala/com/wavesplatform/dex/MatcherTool.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wavesplatform.dex

import java.io.File
import java.io.{File, PrintWriter}
import java.nio.file.{Files, Paths}
import java.util.{HashMap => JHashMap}

import akka.actor.ActorSystem
Expand All @@ -18,6 +19,7 @@ import com.wavesplatform.common.utils.Base58
import com.wavesplatform.database._
import com.wavesplatform.db.openDB
import com.wavesplatform.dex.db.{AssetPairsDB, OrderBookSnapshotDB}
import com.wavesplatform.dex.doc.MatcherErrorDoc
import com.wavesplatform.dex.market.{MatcherActor, OrderBookActor}
import com.wavesplatform.dex.model.{LimitOrder, OrderBook}
import com.wavesplatform.dex.settings.MatcherSettings
Expand Down Expand Up @@ -95,6 +97,16 @@ object MatcherTool extends ScorexLogging {

val start = System.currentTimeMillis()
args(1) match {
case "gen-docs" =>
val outDir = args(2)
val outPath = Paths.get(outDir)
Files.createDirectories(outPath)
val errors = new PrintWriter(outPath.resolve("errors.md").toFile)
try {
errors.write(MatcherErrorDoc.mkMarkdown)
} finally {
errors.close()
}
case "stats" => collectStats(db)
case "ob" =>
val pair = AssetPair.createAssetPair(args(2), args(3)).get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.wavesplatform.dex.market.OrderBookActor._
import com.wavesplatform.dex.model._
import com.wavesplatform.dex.queue.{QueueEvent, QueueEventWithMeta}
import com.wavesplatform.dex.settings.{MatcherSettings, OrderRestrictionsSettings, formatValue}
import com.wavesplatform.dex.{AddressActor, AssetPairBuilder, Matcher, RateCache}
import com.wavesplatform.dex._
import com.wavesplatform.metrics.TimerExt
import com.wavesplatform.transaction.Asset
import com.wavesplatform.transaction.Asset.Waves
Expand Down Expand Up @@ -92,13 +92,13 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
}

private def unavailableOrderBookBarrier(p: AssetPair): Directive0 = orderBook(p) match {
case Some(x) => if (x.isRight) pass else complete(OrderBookUnavailable(MatcherError.OrderBookBroken(p)))
case Some(x) => if (x.isRight) pass else complete(OrderBookUnavailable(error.OrderBookBroken(p)))
case None => forceCheckOrderBook(p)
}

private def forceCheckOrderBook(p: AssetPair): Directive0 = onComplete(matcher ? ForceStartOrderBook(p)).flatMap {
case Success(_) => pass
case _ => complete(OrderBookUnavailable(MatcherError.OrderBookBroken(p)))
case _ => complete(OrderBookUnavailable(error.OrderBookBroken(p)))
}

private def withAssetPair(p: AssetPair,
Expand Down Expand Up @@ -336,7 +336,7 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
complete((timestamp, orderId) match {
case (Some(ts), None) => askAddressActor[MatcherResponse](sender, AddressActor.CancelAllOrders(assetPair, ts))
case (None, Some(oid)) => askAddressActor[MatcherResponse](sender, AddressActor.CancelOrder(oid))
case _ => OrderCancelRejected(MatcherError.CancelRequestIsIncomplete)
case _ => OrderCancelRejected(error.CancelRequestIsIncomplete)
})

private def handleCancelRequest(assetPair: Option[AssetPair]): Route =
Expand Down Expand Up @@ -418,7 +418,7 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
))
def historyDelete: Route = (path("orderbook" / AssetPairPM / "delete") & post) { _ =>
json[CancelOrderRequest] { req =>
req.orderId.fold[MatcherResponse](NotImplemented(MatcherError.FeatureNotImplemented))(OrderDeleted)
req.orderId.fold[MatcherResponse](NotImplemented(error.FeatureNotImplemented))(OrderDeleted)
}
}

Expand Down Expand Up @@ -516,7 +516,7 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
def forceCancelOrder: Route = (path("orders" / "cancel" / ByteStrPM) & post & withAuth) { orderId =>
DBUtils.order(db, orderId) match {
case Some(order) => handleCancelRequest(None, order.sender, Some(orderId), None)
case None => complete(OrderCancelRejected(MatcherError.OrderNotFound(orderId)))
case None => complete(OrderCancelRejected(error.OrderNotFound(orderId)))
}
}

Expand Down Expand Up @@ -613,10 +613,10 @@ case class MatcherApiRoute(assetPairBuilder: AssetPairBuilder,
orderBook(pair) match {
case Some(Right(_)) =>
complete(storeEvent(QueueEvent.OrderBookDeleted(pair)).map {
case None => NotImplemented(MatcherError.FeatureDisabled)
case None => NotImplemented(error.FeatureDisabled)
case _ => SimpleResponse(StatusCodes.Accepted, "Deleting order book")
})
case _ => complete(OrderBookUnavailable(MatcherError.OrderBookBroken(pair)))
case _ => complete(OrderBookUnavailable(error.OrderBookBroken(pair)))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ case class MatcherApiRouteV1(assetPairBuilder: AssetPairBuilder,
.validateAssetPair(p.reverse)
.fold(
_ => complete(formatError(e)),
_ => redirect(s"/matcher/orderbook/${p.priceAssetStr}/${p.amountAssetStr}$suffix", StatusCodes.MovedPermanently)
_ => redirect(s"/api/v1/${p.priceAssetStr}/${p.amountAssetStr}$suffix", StatusCodes.MovedPermanently)
)
case Left(e) => complete(formatError(e))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.http.scaladsl.marshalling.{Marshaller, ToResponseMarshaller}
import akka.http.scaladsl.model.{StatusCodes => C, _}
import akka.util.ByteString
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.dex.error
import com.wavesplatform.dex.error.MatcherError
import com.wavesplatform.dex.model.OrderBookResult
import com.wavesplatform.transaction.assets.exchange.Order
Expand Down Expand Up @@ -58,10 +59,9 @@ case class BatchCancelCompleted(orders: Map[Order.Id, WrappedMatcherResponse])

case class OrderRejected(error: MatcherError) extends WrappedMatcherResponse(C.BadRequest, error)
case class OrderCancelRejected(error: MatcherError) extends WrappedMatcherResponse(C.BadRequest, error)
case object CancelRequestInvalidSignature extends WrappedMatcherResponse(C.BadRequest, MatcherError.CancelRequestInvalidSignature)
case object CancelRequestInvalidSignature extends WrappedMatcherResponse(C.BadRequest, error.RequestInvalidSignature)
case class NotImplemented(error: MatcherError) extends WrappedMatcherResponse(C.NotImplemented, error)
case object OperationTimedOut extends WrappedMatcherResponse(C.InternalServerError, MatcherError.OperationTimedOut)
case class OrderBookUnavailable(error: MatcherError) extends WrappedMatcherResponse(C.ServiceUnavailable, error)
case object DuringStart extends WrappedMatcherResponse(C.ServiceUnavailable, MatcherError.MatcherIsStarting)
case object DuringShutdown extends WrappedMatcherResponse(C.ServiceUnavailable, MatcherError.MatcherIsStopping)
case object DuringStart extends WrappedMatcherResponse(C.ServiceUnavailable, error.MatcherIsStarting)
case object DuringShutdown extends WrappedMatcherResponse(C.ServiceUnavailable, error.MatcherIsStopping)
case class InfoNotFound(error: MatcherError) extends WrappedMatcherResponse(C.NotFound, error)
Loading

0 comments on commit 205d243

Please sign in to comment.