Skip to content

Commit

Permalink
Merge pull request #8 from dvgica/better-interface
Browse files Browse the repository at this point in the history
Better interface
  • Loading branch information
dvgica authored Oct 22, 2023
2 parents 89a453b + dfd7440 commit 19e48c7
Show file tree
Hide file tree
Showing 18 changed files with 636 additions and 524 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: sbt '++ ${{ matrix.scala }}' test

- name: Compress target directories
run: tar cf targets.tar target periodic-api/target periodic-jdk/target project/target
run: tar cf targets.tar target periodic-core/target project/target

- name: Upload target directories
uses: actions/upload-artifact@v3
Expand Down
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Periodic
[![Maven](https://img.shields.io/maven-central/v/ca.dvgi/periodic_2.13?color=blue)](https://search.maven.org/search?q=g:ca.dvgi%20periodic) [![CI](https://img.shields.io/github/actions/workflow/status/dvgica/periodic/ci.yml?branch=main)](https://github.com/dvgica/periodic/actions)
[![Maven](https://img.shields.io/maven-central/v/ca.dvgi/periodic-core_2.13?color=blue)](https://search.maven.org/search?q=g:ca.dvgi%20periodic) [![CI](https://img.shields.io/github/actions/workflow/status/dvgica/periodic/ci.yml?branch=main)](https://github.com/dvgica/periodic/actions)

Periodic is a Scala library providing an in-memory cached variable (`AutoUpdatingVar`) that self-updates on a periodic basis.

Expand All @@ -21,28 +21,23 @@ For data that changes irregularly but must be up-to-date, you likely want to be

Periodic is available on Maven Central for Scala 2.12, 2.13, and 3. Java 11+ is required.

There is a `periodic-api` project which exposes an interface for `AutoUpdatingVar`, and currently a single implementation using JDK primitives in the `periodic-jdk` project. Other implementations may follow.
For the default JDK-based implementation, add the following dependency:

If a specific implementation is required, add the following dependency to your project:

`"ca.dvgi" %% "periodic-jdk" % "<latest>"`

If only the interface is required, add:

`"ca.dvgi" %% "periodic-api" % "<latest>"`
`"ca.dvgi" %% "periodic-core" % "<latest>"`

## Usage Example

Using the default JDK implementation:

``` scala
import ca.dvgi.periodic.jdk._
import ca.dvgi.periodic._
import scala.concurrent.duration._
import scala.concurrent.Await
import java.time.Instant

def updateData(): String = Instant.now.toString

val data = new JdkAutoUpdatingVar(
val data = AutoUpdatingVar.jdk(
updateData(),
// can also be dynamic based on the last data
UpdateInterval.Static(1.second),
Expand Down Expand Up @@ -73,7 +68,21 @@ Cached data is still 2023-10-19T02:35:22.467418Z
New cached data is 2023-10-19T02:35:23.474155Z
```

For handling errors during update, and other options, see the Scaladocs.
For handling errors during update, and other options, see the Scaladocs.

### Alternate Implementations

Alternate implementations are provided by passing an `AutoUpdater` to `AutoUpdatingVar.apply`:

``` scala
AutoUpdatingVar(
SomeOtherAutoUpdater[String]() // T must be explicitly provided, it can't be inferred
)(
updateData(),
UpdateInterval.Static(1.second),
UpdateAttemptStrategy.Infinite(5.seconds)
)
```

## Contributing

Expand Down
13 changes: 2 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,7 @@ def subproject(name: String) = {
)
}

lazy val api = subproject("api")
.settings(
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % "2.0.9"
)
)

lazy val jdk = subproject("jdk")
.dependsOn(api)
lazy val core = subproject("core")
.settings(
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % "2.0.9",
Expand All @@ -59,8 +51,7 @@ lazy val jdk = subproject("jdk")
lazy val root = project
.in(file("."))
.aggregate(
api,
jdk
core
)
.settings(
publish / skip := true,
Expand Down
74 changes: 0 additions & 74 deletions periodic-api/src/main/scala/ca/dvgi/periodic/AutoUpdatingVar.scala

This file was deleted.

File renamed without changes.
34 changes: 34 additions & 0 deletions periodic-core/src/main/scala/ca/dvgi/periodic/AutoUpdater.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ca.dvgi.periodic

import org.slf4j.Logger

/** AutoUpdatingVar delegates most functionality to an AutoUpdater, which may have many
* implementations.
*/
trait AutoUpdater[U[_], R[_], T] extends AutoCloseable {

/** Initializes the var for the first time, handling errors as specified. If successful, schedules
* the next update.
*
* @param log
* Implementations should use this logger for consistency.
*
* @return
* An effect, which, if successfully completed, signifies that a value is available. If
* initialization failed, the effect should also be failed.
*/
def start(
log: Logger,
updateVar: () => U[T],
updateInterval: UpdateInterval[T],
updateAttemptStrategy: UpdateAttemptStrategy,
handleInitializationError: PartialFunction[Throwable, U[T]]
): R[Unit]

/** The latest in-memory value of the variable.
*
* @return
* Some[T] if the variable has been initialized successfully, otherwise None.
*/
def latest: Option[T]
}
134 changes: 134 additions & 0 deletions periodic-core/src/main/scala/ca/dvgi/periodic/AutoUpdatingVar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package ca.dvgi.periodic

import scala.reflect.ClassTag
import org.slf4j.LoggerFactory
import ca.dvgi.periodic.jdk.Identity
import ca.dvgi.periodic.jdk.JdkAutoUpdater
import scala.concurrent.duration.Duration
import scala.concurrent.Future
import java.util.concurrent.ScheduledExecutorService

/** A variable that updates itself. `latest` can be called from multiple threads, which are all
* guaranteed to get the latest var.
*
* An AutoUpdatingVar attempts to get the variable immediately upon class instantiation. If this
* fails, there are no further attempts (unless specified via `handleInitializationError`), and the
* effect returned by the `ready` method will complete unsuccesfully. If it succeeds, the effect
* completes successfully and `latest` can be safely called.
*
* Failed updates other than the first (those that throw an exception) may be retried with various
* configurations.
*
* A successful update schedules the next update, with an interval that can vary based on the
* just-updated var.
*
* @param updateVar
* A thunk to initialize and update the var
* @param updateInterval
* Configuration for the update interval
* @param updateAttemptStrategy
* Configuration for retrying updates on failure
* @param handleInitializationError
* A PartialFunction used to recover from exceptions in the var initialization. If unspecified,
* the exception will fail the effect returned by `ready`.
* @param varNameOverride
* A name for this variable, used in logging. If unspecified, the simple class name of T will be
* used.
*/
class AutoUpdatingVar[U[_], R[_], T](autoUpdater: AutoUpdater[U, R, T])(
updateVar: => U[T],
updateInterval: UpdateInterval[T],
updateAttemptStrategy: UpdateAttemptStrategy,
handleInitializationError: PartialFunction[Throwable, U[T]] = PartialFunction.empty,
varNameOverride: Option[String] = None
)(implicit ct: ClassTag[T])
extends AutoCloseable {

private val varName = varNameOverride match {
case Some(n) => n
case None => ct.runtimeClass.getSimpleName
}

private val log = LoggerFactory.getLogger(s"AutoUpdatingVar[$varName]")

log.info(s"Starting. ${updateAttemptStrategy.description}")

private val _ready = autoUpdater.start(
log,
() => updateVar,
updateInterval,
updateAttemptStrategy,
handleInitializationError
)

/** @return
* An effect which, once successfully completed, signifies that the AutoUpdatingVar has a
* value, i.e. `latest` can be called and no exception will be thrown.
*/
def ready: R[Unit] = _ready

/** Get the latest variable value from memory. Does not attempt to update the var.
*
* Wait for `ready` to be completed before calling this method.
*
* @return
* The latest value of the variable. Calling this method is thread-safe.
* @throws UnreadyAutoUpdatingVarException
* if there is not yet a value to return
*/
def latest: T = autoUpdater.latest.getOrElse(throw UnreadyAutoUpdatingVarException)

override def close(): Unit = {
autoUpdater.close()
log.info(s"Shut down sucessfully")
}
}

object AutoUpdatingVar {

/** @see
* [[ca.dvgi.periodic.AutoUpdatingVar]]
*/
def apply[U[_], R[_], T](autoUpdater: AutoUpdater[U, R, T])(
updateVar: => U[T],
updateInterval: UpdateInterval[T],
updateAttemptStrategy: UpdateAttemptStrategy,
handleInitializationError: PartialFunction[Throwable, U[T]] = PartialFunction.empty,
varNameOverride: Option[String] = None
)(implicit ct: ClassTag[T]): AutoUpdatingVar[U, R, T] = {
new AutoUpdatingVar(autoUpdater)(
updateVar,
updateInterval,
updateAttemptStrategy,
handleInitializationError,
varNameOverride
)
}

/** An AutoUpdatingVar based on only the JDK.
*
* @see
* [[ca.dvgi.periodic.jdk.JdkAutoUpdater]]
* @see
* [[ca.dvgi.periodic.AutoUpdatingVar]]
*/
def jdk[T](
updateVar: => T,
updateInterval: UpdateInterval[T],
updateAttemptStrategy: UpdateAttemptStrategy,
handleInitializationError: PartialFunction[Throwable, T] = PartialFunction.empty,
varNameOverride: Option[String] = None,
blockUntilReadyTimeout: Option[Duration] = None,
executorOverride: Option[ScheduledExecutorService] = None
)(implicit ct: ClassTag[T]): AutoUpdatingVar[Identity, Future, T] = {
new AutoUpdatingVar(
new JdkAutoUpdater[T](blockUntilReadyTimeout, executorOverride)
)(
updateVar,
updateInterval,
updateAttemptStrategy,
handleInitializationError,
varNameOverride
)
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
package ca.dvgi.periodic

import org.slf4j.LoggerFactory
import org.slf4j.Logger

sealed trait UpdateAttemptExhaustionBehavior {
def run: String => Unit
def run: Logger => Unit
def description: String
}

object UpdateAttemptExhaustionBehavior {
case class Terminate(exitCode: Int = 1) extends UpdateAttemptExhaustionBehavior {
private val log = LoggerFactory.getLogger(getClass)

def run: String => Unit = name => {
def run: Logger => Unit = log => {
log.error(
s"$name: Var update attempts exhausted, will now attempt to exit the process with exit code: $exitCode..."
s"Var update attempts exhausted, will now attempt to exit the process with exit code: $exitCode..."
)
sys.exit(exitCode)
}

val description: String = s"Terminate with exit code $exitCode"
}

case class Custom(run: String => Unit, descriptionOverride: Option[String] = None)
case class Custom(run: Logger => Unit, descriptionOverride: Option[String] = None)
extends UpdateAttemptExhaustionBehavior {
val description: String = descriptionOverride.getOrElse("Run custom logic")
}
Expand Down
Loading

0 comments on commit 19e48c7

Please sign in to comment.