From df1a2f7fcbdd85ac84162cf8eae8cdb6bb25cbb5 Mon Sep 17 00:00:00 2001 From: Sergey Nastich Date: Fri, 24 Aug 2018 13:41:27 -0400 Subject: Migration to `java.time.Instant` and `java.time.LocalDate`: Part 1 (#200) * Add semi-backwards-compatible JSON formats and path matchers for java.time.Instant and java.time.LocalDate * Use `Clock` in `ApplicationContext` instead of `TimeProvider`, deprecate `TimeProvider` * Add `ChangeableClock` in time package for tests * Add generators for instants and LocalDates --- src/main/scala/xyz/driver/core/app/init.scala | 26 +++++----- src/main/scala/xyz/driver/core/generators.scala | 7 ++- src/main/scala/xyz/driver/core/json.scala | 57 +++++++++++++++++++--- .../driver/core/logging/MdcExecutionContext.scala | 20 ++++---- src/main/scala/xyz/driver/core/time.scala | 45 ++++++++++++++--- 5 files changed, 117 insertions(+), 38 deletions(-) (limited to 'src/main/scala/xyz') diff --git a/src/main/scala/xyz/driver/core/app/init.scala b/src/main/scala/xyz/driver/core/app/init.scala index 119c91a..f1e80b9 100644 --- a/src/main/scala/xyz/driver/core/app/init.scala +++ b/src/main/scala/xyz/driver/core/app/init.scala @@ -1,6 +1,7 @@ package xyz.driver.core.app import java.nio.file.{Files, Paths} +import java.time.Clock import java.util.concurrent.{Executor, Executors} import akka.actor.ActorSystem @@ -9,7 +10,7 @@ import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.Logger import org.slf4j.LoggerFactory import xyz.driver.core.logging.MdcExecutionContext -import xyz.driver.core.time.provider.{SystemTimeProvider, TimeProvider} +import xyz.driver.core.time.provider.TimeProvider import xyz.driver.tracing.{GoogleTracer, NoTracer, Tracer} import scala.concurrent.ExecutionContext @@ -23,7 +24,9 @@ object init { val gitHeadCommit: scala.Option[String] } - case class ApplicationContext(config: Config, time: TimeProvider, log: Logger) + case class ApplicationContext(config: Config, clock: Clock, log: Logger) { + val time: TimeProvider = clock + } /** NOTE: This needs to be the first that is run when application starts. * Otherwise if another command causes the logger to be instantiated, @@ -68,9 +71,9 @@ object init { val actorSystem = ActorSystem(s"$serviceName-actors", Option(config), Option.empty[ClassLoader], Option(executionContext)) - Runtime.getRuntime.addShutdownHook(new Thread() { - override def run(): Unit = Try(actorSystem.terminate()) - }) + sys.addShutdownHook { + Try(actorSystem.terminate()) + } actorSystem } @@ -81,14 +84,11 @@ object init { def newFixedMdcExecutionContext(capacity: Int): MdcExecutionContext = toMdcExecutionContext(Executors.newFixedThreadPool(capacity)) - def defaultApplicationContext(): ApplicationContext = { - val config = getEnvironmentSpecificConfig() - - val time = new SystemTimeProvider() - val log = Logger(LoggerFactory.getLogger(classOf[DriverApp])) - - ApplicationContext(config, time, log) - } + def defaultApplicationContext(): ApplicationContext = + ApplicationContext( + config = getEnvironmentSpecificConfig(), + clock = Clock.systemUTC(), + log = Logger(LoggerFactory.getLogger(classOf[DriverApp]))) def createDefaultApplication( modules: Seq[Module], diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index d57980e..d00b6dd 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -2,6 +2,7 @@ package xyz.driver.core import enumeratum._ import java.math.MathContext +import java.time.{Instant, LocalDate, ZoneOffset} import java.util.UUID import xyz.driver.core.time.{Time, TimeOfDay, TimeRange} @@ -88,7 +89,9 @@ object generators { def nextTriad[F, S, T](first: => F, second: => S, third: => T): (F, S, T) = (first, second, third) - def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis)) + def nextInstant(): Instant = Instant.ofEpochMilli(math.abs(nextLong() % System.currentTimeMillis)) + + def nextTime(): Time = nextInstant() def nextTimeOfDay: TimeOfDay = TimeOfDay(java.time.LocalTime.MIN.plusSeconds(nextLong), java.util.TimeZone.getDefault) @@ -103,6 +106,8 @@ object generators { def nextDate(): Date = nextTime().toDate(java.util.TimeZone.getTimeZone("UTC")) + def nextLocalDate(): LocalDate = nextInstant().atZone(ZoneOffset.UTC).toLocalDate + def nextDayOfWeek(): DayOfWeek = oneOf(DayOfWeek.All) def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 639af22..98725fb 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -1,6 +1,8 @@ package xyz.driver.core import java.net.InetAddress +import java.time.format.DateTimeFormatter +import java.time.{Instant, LocalDate} import java.util.{TimeZone, UUID} import akka.http.scaladsl.marshalling.{Marshaller, Marshalling} @@ -23,6 +25,7 @@ import xyz.driver.core.time.{Time, TimeOfDay} import scala.reflect.{ClassTag, classTag} import scala.reflect.runtime.universe._ import scala.util.Try +import scala.util.control.NonFatal object json { import DefaultJsonProtocol._ @@ -77,25 +80,49 @@ object json { } } - def TimeInPath: PathMatcher1[Time] = + def TimeInPath: PathMatcher1[Time] = InstantInPath.map(instant => Time(instant.toEpochMilli)) + + private def timestampInPath: PathMatcher1[Long] = PathMatcher("""[+-]?\d*""".r) flatMap { string => - try Some(Time(string.toLong)) + try Some(string.toLong) catch { case _: IllegalArgumentException => None } } - implicit val timeFormat = new RootJsonFormat[Time] { + def InstantInPath: PathMatcher1[Instant] = + new PathMatcher1[Instant] { + def apply(path: Path): PathMatcher.Matching[Tuple1[Instant]] = path match { + case Path.Segment(head, tail) => + try Matched(tail, Tuple1(Instant.parse(head))) + catch { + case NonFatal(_) => Unmatched + } + case _ => Unmatched + } + } | timestampInPath.map(Instant.ofEpochMilli) + + implicit val timeFormat: RootJsonFormat[Time] = new RootJsonFormat[Time] { def write(time: Time) = JsObject("timestamp" -> JsNumber(time.millis)) - def read(value: JsValue): Time = value match { + def read(value: JsValue): Time = Time(instantFormat.read(value)) + } + + implicit val instantFormat: JsonFormat[Instant] = new JsonFormat[Instant] { + def write(instant: Instant): JsValue = JsString(instant.toString) + + def read(value: JsValue): Instant = value match { case JsObject(fields) => fields .get("timestamp") .flatMap { - case JsNumber(millis) => Some(Time(millis.toLong)) + case JsNumber(millis) => Some(Instant.ofEpochMilli(millis.longValue())) case _ => None } - .getOrElse(throw DeserializationException("Time expects number")) - case _ => throw DeserializationException("Time expects number") + .getOrElse(deserializationError(s"Instant expects ISO timestamp but got ${value.compactPrint}")) + case JsNumber(millis) => Instant.ofEpochMilli(millis.longValue()) + case JsString(str) => + try Instant.parse(str) + catch { case NonFatal(_) => deserializationError(s"Instant expects ISO timestamp but got $str") } + case _ => deserializationError(s"Instant expects ISO timestamp but got ${value.compactPrint}") } } @@ -140,6 +167,22 @@ object json { } } + implicit val localDateFormat = new RootJsonFormat[LocalDate] { + val format = DateTimeFormatter.ISO_LOCAL_DATE + + def write(date: LocalDate): JsValue = JsString(date.format(format)) + def read(value: JsValue): LocalDate = value match { + case JsString(dateString) => + try LocalDate.parse(dateString, format) + catch { + case NonFatal(_) => + throw deserializationError(s"Malformed ISO 8601 Date. Expected YYYY-MM-DD, but got $dateString.") + } + case _ => + throw deserializationError(s"Malformed ISO 8601 Date. Expected YYYY-MM-DD, but got ${value.compactPrint}.") + } + } + implicit val monthFormat = new RootJsonFormat[Month] { def write(month: Month) = JsNumber(month) def read(value: JsValue): Month = value match { diff --git a/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala b/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala index df21b48..11c99f9 100644 --- a/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala +++ b/src/main/scala/xyz/driver/core/logging/MdcExecutionContext.scala @@ -13,18 +13,16 @@ import scala.concurrent.ExecutionContext class MdcExecutionContext(executionContext: ExecutionContext) extends ExecutionContext { override def execute(runnable: Runnable): Unit = { val callerMdc = MDC.getCopyOfContextMap - executionContext.execute(new Runnable { - def run(): Unit = { - // copy caller thread diagnostic context to execution thread - Option(callerMdc).foreach(MDC.setContextMap) - try { - runnable.run() - } finally { - // the thread might be reused, so we clean up for the next use - MDC.clear() - } + executionContext.execute { () => + // copy caller thread diagnostic context to execution thread + Option(callerMdc).foreach(MDC.setContextMap) + try { + runnable.run() + } finally { + // the thread might be reused, so we clean up for the next use + MDC.clear() } - }) + } } override def reportFailure(cause: Throwable): Unit = executionContext.reportFailure(cause) diff --git a/src/main/scala/xyz/driver/core/time.scala b/src/main/scala/xyz/driver/core/time.scala index 6dbd173..c7a32ad 100644 --- a/src/main/scala/xyz/driver/core/time.scala +++ b/src/main/scala/xyz/driver/core/time.scala @@ -1,6 +1,7 @@ package xyz.driver.core import java.text.SimpleDateFormat +import java.time.{Clock, Instant, ZoneId, ZoneOffset} import java.util._ import java.util.concurrent.TimeUnit @@ -40,6 +41,14 @@ object time { cal.setTimeInMillis(millis) date.Date(cal.get(Calendar.YEAR), date.Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) } + + def toInstant: Instant = Instant.ofEpochMilli(millis) + } + + object Time { + implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) + + implicit def apply(instant: Instant): Time = Time(instant.toEpochMilli) } /** @@ -127,11 +136,6 @@ object time { } } - object Time { - - implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) - } - final case class TimeRange(start: Time, end: Time) { def duration: Duration = FiniteDuration(end.millis - start.millis, MILLISECONDS) } @@ -149,6 +153,18 @@ object time { def textualTime(timezone: TimeZone)(time: Time): String = make(new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a"))(_.setTimeZone(timezone)).format(new Date(time.millis)) + class ChangeableClock(@volatile var instant: Instant, val zone: ZoneId = ZoneOffset.UTC) extends Clock { + + def tick(duration: FiniteDuration): Unit = + instant = instant.plusNanos(duration.toNanos) + + val getZone: ZoneId = zone + + def withZone(zone: ZoneId): Clock = new ChangeableClock(instant, zone = zone) + + override def toString: String = "ChangeableClock(" + instant + "," + zone + ")" + } + object provider { /** @@ -159,17 +175,34 @@ object time { * All the calls to receive current time must be made using time * provider injected to the caller. */ + @deprecated( + "Use java.time.Clock instead. Note that xyz.driver.core.Time and xyz.driver.core.date.Date will also be deprecated soon!", + "0.13.0") trait TimeProvider { def currentTime(): Time + def toClock: Clock + } + + final implicit class ClockTimeProvider(clock: Clock) extends TimeProvider { + def currentTime(): Time = Time(clock.instant().toEpochMilli) + + val toClock: Clock = clock } final class SystemTimeProvider extends TimeProvider { def currentTime() = Time(System.currentTimeMillis()) + + lazy val toClock: Clock = Clock.systemUTC() } + final val SystemTimeProvider = new SystemTimeProvider final class SpecificTimeProvider(time: Time) extends TimeProvider { - def currentTime() = time + + def currentTime(): Time = time + + lazy val toClock: Clock = Clock.fixed(time.toInstant, ZoneOffset.UTC) } + } } -- cgit v1.2.3