Repository: softwaremill/elasticmq
Pull request #872 (2023-09-02 10:36:48)

Remove joda time


Patch:
diff --git a/NOTICE.txt b/NOTICE.txt
index 4ae86675..fe771a4e 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -5,7 +5,6 @@ This product uses the following libraries:
 
 For the core system:
 * scala (BSD-like license, http://www.scala-lang.org/node/146)
-* joda-time (Apache 2)
 * slf4j (MIT, http://www.slf4j.org/license.html)
 * logback (EPL 1.0/LGPL 2.1, http://logback.qos.ch/license.html)
 
diff --git a/build.sbt b/build.sbt
index eef64b45..41d72732 100644
--- a/build.sbt
+++ b/build.sbt
@@ -4,7 +4,7 @@ import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings
 import com.typesafe.sbt.packager.docker.{Cmd, ExecCmd}
 import sbt.Keys.javaOptions
 import sbt.internal.util.complete.Parsers.spaceDelimited
-import scoverage.ScoverageKeys._
+import scoverage.ScoverageKeys.*
 
 import scala.sys.process.Process
 
@@ -25,15 +25,13 @@ lazy val yarnTask = inputKey[Unit]("Run yarn with arguments")
 lazy val ensureDockerBuildx = taskKey[Unit]("Ensure that docker buildx configuration exists")
 lazy val dockerBuildWithBuildx = taskKey[Unit]("Build docker images using buildx")
 
-val jodaTime = "joda-time" % "joda-time" % "2.12.5"
-val jodaConvert = "org.joda" % "joda-convert" % "2.2.3"
 val config = "com.typesafe" % "config" % "1.4.2"
 val pureConfig = "com.github.pureconfig" %% "pureconfig" % "0.17.4"
 val scalaXml = "org.scala-lang.modules" %% "scala-xml" % "2.2.0"
 
 val scalalogging = "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
 val logback = "ch.qos.logback" % "logback-classic" % "1.3.11"
-val jclOverSlf4j = "org.slf4j" % "jcl-over-slf4j" % "2.0.7" // needed form amazon java sdk
+val jclOverSlf4j = "org.slf4j" % "jcl-over-slf4j" % "2.0.9" // needed form amazon java sdk
 
 val scalatest = "org.scalatest" %% "scalatest" % "3.2.16"
 val awaitility = "org.awaitility" % "awaitility-scala" % "4.2.0"
@@ -111,7 +109,7 @@ lazy val core: Project = (project in file("core"))
   .settings(
     Seq(
       name := "elasticmq-core",
-      libraryDependencies ++= Seq(jodaTime, jodaConvert, akka2Actor, akka2Testkit) ++ common,
+      libraryDependencies ++= Seq(akka2Actor, akka2Testkit) ++ common,
       coverageMinimumStmtTotal := 94
     )
   )
@@ -288,7 +286,6 @@ lazy val nativeServer: Project = (project in file("native-server"))
         "-H:IncludeResources=.*conf",
         "-H:IncludeResources=version",
         "-H:IncludeResources=.*\\.properties",
-        "-H:IncludeResources=org/joda/time/tz/data/.*",
         "-H:+ReportExceptionStackTraces",
         "-H:-ThrowUnsafeOffsetErrors",
         "--enable-http",
diff --git a/core/src/main/scala/org/elasticmq/CreateQueueData.scala b/core/src/main/scala/org/elasticmq/CreateQueueData.scala
index b126a302..e4ef1051 100644
--- a/core/src/main/scala/org/elasticmq/CreateQueueData.scala
+++ b/core/src/main/scala/org/elasticmq/CreateQueueData.scala
@@ -1,14 +1,14 @@
 package org.elasticmq
 
-import org.joda.time.{DateTime, Duration}
+import java.time.{Duration, OffsetDateTime}
 
 case class CreateQueueData(
     name: String,
     defaultVisibilityTimeout: Option[MillisVisibilityTimeout] = None,
     delay: Option[Duration] = None,
     receiveMessageWait: Option[Duration] = None,
-    created: Option[DateTime] = None,
-    lastModified: Option[DateTime] = None,
+    created: Option[OffsetDateTime] = None,
+    lastModified: Option[OffsetDateTime] = None,
     deadLettersQueue: Option[DeadLettersQueueData] = None,
     isFifo: Boolean = false,
     hasContentBasedDeduplication: Boolean = false,
@@ -17,14 +17,14 @@ case class CreateQueueData(
     tags: Map[String, String] = Map[String, String]()
 ) {
   def toQueueData: QueueData = {
-    val now = new DateTime()
+    val now = OffsetDateTime.now()
     QueueData(
       name,
       defaultVisibilityTimeout.getOrElse(
         MillisVisibilityTimeout.fromSeconds(CreateQueueDefaults.DefaultVisibilityTimeout)
       ),
-      delay.getOrElse(Duration.standardSeconds(CreateQueueDefaults.DefaultDelay)),
-      receiveMessageWait.getOrElse(Duration.standardSeconds(CreateQueueDefaults.DefaultReceiveMessageWait)),
+      delay.getOrElse(Duration.ofSeconds(CreateQueueDefaults.DefaultDelay)),
+      receiveMessageWait.getOrElse(Duration.ofSeconds(CreateQueueDefaults.DefaultReceiveMessageWait)),
       created.getOrElse(now),
       lastModified.getOrElse(now),
       deadLettersQueue,
diff --git a/core/src/main/scala/org/elasticmq/FifoDeduplicationIdsHistory.scala b/core/src/main/scala/org/elasticmq/FifoDeduplicationIdsHistory.scala
index 4d438d27..d34cc8e0 100644
--- a/core/src/main/scala/org/elasticmq/FifoDeduplicationIdsHistory.scala
+++ b/core/src/main/scala/org/elasticmq/FifoDeduplicationIdsHistory.scala
@@ -4,8 +4,8 @@ import com.typesafe.scalalogging.LazyLogging
 import org.elasticmq.FifoDeduplicationIdsHistory.DeduplicationIntervalMinutes
 import org.elasticmq.actor.queue.InternalMessage
 import org.elasticmq.util.NowProvider
-import org.joda.time.DateTime
 
+import java.time.OffsetDateTime
 import scala.annotation.tailrec
 
 /** Contains history of used Deduplication IDs associated with incoming messages to FIFO queues
@@ -75,4 +75,4 @@ object FifoDeduplicationIdsHistory {
   def newHistory(): FifoDeduplicationIdsHistory = FifoDeduplicationIdsHistory(Map.empty, List.empty)
 }
 
-case class DeduplicationIdWithCreationDate(id: DeduplicationId, creationDate: DateTime)
+case class DeduplicationIdWithCreationDate(id: DeduplicationId, creationDate: OffsetDateTime)
diff --git a/core/src/main/scala/org/elasticmq/MessageData.scala b/core/src/main/scala/org/elasticmq/MessageData.scala
index b47a37a2..206164e3 100644
--- a/core/src/main/scala/org/elasticmq/MessageData.scala
+++ b/core/src/main/scala/org/elasticmq/MessageData.scala
@@ -1,6 +1,6 @@
 package org.elasticmq
 
-import org.joda.time.DateTime
+import java.time.OffsetDateTime
 
 case class MessageData(
     id: MessageId,
@@ -8,7 +8,7 @@ case class MessageData(
     content: String,
     messageAttributes: Map[String, MessageAttribute],
     nextDelivery: MillisNextDelivery,
-    created: DateTime,
+    created: OffsetDateTime,
     statistics: MessageStatistics,
     messageGroupId: Option[String],
     messageDeduplicationId: Option[DeduplicationId],
diff --git a/core/src/main/scala/org/elasticmq/MessageStatistics.scala b/core/src/main/scala/org/elasticmq/MessageStatistics.scala
index 46cbe812..61a93ea6 100644
--- a/core/src/main/scala/org/elasticmq/MessageStatistics.scala
+++ b/core/src/main/scala/org/elasticmq/MessageStatistics.scala
@@ -1,6 +1,6 @@
 package org.elasticmq
 
-import org.joda.time.DateTime
+import java.time.OffsetDateTime
 
 case class MessageStatistics(approximateFirstReceive: Received, approximateReceiveCount: Int)
 
@@ -9,5 +9,5 @@ object MessageStatistics {
 }
 
 sealed trait Received
-case class OnDateTimeReceived(when: DateTime) extends Received
+case class OnDateTimeReceived(when: OffsetDateTime) extends Received
 case object NeverReceived extends Received
diff --git a/core/src/main/scala/org/elasticmq/QueueData.scala b/core/src/main/scala/org/elasticmq/QueueData.scala
index c36eb39f..5a61c821 100644
--- a/core/src/main/scala/org/elasticmq/QueueData.scala
+++ b/core/src/main/scala/org/elasticmq/QueueData.scala
@@ -1,14 +1,14 @@
 package org.elasticmq
 
-import org.joda.time.{Duration, DateTime}
+import java.time.{Duration, OffsetDateTime}
 
 case class QueueData(
     name: String,
     defaultVisibilityTimeout: MillisVisibilityTimeout,
     delay: Duration,
     receiveMessageWait: Duration,
-    created: DateTime,
-    lastModified: DateTime,
+    created: OffsetDateTime,
+    lastModified: OffsetDateTime,
     deadLettersQueue: Option[DeadLettersQueueData] = None,
     isFifo: Boolean = false,
     hasContentBasedDeduplication: Boolean = false,
diff --git a/core/src/main/scala/org/elasticmq/actor/queue/InternalMessage.scala b/core/src/main/scala/org/elasticmq/actor/queue/InternalMessage.scala
index 6c5a4895..3ad666ec 100644
--- a/core/src/main/scala/org/elasticmq/actor/queue/InternalMessage.scala
+++ b/core/src/main/scala/org/elasticmq/actor/queue/InternalMessage.scala
@@ -1,11 +1,10 @@
 package org.elasticmq.actor.queue
 
-import java.util.UUID
-
 import org.elasticmq._
 import org.elasticmq.util.NowProvider
-import org.joda.time.DateTime
 
+import java.time.OffsetDateTime
+import java.util.UUID
 import scala.collection.mutable
 
 case class InternalMessage(
@@ -15,7 +14,7 @@ case class InternalMessage(
     content: String,
     messageAttributes: Map[String, MessageAttribute],
     messageSystemAttributes: Map[String, MessageAttribute],
-    created: DateTime,
+    created: OffsetDateTime,
     orderIndex: Int,
     var firstReceive: Received,
     var receiveCount: Int,
@@ -52,7 +51,7 @@ case class InternalMessage(
     receiveCount += 1
 
     if (firstReceive == NeverReceived) {
-      firstReceive = OnDateTimeReceived(new DateTime(nowProvider.nowMillis))
+      firstReceive = OnDateTimeReceived(nowProvider.now)
     }
   }
 
@@ -95,11 +94,11 @@ object InternalMessage {
     new InternalMessage(
       newMessageData.id.getOrElse(generateId()).id,
       mutable.Buffer.empty,
-      newMessageData.nextDelivery.toMillis(now, queueData.delay.getMillis).millis,
+      newMessageData.nextDelivery.toMillis(now, queueData.delay.toMillis).millis,
       newMessageData.content,
       newMessageData.messageAttributes,
       newMessageData.messageSystemAttributes,
-      new DateTime(),
+      OffsetDateTime.now(),
       newMessageData.orderIndex,
       NeverReceived,
       0,
diff --git a/core/src/main/scala/org/elasticmq/actor/queue/QueueActorWaitForMessagesOps.scala b/core/src/main/scala/org/elasticmq/actor/queue/QueueActorWaitForMessagesOps.scala
index 20d3d8d6..5b19557f 100644
--- a/core/src/main/scala/org/elasticmq/actor/queue/QueueActorWaitForMessagesOps.scala
+++ b/core/src/main/scala/org/elasticmq/actor/queue/QueueActorWaitForMessagesOps.scala
@@ -3,8 +3,8 @@ package org.elasticmq.actor.queue
 import akka.actor.{ActorRef, Cancellable}
 import org.elasticmq.actor.reply._
 import org.elasticmq.msg.{QueueMessageMsg, ReceiveMessages, SendMessage, UpdateVisibilityTimeout}
-import org.joda.time.Duration
 
+import java.time.Duration
 import scala.annotation.tailrec
 import scala.concurrent.{duration => scd}
 
@@ -41,7 +41,7 @@ trait QueueActorWaitForMessagesOps extends ReplyingActor with QueueActorMessageO
         val result = receiveMessages(visibilityTimeout, count, receiveRequestAttemptId)
         val waitForMessages = waitForMessagesOpt.getOrElse(queueData.receiveMessageWait)
 
-        if (result.result.contains(Nil) && waitForMessages.getMillis > 0) {
+        if (result.result.contains(Nil) && waitForMessages.toMillis > 0) {
           val seq = assignSequenceFor(rm, context.sender())
           logger.debug(s"${queueData.name}: Awaiting messages: start for sequence $seq.")
           scheduleTimeoutReply(seq, waitForMessages)
@@ -90,7 +90,7 @@ trait QueueActorWaitForMessagesOps extends ReplyingActor with QueueActorMessageO
   }
 
   private def scheduleTimeoutReply(seq: Long, waitForMessages: Duration): Unit = {
-    schedule(waitForMessages.getMillis, ReplyIfTimeout(seq, Nil))
+    schedule(waitForMessages.toMillis, ReplyIfTimeout(seq, Nil))
   }
 
   private def scheduleTryReplyWhenAvailable(): Unit = {
diff --git a/core/src/main/scala/org/elasticmq/msg/QueueMsg.scala b/core/src/main/scala/org/elasticmq/msg/QueueMsg.scala
index 532c3287..9cba7e95 100644
--- a/core/src/main/scala/org/elasticmq/msg/QueueMsg.scala
+++ b/core/src/main/scala/org/elasticmq/msg/QueueMsg.scala
@@ -4,7 +4,8 @@ import akka.actor.ActorRef
 import org.elasticmq._
 import org.elasticmq.actor.queue.InternalMessage
 import org.elasticmq.actor.reply.Replyable
-import org.joda.time.Duration
+
+import java.time.Duration
 
 sealed trait QueueMsg[T] extends Replyable[T]
 
diff --git a/core/src/main/scala/org/elasticmq/util/NowProvider.scala b/core/src/main/scala/org/elasticmq/util/NowProvider.scala
index 83efff4a..91633653 100644
--- a/core/src/main/scala/org/elasticmq/util/NowProvider.scala
+++ b/core/src/main/scala/org/elasticmq/util/NowProvider.scala
@@ -1,8 +1,8 @@
 package org.elasticmq.util
 
-import org.joda.time.DateTime
+import java.time.OffsetDateTime
 
 class NowProvider {
   def nowMillis: Long = System.currentTimeMillis()
-  def now = new DateTime()
+  def now: OffsetDateTime = OffsetDateTime.now()
 }
diff --git a/core/src/main/scala/org/elasticmq/util/OffsetDateTimeUtil.scala b/core/src/main/scala/org/elasticmq/util/OffsetDateTimeUtil.scala
new file mode 100644
index 00000000..bc21db95
--- /dev/null
+++ b/core/src/main/scala/org/elasticmq/util/OffsetDateTimeUtil.scala
@@ -0,0 +1,8 @@
+package org.elasticmq.util
+
+import java.time.{Instant, OffsetDateTime, ZoneOffset}
+
+object OffsetDateTimeUtil {
+
+  def ofEpochMilli(millis: Long): OffsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC)
+}
diff --git a/native-server/src/main/resources/META-INF/native-image/resource-config.json b/native-server/src/main/resources/META-INF/native-image/resource-config.json
index 7cc600d2..87b8d8f9 100644
--- a/native-server/src/main/resources/META-INF/native-image/resource-config.json
+++ b/native-server/src/main/resources/META-INF/native-image/resource-config.json
@@ -24,13 +24,7 @@
     }, 
     {
       "pattern":"\\Qorg/h2/util/data.zip\\E"
-    }, 
-    {
-      "pattern":"\\Qorg/joda/time/tz/data/Europe/Warsaw\\E"
-    }, 
-    {
-      "pattern":"\\Qorg/joda/time/tz/data/ZoneInfoMap\\E"
-    }, 
+    },
     {
       "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
     }, 
diff --git a/persistence/persistence-core/src/main/scala/org/elasticmq/persistence/CreateQueueMetadata.scala b/persistence/persistence-core/src/main/scala/org/elasticmq/persistence/CreateQueueMetadata.scala
index 687062d6..20028fa7 100644
--- a/persistence/persistence-core/src/main/scala/org/elasticmq/persistence/CreateQueueMetadata.scala
+++ b/persistence/persistence-core/src/main/scala/org/elasticmq/persistence/CreateQueueMetadata.scala
@@ -1,7 +1,9 @@
 package org.elasticmq.persistence
 
+import org.elasticmq.util.OffsetDateTimeUtil
 import org.elasticmq.{CreateQueueData, DeadLettersQueueData, MillisVisibilityTimeout, QueueData}
-import org.joda.time.{DateTime, Duration}
+
+import java.time.Duration
 
 case class CreateQueueMetadata(
     name: String,
@@ -22,10 +24,10 @@ case class CreateQueueMetadata(
     CreateQueueData(
       name = name,
       defaultVisibilityTimeoutSeconds.map(sec => MillisVisibilityTimeout.fromSeconds(sec)),
-      delaySeconds.map(sec => Duration.standardSeconds(sec)),
-      receiveMessageWaitSeconds.map(sec => Duration.standardSeconds(sec)),
-      created = Some(new DateTime(created)),
-      lastModified = Some(new DateTime(lastModified)),
+      delaySeconds.map(sec => Duration.ofSeconds(sec)),
+      receiveMessageWaitSeconds.map(sec => Duration.ofSeconds(sec)),
+      created = Some(OffsetDateTimeUtil.ofEpochMilli(created)),
+      lastModified = Some(OffsetDateTimeUtil.ofEpochMilli(lastModified)),
       deadLettersQueue = deadLettersQueue.map(dlq => DeadLettersQueueData(dlq.name, dlq.maxReceiveCount)),
       isFifo = isFifo,
       hasContentBasedDeduplication = hasContentBasedDeduplication,
@@ -42,10 +44,10 @@ object CreateQueueMetadata {
     CreateQueueMetadata(
       queueData.name,
       Some(queueData.defaultVisibilityTimeout.seconds),
-      Some(queueData.delay.getStandardSeconds),
-      Some(queueData.receiveMessageWait.getStandardSeconds),
-      queueData.created.toInstant.getMillis,
-      queueData.lastModified.toInstant.getMillis,
+      Some(queueData.delay.getSeconds),
+      Some(queueData.receiveMessageWait.getSeconds),
+      queueData.created.toInstant.toEpochMilli,
+      queueData.lastModified.toInstant.toEpochMilli,
       queueData.deadLettersQueue.map(dlq => DeadLettersQueue(dlq.name, dlq.maxReceiveCount)),
       queueData.isFifo,
       queueData.hasContentBasedDeduplication,
diff --git a/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueueConfigUtil.scala b/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueueConfigUtil.scala
index 53384298..00cf8055 100644
--- a/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueueConfigUtil.scala
+++ b/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueueConfigUtil.scala
@@ -3,9 +3,9 @@ package org.elasticmq.persistence.file
 import com.typesafe.config.{Config, ConfigFactory, ConfigObject, ConfigValue}
 import org.elasticmq.persistence.{CreateQueueMetadata, DeadLettersQueue, QueueSorter}
 import org.elasticmq.util.Logging
-import org.joda.time.DateTime
 
 import java.io.File
+import java.time.OffsetDateTime
 import java.util.concurrent.TimeUnit
 import scala.collection.JavaConverters._
 import scala.util.{Failure, Success, Try}
@@ -56,7 +56,7 @@ object QueueConfigUtil extends Logging {
 
     val deadLettersQueueKey = "deadLettersQueue"
 
-    val now = new DateTime().toInstant.getMillis
+    val now = OffsetDateTime.now().toInstant.toEpochMilli
 
     val unsortedQueues = queuesConfig.map { case (n, v) =>
       val c = v.asInstanceOf[ConfigObject].toConfig
diff --git a/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueuePersister.scala b/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueuePersister.scala
index 1fa95a1f..1d1c78bd 100644
--- a/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueuePersister.scala
+++ b/persistence/persistence-file/src/main/scala/org/elasticmq/persistence/file/QueuePersister.scala
@@ -42,8 +42,8 @@ object QueuePersister {
   private def toQueueConfig(queue: QueueData): QueueMetadata =
     QueueMetadata(
       queue.defaultVisibilityTimeout.millis,
-      queue.delay.getMillis,
-      queue.receiveMessageWait.getMillis,
+      queue.delay.toMillis,
+      queue.receiveMessageWait.toMillis,
       queue.deadLettersQueue,
       queue.isFifo,
       queue.hasContentBasedDeduplication,
diff --git a/persistence/persistence-sql/src/main/scala/org/elasticmq/persistence/sql/DBMessage.scala b/persistence/persistence-sql/src/main/scala/org/elasticmq/persistence/sql/DBMessage.scala
index ed7acae7..55ec5847 100644
--- a/persistence/persistence-sql/src/main/scala/org/elasticmq/persistence/sql/DBMessage.scala
+++ b/persistence/persistence-sql/src/main/scala/org/elasticmq/persistence/sql/DBMessage.scala
@@ -3,7 +3,7 @@ package org.elasticmq.persistence.sql
 import org.elasticmq._
 import org.elasticmq.actor.queue.InternalMessage
 import org.elasticmq.persistence.sql.SerializableAttributeProtocol._
-import org.joda.time.DateTime
+import org.elasticmq.util.OffsetDateTimeUtil
 import scalikejdbc.WrappedResultSet
 import spray.json._
 
@@ -39,7 +39,7 @@ case class DBMessage(
 
     val serializedDeliveryReceipts = new String(deliveryReceipts).parseJson.convertTo[List[String]]
 
-    val firstReceive = received.map(time => OnDateTimeReceived(new DateTime(time))).getOrElse(NeverReceived)
+    val firstReceive = received.map(time => OnDateTimeReceived(OffsetDateTimeUtil.ofEpochMilli(time))).getOrElse(NeverReceived)
 
     InternalMessage(
       messageId,
@@ -48,7 +48,7 @@ case class DBMessage(
       new String(content),
       serializedAttrs,
       Map.empty,
-      new DateTime(created),
+      OffsetDateTimeUtil.ofEpochMilli(created),
       orderIndex = 0,
       firstReceive,
       receiveCount,
@@ -93,7 +93,7 @@ object DBMessage {
 
     val received = message.firstReceive match {
       case NeverReceived            => None
-      case OnDateTimeReceived(when) => Some(when.toInstant.getMillis)
+      case OnDateTimeReceived(when) => Some(when.toInstant.toEpochMilli)
     }
 
     val deduplicationId = message.messageDeduplicationId.map(_.id)
@@ -104,7 +104,7 @@ object DBMessage {
       message.nextDelivery,
       message.content.getBytes,
       attributes.toJson.toString.getBytes,
-      message.created.toInstant.getMillis,
+      message.created.toInstant.toEpochMilli,
       received,
       message.receiveCount,
       message.messageGroupId,
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/CreateQueueDirectives.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/CreateQueueDirectives.scala
index a9f94fd3..f3c04971 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/CreateQueueDirectives.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/CreateQueueDirectives.scala
@@ -9,11 +9,11 @@ import org.elasticmq.rest.sqs.ParametersUtil._
 import org.elasticmq.rest.sqs.directives.ElasticMQDirectives
 import org.elasticmq.rest.sqs.model.RedrivePolicy.BackwardCompatibleRedrivePolicy
 import org.elasticmq.rest.sqs.model.RequestPayload
-import org.joda.time.Duration
 import spray.json.DefaultJsonProtocol._
 import spray.json.JsonParser.ParsingException
 import spray.json._
 
+import java.time.Duration
 import scala.async.Async._
 import scala.concurrent.Future
 import scala.xml.Elem
@@ -65,8 +65,8 @@ trait CreateQueueDirectives {
           val newQueueData = CreateQueueData(
             requestParams.QueueName,
             secondsVisibilityTimeoutOpt.map(sec => MillisVisibilityTimeout.fromSeconds(sec)),
-            secondsDelayOpt.map(sec => Duration.standardSeconds(sec)),
-            secondsReceiveMessageWaitTimeOpt.map(sec => Duration.standardSeconds(sec)),
+            secondsDelayOpt.map(sec => Duration.ofSeconds(sec)),
+            secondsReceiveMessageWaitTimeOpt.map(sec => Duration.ofSeconds(sec)),
             None,
             None,
             redrivePolicy.map(rd => DeadLettersQueueData(rd.queueName, rd.maxReceiveCount)),
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/QueueAttributesOps.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/QueueAttributesOps.scala
index 6586bf33..3d7346d5 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/QueueAttributesOps.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/QueueAttributesOps.scala
@@ -10,10 +10,10 @@ import org.elasticmq.rest.sqs.model.RedrivePolicy.BackwardCompatibleRedrivePolic
 import org.elasticmq.rest.sqs.model.RedrivePolicyJson._
 import org.elasticmq.util.Logging
 import org.elasticmq.{DeadLettersQueueData, MillisVisibilityTimeout, QueueData}
-import org.joda.time.Duration
 import spray.json.JsonParser.ParsingException
 import spray.json._
 
+import java.time.Duration
 import scala.async.Async.{async, await}
 import scala.concurrent.{ExecutionContext, Future}
 
@@ -49,7 +49,7 @@ trait QueueAttributesOps extends AttributesModule {
           () => Future.successful(queueData.defaultVisibilityTimeout.seconds.toString)
         ),
         AttributeValuesCalculator
-          .Rule(DelaySecondsAttribute, () => Future.successful(queueData.delay.getStandardSeconds.toString)),
+          .Rule(DelaySecondsAttribute, () => Future.successful(queueData.delay.getSeconds.toString)),
         AttributeValuesCalculator
           .Rule(ApproximateNumberOfMessagesAttribute, () => stats.map(_.approximateNumberOfVisibleMessages.toString)),
         AttributeValuesCalculator.Rule(
@@ -61,14 +61,14 @@ trait QueueAttributesOps extends AttributesModule {
           () => stats.map(_.approximateNumberOfMessagesDelayed.toString)
         ),
         AttributeValuesCalculator
-          .Rule(CreatedTimestampAttribute, () => Future.successful((queueData.created.getMillis / 1000L).toString)),
+          .Rule(CreatedTimestampAttribute, () => Future.successful((queueData.created.toInstant.toEpochMilli / 1000L).toString)),
         AttributeValuesCalculator.Rule(
           LastModifiedTimestampAttribute,
-          () => Future.successful((queueData.lastModified.getMillis / 1000L).toString)
+          () => Future.successful((queueData.lastModified.toInstant.toEpochMilli / 1000L).toString)
         ),
         AttributeValuesCalculator.Rule(
           ReceiveMessageWaitTimeSecondsAttribute,
-          () => Future.successful(queueData.receiveMessageWait.getStandardSeconds.toString)
+          () => Future.successful(queueData.receiveMessageWait.getSeconds.toString)
         ),
         AttributeValuesCalculator.Rule(
           QueueArnAttribute,
@@ -116,9 +116,9 @@ trait QueueAttributesOps extends AttributesModule {
             MillisVisibilityTimeout.fromSeconds(attributeValue.toLong)
           )
         case DelaySecondsAttribute =>
-          queueActor ? UpdateQueueDelay(Duration.standardSeconds(attributeValue.toLong))
+          queueActor ? UpdateQueueDelay(Duration.ofSeconds(attributeValue.toLong))
         case ReceiveMessageWaitTimeSecondsAttribute =>
-          queueActor ? UpdateQueueReceiveMessageWait(Duration.standardSeconds(attributeValue.toLong))
+          queueActor ? UpdateQueueReceiveMessageWait(Duration.ofSeconds(attributeValue.toLong))
 
         case RedrivePolicyParameter =>
           val redrivePolicy =
diff --git a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/ReceiveMessageDirectives.scala b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/ReceiveMessageDirectives.scala
index 7c5076fe..09848b73 100644
--- a/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/ReceiveMessageDirectives.scala
+++ b/rest/rest-sqs/src/main/scala/org/elasticmq/rest/sqs/ReceiveMessageDirectives.scala
@@ -9,11 +9,10 @@ import org.elasticmq.rest.sqs.Constants._
 import org.elasticmq.rest.sqs.MD5Util._
 import org.elasticmq.rest.sqs.directives.ElasticMQDirectives
 import org.elasticmq.rest.sqs.model.RequestPayload
-import org.joda.time.Duration
-import spray.json.DefaultJsonProtocol.{StringJsonFormat, jsonFormat7}
-import spray.json.RootJsonFormat
 import spray.json.DefaultJsonProtocol._
+import spray.json.RootJsonFormat
 
+import java.time.Duration
 import scala.xml.Elem
 
 trait ReceiveMessageDirectives {
@@ -72,7 +71,7 @@ trait ReceiveMessageDirectives {
           maxNumberOfMessagesAttributeOpt.getOrElse(1)
 
         val waitTimeSecondsFromParameters =
-          waitTimeSecondsAttributeOpt.map(Duration.standardSeconds)
+          waitTimeSecondsAttributeOpt.map(Duration.ofSeconds)
 
         val messageAttributeNames = requestParameters.MessageAttributeNames.getOrElse(List.empty)
 
@@ -100,7 +99,7 @@ trait ReceiveMessageDirectives {
           possiblyEmptyAttributeValuesCalculator.calculate[String](
             attributeNames,
             Rule(SenderIdAttribute, () => Some("127.0.0.1")),
-            Rule(SentTimestampAttribute, () => Some(msg.created.getMillis.toString)),
+            Rule(SentTimestampAttribute, () => Some(msg.created.toInstant.toEpochMilli.toString)),
             Rule(ApproximateReceiveCountAttribute, () => Some(msg.statistics.approximateReceiveCount.toString)),
             Rule(MessageDeduplicationIdAttribute, () => msg.messageDeduplicationId.map(_.id)),
             Rule(MessageGroupIdAttribute, () => msg.messageGroupId),
@@ -109,7 +108,7 @@ trait ReceiveMessageDirectives {
               () =>
                 Some((msg.statistics.approximateFirstReceive match {
                   case NeverReceived            => 0
-                  case OnDateTimeReceived(when) => when.getMillis
+                  case OnDateTimeReceived(when) => when.toInstant.toEpochMilli
                 }).toString)
             ),
             Rule(AWSTraceHeaderAttribute, () => msg.tracingId.map(_.id)),


Test patch:
diff --git a/core/src/test/scala/org/elasticmq/FifoDeduplicationIdsHistoryTest.scala b/core/src/test/scala/org/elasticmq/FifoDeduplicationIdsHistoryTest.scala
index b0babfd4..e59028ba 100644
--- a/core/src/test/scala/org/elasticmq/FifoDeduplicationIdsHistoryTest.scala
+++ b/core/src/test/scala/org/elasticmq/FifoDeduplicationIdsHistoryTest.scala
@@ -2,10 +2,10 @@ package org.elasticmq
 
 import org.elasticmq.actor.queue.InternalMessage
 import org.elasticmq.util.NowProvider
-import org.joda.time.DateTime
 import org.scalatest.funsuite.AnyFunSuite
 import org.scalatest.matchers.should.Matchers
 
+import java.time.OffsetDateTime
 import scala.collection.mutable
 
 class FifoDeduplicationIdsHistoryTest extends AnyFunSuite with Matchers {
@@ -13,10 +13,10 @@ class FifoDeduplicationIdsHistoryTest extends AnyFunSuite with Matchers {
   test("Should store added deduplication IDs") {
     val history = FifoDeduplicationIdsHistory.newHistory()
     val updatedHistory = history
-      .addNew(newInternalMessage(Some(DeduplicationId("1")), DateTime.now()))
-      .addNew(newInternalMessage(Some(DeduplicationId("2")), DateTime.now()))
-      .addNew(newInternalMessage(Some(DeduplicationId("3")), DateTime.now()))
-      .addNew(newInternalMessage(Some(DeduplicationId("4")), DateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("1")), OffsetDateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("2")), OffsetDateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("3")), OffsetDateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("4")), OffsetDateTime.now()))
 
     updatedHistory.messagesByDeduplicationId.keys shouldBe Set(
       DeduplicationId("1"),
@@ -30,26 +30,26 @@ class FifoDeduplicationIdsHistoryTest extends AnyFunSuite with Matchers {
 
   test("History should not override given deduplication ID entry if such one already exists") {
     val history = FifoDeduplicationIdsHistory.newHistory()
-    val dateTimeInPast = DateTime.now().minusMinutes(10)
+    val OffsetDateTimeInPast = OffsetDateTime.now().minusMinutes(10)
     val updatedHistory = history
-      .addNew(newInternalMessage(Some(DeduplicationId("1")), dateTimeInPast))
-      .addNew(newInternalMessage(Some(DeduplicationId("1")), DateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("1")), OffsetDateTimeInPast))
+      .addNew(newInternalMessage(Some(DeduplicationId("1")), OffsetDateTime.now()))
 
     updatedHistory.messagesByDeduplicationId.keys shouldBe Set(
       DeduplicationId("1")
     )
     updatedHistory.deduplicationIdsByCreationDate shouldBe List(
-      DeduplicationIdWithCreationDate(DeduplicationId("1"), dateTimeInPast)
+      DeduplicationIdWithCreationDate(DeduplicationId("1"), OffsetDateTimeInPast)
     )
   }
 
   test("History should show if given deduplication ID was already used or not") {
     val history = FifoDeduplicationIdsHistory.newHistory()
     val updatedHistory = history
-      .addNew(newInternalMessage(Some(DeduplicationId("1")), DateTime.now()))
-      .addNew(newInternalMessage(Some(DeduplicationId("2")), DateTime.now()))
-      .addNew(newInternalMessage(Some(DeduplicationId("3")), DateTime.now()))
-      .addNew(newInternalMessage(Some(DeduplicationId("4")), DateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("1")), OffsetDateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("2")), OffsetDateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("3")), OffsetDateTime.now()))
+      .addNew(newInternalMessage(Some(DeduplicationId("4")), OffsetDateTime.now()))
 
     updatedHistory.wasRegistered(Some(DeduplicationId("1"))) should be(defined)
     updatedHistory.wasRegistered(Some(DeduplicationId("4"))) should be(defined)
@@ -59,7 +59,7 @@ class FifoDeduplicationIdsHistoryTest extends AnyFunSuite with Matchers {
 
   test("History should erase all entries that were created 5 or more minutes ago") {
     val history = FifoDeduplicationIdsHistory.newHistory()
-    val now = DateTime.now()
+    val now = OffsetDateTime.now()
     val updatedHistory = history
       .addNew(newInternalMessage(Some(DeduplicationId("1")), now.minusMinutes(20)))
       .addNew(newInternalMessage(Some(DeduplicationId("2")), now.minusMinutes(10)))
@@ -77,7 +77,7 @@ class FifoDeduplicationIdsHistoryTest extends AnyFunSuite with Matchers {
 
   test("Cleaning outdated IDs should stop at first ID which was created in last 5 minutes") {
     val history = FifoDeduplicationIdsHistory.newHistory()
-    val now = DateTime.now()
+    val now = OffsetDateTime.now()
     val updatedHistory = history
       .addNew(newInternalMessage(Some(DeduplicationId("1")), now.minusMinutes(20)))
       .addNew(newInternalMessage(Some(DeduplicationId("2")), now.minusMinutes(4)))
@@ -100,7 +100,7 @@ class FifoDeduplicationIdsHistoryTest extends AnyFunSuite with Matchers {
     )
   }
 
-  def newInternalMessage(maybeDeduplicationId: Option[DeduplicationId], created: DateTime): InternalMessage =
+  def newInternalMessage(maybeDeduplicationId: Option[DeduplicationId], created: OffsetDateTime): InternalMessage =
     InternalMessage(
       id = "1",
       deliveryReceipts = mutable.Buffer.empty,
diff --git a/core/src/test/scala/org/elasticmq/actor/DeduplicationTimerTest.scala b/core/src/test/scala/org/elasticmq/actor/DeduplicationTimerTest.scala
index b7c674ad..03f21ac5 100644
--- a/core/src/test/scala/org/elasticmq/actor/DeduplicationTimerTest.scala
+++ b/core/src/test/scala/org/elasticmq/actor/DeduplicationTimerTest.scala
@@ -4,10 +4,10 @@ import org.elasticmq._
 import org.elasticmq.actor.reply._
 import org.elasticmq.actor.test.{ActorTest, DataCreationHelpers, QueueManagerForEachTest}
 import org.elasticmq.msg._
-import org.joda.time.DateTime
 import org.scalatest.EitherValues
 import org.scalatest.matchers.should.Matchers
 
+import java.time.OffsetDateTime
 import scala.concurrent.Await
 import scala.concurrent.duration.DurationInt
 
@@ -33,7 +33,7 @@ class DeduplicationTimerTest
       } yield (fifoQueue, firstLookup),
       1.second
     )
-    nowProvider.mutableNowMillis.set(DateTime.now().plusMinutes(5).plusSeconds(1).getMillis)
+    nowProvider.mutableNowMillis.set(OffsetDateTime.now().plusMinutes(5).plusSeconds(1).toInstant.toEpochMilli)
 
     // Timer is scheduled to run each second, to eliminate random time errors we are waiting a little bit longer
     Thread.sleep(1500)
diff --git a/core/src/test/scala/org/elasticmq/actor/QueueActorMsgOpsTest.scala b/core/src/test/scala/org/elasticmq/actor/QueueActorMsgOpsTest.scala
index e0ed81bb..a8c94cc8 100644
--- a/core/src/test/scala/org/elasticmq/actor/QueueActorMsgOpsTest.scala
+++ b/core/src/test/scala/org/elasticmq/actor/QueueActorMsgOpsTest.scala
@@ -1,10 +1,12 @@
 package org.elasticmq.actor
 
-import org.elasticmq.actor.reply._
 import org.elasticmq._
-import org.elasticmq.msg._
+import org.elasticmq.actor.reply._
 import org.elasticmq.actor.test.{ActorTest, DataCreationHelpers, QueueManagerForEachTest}
-import org.joda.time.{DateTime, Duration}
+import org.elasticmq.msg._
+import org.elasticmq.util.OffsetDateTimeUtil
+
+import java.time.Duration
 
 class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with DataCreationHelpers {
 
@@ -290,8 +292,8 @@ class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with D
     } yield {
       // Then
       lookupResult.statistics should be(MessageStatistics.empty)
-      receiveResult1.statistics should be(MessageStatistics(OnDateTimeReceived(new DateTime(100L)), 1))
-      receiveResult2.statistics should be(MessageStatistics(OnDateTimeReceived(new DateTime(100L)), 2))
+      receiveResult1.statistics should be(MessageStatistics(OnDateTimeReceived(OffsetDateTimeUtil.ofEpochMilli(100L)), 1))
+      receiveResult2.statistics should be(MessageStatistics(OnDateTimeReceived(OffsetDateTimeUtil.ofEpochMilli(100L)), 2))
     }
   }
 
@@ -352,7 +354,7 @@ class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with D
       Right(queueActor) <- queueManagerActor ? CreateQueue(q1)
 
       // When
-      receiveResults <- queueActor ? ReceiveMessages(DefaultVisibilityTimeout, 5, Some(Duration.millis(500L)), None)
+      receiveResults <- queueActor ? ReceiveMessages(DefaultVisibilityTimeout, 5, Some(Duration.ofMillis(500L)), None)
     } yield {
       // Then
       val end = System.currentTimeMillis()
@@ -375,7 +377,7 @@ class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with D
       receiveResultsFuture = queueActor ? ReceiveMessages(
         DefaultVisibilityTimeout,
         5,
-        Some(Duration.millis(1000L)),
+        Some(Duration.ofMillis(1000L)),
         None
       )
       _ <- {
@@ -404,13 +406,13 @@ class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with D
       receiveResults1Future = queueActor ? ReceiveMessages(
         DefaultVisibilityTimeout,
         5,
-        Some(Duration.millis(1000L)),
+        Some(Duration.ofMillis(1000L)),
         None
       )
       receiveResults2Future = queueActor ? ReceiveMessages(
         DefaultVisibilityTimeout,
         5,
-        Some(Duration.millis(1000L)),
+        Some(Duration.ofMillis(1000L)),
         None
       )
 
@@ -443,19 +445,19 @@ class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with D
       receiveResults1Future = queueActor ? ReceiveMessages(
         DefaultVisibilityTimeout,
         5,
-        Some(Duration.millis(1000L)),
+        Some(Duration.ofMillis(1000L)),
         None
       )
       receiveResults2Future = queueActor ? ReceiveMessages(
         DefaultVisibilityTimeout,
         5,
-        Some(Duration.millis(1000L)),
+        Some(Duration.ofMillis(1000L)),
         None
       )
       receiveResults3Future = queueActor ? ReceiveMessages(
         DefaultVisibilityTimeout,
         5,
-        Some(Duration.millis(1000L)),
+        Some(Duration.ofMillis(1000L)),
         None
       )
 
@@ -585,7 +587,7 @@ class QueueActorMsgOpsTest extends ActorTest with QueueManagerForEachTest with D
       _ <- queueActor ? SendMessage(m1)
       _ = nowProvider.mutableNowMillis.set(1000L)
       resultsOriginal <- queueActor ? ReceiveMessages(DefaultVisibilityTimeout, 5, None, None)
-      resultsCopied <- copyToActor ? ReceiveMessages(DefaultVisibilityTimeout, 5, Some(Duration.millis(500)), None)
+      resultsCopied <- copyToActor ? ReceiveMessages(DefaultVisibilityTimeout, 5, Some(Duration.ofMillis(500)), None)
     } yield {
       // Then
       resultsOriginal.map(_.id) should be(List(MessageId(m1ID)))
diff --git a/core/src/test/scala/org/elasticmq/actor/QueueActorQueueOpsTest.scala b/core/src/test/scala/org/elasticmq/actor/QueueActorQueueOpsTest.scala
index d0f9dd1d..b37b21d4 100644
--- a/core/src/test/scala/org/elasticmq/actor/QueueActorQueueOpsTest.scala
+++ b/core/src/test/scala/org/elasticmq/actor/QueueActorQueueOpsTest.scala
@@ -4,14 +4,16 @@ import org.elasticmq._
 import org.elasticmq.actor.reply._
 import org.elasticmq.actor.test.{ActorTest, DataCreationHelpers, QueueManagerForEachTest}
 import org.elasticmq.msg._
-import org.joda.time.{DateTime, Duration}
+import org.elasticmq.util.OffsetDateTimeUtil
+
+import java.time.Duration
 
 class QueueActorQueueOpsTest extends ActorTest with QueueManagerForEachTest with DataCreationHelpers {
 
   test("queue modified and created dates should be stored") {
     // Given
-    val created = new DateTime(1216168602L)
-    val lastModified = new DateTime(1316168602L)
+    val created = OffsetDateTimeUtil.ofEpochMilli(1216168602L)
+    val lastModified = OffsetDateTimeUtil.ofEpochMilli(1316168602L)
 
     for {
       Right(queueActor) <- queueManagerActor ? CreateQueue(
diff --git a/core/src/test/scala/org/elasticmq/actor/queue/InternalMessageSpec.scala b/core/src/test/scala/org/elasticmq/actor/queue/InternalMessageSpec.scala
index 7bc31a6e..a7fc9eec 100644
--- a/core/src/test/scala/org/elasticmq/actor/queue/InternalMessageSpec.scala
+++ b/core/src/test/scala/org/elasticmq/actor/queue/InternalMessageSpec.scala
@@ -1,15 +1,16 @@
 package org.elasticmq.actor.queue
 
 import org.elasticmq.NeverReceived
-import org.joda.time.DateTime
-
-import scala.collection.mutable
 import org.scalatest.funsuite.AnyFunSuite
 import org.scalatest.matchers.should.Matchers
 
+import java.time.OffsetDateTime
+import java.time.temporal.ChronoUnit
+import scala.collection.mutable
+
 class InternalMessageSpec extends AnyFunSuite with Matchers {
   test("Internal FIFO message should be ordered by their creation time") {
-    val freezedDateTime = new DateTime()
+    val freezedDateTime = OffsetDateTime.now()
 
     val first = InternalMessage(
       id = "id",
@@ -30,14 +31,14 @@ class InternalMessageSpec extends AnyFunSuite with Matchers {
     )
 
     val second = first.copy(
-      created = freezedDateTime.plusMillis(1)
+      created = freezedDateTime.plus(1, ChronoUnit.MILLIS)
     )
 
     first.compareTo(second) shouldBe 1
   }
 
   test("Internal FIFO messages should be ordered by their orderIndex if they share the same creation time") {
-    val freezedDateTime = new DateTime()
+    val freezedDateTime = OffsetDateTime.now()
 
     val first = InternalMessage(
       id = "id",
diff --git a/core/src/test/scala/org/elasticmq/actor/test/DataCreationHelpers.scala b/core/src/test/scala/org/elasticmq/actor/test/DataCreationHelpers.scala
index 6c436c37..8eb125d4 100644
--- a/core/src/test/scala/org/elasticmq/actor/test/DataCreationHelpers.scala
+++ b/core/src/test/scala/org/elasticmq/actor/test/DataCreationHelpers.scala
@@ -1,7 +1,9 @@
 package org.elasticmq.actor.test
 
 import org.elasticmq._
-import org.joda.time.{DateTime, Duration}
+import org.elasticmq.util.OffsetDateTimeUtil
+
+import java.time.Duration
 
 trait DataCreationHelpers {
   def createQueueData(
@@ -19,8 +21,8 @@ trait DataCreationHelpers {
       defaultVisibilityTimeout = Some(defaultVisibilityTimeout),
       delay = Some(Duration.ZERO),
       receiveMessageWait = Some(Duration.ZERO),
-      created = Some(new DateTime(0)),
-      lastModified = Some(new DateTime(0)),
+      created = Some(OffsetDateTimeUtil.ofEpochMilli(0)),
+      lastModified = Some(OffsetDateTimeUtil.ofEpochMilli(0)),
       deadLettersQueue = deadLettersQueue,
       copyMessagesTo = copyMessagesToQueue,
       moveMessagesTo = moveMessagesToQueue,
@@ -45,7 +47,7 @@ trait DataCreationHelpers {
       content,
       messageAttributes,
       nextDelivery,
-      new DateTime(0),
+      OffsetDateTimeUtil.ofEpochMilli(0),
       MessageStatistics(NeverReceived, 0),
       messageGroupId,
       messageDeduplicationId,
diff --git a/core/src/test/scala/org/elasticmq/util/MutableNowProvider.scala b/core/src/test/scala/org/elasticmq/util/MutableNowProvider.scala
index f48ce0dd..2b8610ff 100644
--- a/core/src/test/scala/org/elasticmq/util/MutableNowProvider.scala
+++ b/core/src/test/scala/org/elasticmq/util/MutableNowProvider.scala
@@ -1,13 +1,12 @@
 package org.elasticmq.util
 
+import java.time.OffsetDateTime
 import java.util.concurrent.atomic.AtomicLong
 
-import org.joda.time.DateTime
-
 class MutableNowProvider(initalValue: Long = 100L) extends NowProvider {
   var mutableNowMillis = new AtomicLong(initalValue)
 
-  override def nowMillis = mutableNowMillis.get()
+  override def nowMillis: Long = mutableNowMillis.get()
 
-  override def now = new DateTime(nowMillis)
+  override def now: OffsetDateTime = OffsetDateTimeUtil.ofEpochMilli(nowMillis)
 }
diff --git a/performance-tests/src/test/scala/org/elasticmq/performance/LocalPerformanceTest.scala b/performance-tests/src/test/scala/org/elasticmq/performance/LocalPerformanceTest.scala
index f0bc8d99..c4296c20 100644
--- a/performance-tests/src/test/scala/org/elasticmq/performance/LocalPerformanceTest.scala
+++ b/performance-tests/src/test/scala/org/elasticmq/performance/LocalPerformanceTest.scala
@@ -4,12 +4,7 @@ import akka.actor.{ActorRef, ActorSystem, Props}
 import akka.util.Timeout
 import com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials}
 import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration
-import com.amazonaws.services.sqs.model.{
-  CreateQueueRequest,
-  DeleteMessageRequest,
-  ReceiveMessageRequest,
-  SendMessageRequest
-}
+import com.amazonaws.services.sqs.model.{CreateQueueRequest, DeleteMessageRequest, ReceiveMessageRequest, SendMessageRequest}
 import com.amazonaws.services.sqs.{AmazonSQS, AmazonSQSClientBuilder}
 import org.elasticmq._
 import org.elasticmq.actor.QueueManagerActor
@@ -18,8 +13,8 @@ import org.elasticmq.msg._
 import org.elasticmq.rest.sqs.{SQSRestServer, SQSRestServerBuilder}
 import org.elasticmq.test._
 import org.elasticmq.util.NowProvider
-import org.joda.time.DateTime
 
+import java.time.OffsetDateTime
 import java.util.concurrent.TimeUnit
 import scala.collection.JavaConverters._
 import scala.concurrent.Await
@@ -109,10 +104,10 @@ object LocalPerformanceTest extends App {
               QueueData(
                 "testQueue",
                 MillisVisibilityTimeout(1000),
-                org.joda.time.Duration.ZERO,
-                org.joda.time.Duration.ZERO,
-                new DateTime(),
-                new DateTime()
+                java.time.Duration.ZERO,
+                java.time.Duration.ZERO,
+                OffsetDateTime.now(),
+                OffsetDateTime.now()
               )
             )
           ),
diff --git a/persistence/persistence-file/src/test/scala/org/elasticmq/persistence/file/QueuePersisterTest.scala b/persistence/persistence-file/src/test/scala/org/elasticmq/persistence/file/QueuePersisterTest.scala
index 6ccf4b87..41ad0ba9 100644
--- a/persistence/persistence-file/src/test/scala/org/elasticmq/persistence/file/QueuePersisterTest.scala
+++ b/persistence/persistence-file/src/test/scala/org/elasticmq/persistence/file/QueuePersisterTest.scala
@@ -1,9 +1,10 @@
 package org.elasticmq.persistence.file
 import org.elasticmq.{MillisVisibilityTimeout, QueueData}
-import org.joda.time.{DateTime, Duration}
 import org.scalatest.funsuite.AnyFunSuite
 import org.scalatest.matchers.should.Matchers
 
+import java.time.{Duration, OffsetDateTime}
+
 class QueuePersisterTest extends AnyFunSuite with Matchers {
   test("standard queue is converted to config") {
 
@@ -11,10 +12,10 @@ class QueuePersisterTest extends AnyFunSuite with Matchers {
       QueueData(
         name = "queue1",
         defaultVisibilityTimeout = MillisVisibilityTimeout.fromSeconds(10),
-        delay = Duration.standardSeconds(8),
-        receiveMessageWait = Duration.standardSeconds(3),
-        created = new DateTime(),
-        lastModified = new DateTime()
+        delay = Duration.ofSeconds(8),
+        receiveMessageWait = Duration.ofSeconds(3),
+        created = OffsetDateTime.now(),
+        lastModified = OffsetDateTime.now()
       )
     )
     val actual = QueuePersister.prepareQueuesConfig(queues)
@@ -30,10 +31,10 @@ class QueuePersisterTest extends AnyFunSuite with Matchers {
       QueueData(
         name = "queue2.fifo",
         defaultVisibilityTimeout = MillisVisibilityTimeout.fromSeconds(10),
-        delay = Duration.standardSeconds(8),
-        receiveMessageWait = Duration.standardSeconds(3),
-        created = new DateTime(),
-        lastModified = new DateTime(),
+        delay = Duration.ofSeconds(8),
+        receiveMessageWait = Duration.ofSeconds(3),
+        created = OffsetDateTime.now(),
+        lastModified = OffsetDateTime.now(),
         isFifo = true
       )
     )
diff --git a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala
index b9035d0c..4c2b3343 100644
--- a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala
+++ b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesTest.scala
@@ -9,13 +9,13 @@ import org.elasticmq.msg.CreateQueue
 import org.elasticmq.rest.sqs.{ActorSystemModule, ContextPathModule, QueueManagerActorModule}
 import org.elasticmq.util.NowProvider
 import org.elasticmq.{CreateQueueData, MillisVisibilityTimeout, QueueData, StrictSQSLimits}
-import org.joda.time.{DateTime, Duration}
 import org.scalatest.concurrent.ScalaFutures
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
-import scala.concurrent.duration._
+import java.time.{Duration, OffsetDateTime}
 import scala.concurrent.ExecutionContextExecutor
+import scala.concurrent.duration._
 
 class QueueDirectivesTest
     extends AnyFlatSpec
@@ -43,7 +43,7 @@ class QueueDirectivesTest
   "queueActorFromUrl" should "return correct queue name based on QueueName" in {
     val future = queueManagerActor ? CreateQueue(
       CreateQueueData.from(
-        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, OffsetDateTime.now(), OffsetDateTime.now())
       )
     )
     future.value
@@ -61,7 +61,7 @@ class QueueDirectivesTest
   "queueActorAndNameFromUrl" should "return correct queue name based on QueueUrl" in {
     val future = queueManagerActor ? CreateQueue(
       CreateQueueData.from(
-        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, OffsetDateTime.now(), OffsetDateTime.now())
       )
     )
     future.value
@@ -79,7 +79,7 @@ class QueueDirectivesTest
   "queueActorAndNameFromUrl" should "return error when invalid QueueUrl" in {
     val future = queueManagerActor ? CreateQueue(
       CreateQueueData.from(
-        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, OffsetDateTime.now(), OffsetDateTime.now())
       )
     )
     future.value
diff --git a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala
index d4f9441a..1e7863f7 100644
--- a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala
+++ b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/sqs/directives/QueueDirectivesWithContextPathTest.scala
@@ -10,10 +10,10 @@ import org.elasticmq.msg.CreateQueue
 import org.elasticmq.rest.sqs.{ActorSystemModule, ContextPathModule, QueueManagerActorModule}
 import org.elasticmq.util.NowProvider
 import org.elasticmq.{CreateQueueData, MillisVisibilityTimeout, QueueData, StrictSQSLimits}
-import org.joda.time.{DateTime, Duration}
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
+import java.time.{Duration, OffsetDateTime}
 import scala.concurrent.duration._
 import scala.concurrent.{Await, ExecutionContextExecutor}
 
@@ -42,7 +42,7 @@ class QueueDirectivesWithContextPathTest
   "queueActorAndNameFromUrl" should "return correct queue name based on QueueName" in {
     val future = queueManagerActor ? CreateQueue(
       CreateQueueData.from(
-        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, OffsetDateTime.now(), OffsetDateTime.now())
       )
     )
     Await.result(future, maxDuration)
@@ -60,7 +60,7 @@ class QueueDirectivesWithContextPathTest
   "queueActorAndNameFromUrl" should "return correct queue name based on QueueUrl" in {
     val future = queueManagerActor ? CreateQueue(
       CreateQueueData.from(
-        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, DateTime.now(), DateTime.now())
+        QueueData("lol", MillisVisibilityTimeout(1L), Duration.ZERO, Duration.ZERO, OffsetDateTime.now(), OffsetDateTime.now())
       )
     )
     Await.result(future, maxDuration)
diff --git a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala
index 2f27e675..79052110 100644
--- a/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala
+++ b/rest/rest-sqs/src/test/scala/org/elasticmq/rest/stats/StatisticsDirectivesTest.scala
@@ -11,12 +11,12 @@ import org.elasticmq.rest.sqs.directives._
 import org.elasticmq.rest.sqs.{ActorSystemModule, QueueAttributesOps, QueueManagerActorModule}
 import org.elasticmq.util.MutableNowProvider
 import org.elasticmq.{CreateQueueData, MillisVisibilityTimeout, QueueData, StrictSQSLimits}
-import org.joda.time.{DateTime, Duration}
 import org.scalatest._
 import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
+import java.time.{Duration, LocalDate, ZoneOffset}
 import scala.concurrent.duration.DurationInt
 
 class StatisticsDirectivesTest
@@ -45,7 +45,7 @@ class StatisticsDirectivesTest
   implicit lazy val actorSystem: ActorSystem = ActorSystem("test-actor-system")
 
   implicit val nowProvider = new MutableNowProvider(
-    new DateTime().withDate(2020, 1, 1).withTimeAtStartOfDay().getMillis
+    LocalDate.of(2020, 1, 1).atStartOfDay(ZoneOffset.UTC).toInstant.toEpochMilli
   )
 
   lazy val queueManagerActor: ActorRef =