4

Let's say I have a method that permits to update some date in DB:

def updateLastConsultationDate(userId: String): Unit = ???

How can I throttle/debounce that method easily so that it won't be run more than once an hour per user.

I'd like the simplest possible solution, not based on any event-bus, actor lib or persistence layer. I'd like an in-memory solution (and I am aware of the risks).

I've seen solutions for throttling in Scala, based on Akka Throttler, but this really looks to me overkill to start using actors just for throttling method calls. Isn't there a very simple way to do that?

Edit: as it seems not clear enough, here's a visual representation of what I want, implemented in JS. As you can see, throttling may not only be about filtering subsequent calls, but also postponing calls (also called trailing events in js/lodash/underscore). The solution I'm looking for can't be based on pure-synchronous code only.

Community
  • 1
  • 1
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • You need to be more specific in your question. Do you mean that a DB access method, when called, might or might not execute the actual DB action, based on time elapsed since last call with the same user parameter? Or do you mean that DB action to be performed periodically in background? And is it a hard requirement to not use any additional libraries? – Haspemulator Nov 17 '16 at 17:25
  • @Haspemulator I think I'm explicit enough, if you want a visual illustration see http://jsfiddle.net/wn1af0te/ . If the throttled method is called twice during same hour, the 2nd call should be ignored (or eventually postponed, I'm ok to use an ExecutionContext for that) – Sebastien Lorber Nov 17 '16 at 17:29
  • Are the throttled calls supposed to be queued for later or ignored? And what about additional libraries? – Haspemulator Nov 17 '16 at 17:32
  • @Haspemulator I've edited my question. I'm ok for using a lib, as long as it stays in memory and does not require me to setup a cluster of whatever tech. – Sebastien Lorber Nov 17 '16 at 17:40

3 Answers3

5

This sounds like a great job for a ReactiveX-based solution. On Scala, Monix is my favorite one. Here's the Ammonite REPL session illustrating it:

import $ivy.`io.monix::monix:2.1.0` // I'm using Ammonite's magic imports, it's equivalent to adding "io.monix" %% "monix" % "2.1.0" into your libraryImports in SBT

import scala.concurrent.duration.DurationInt
import monix.reactive.subjects.ConcurrentSubject
import monix.reactive.Consumer
import monix.execution.Scheduler.Implicits.global
import monix.eval.Task

class DbUpdater {
  val publish = ConcurrentSubject.publish[String]
  val throttled = publish.throttleFirst(1 hour)
  val cancelHandle = throttled.consumeWith(
    Consumer.foreach(userId =>
      println(s"update your database with $userId here")))
    .runAsync

  def updateLastConsultationDate(userId: String): Unit = {
    publish.onNext(userId)
  }

  def stop(): Unit = cancelHandle.cancel()
}

Yes, and with Scala.js this code will work in the browser, too, if it's important for you.

Haspemulator
  • 11,050
  • 9
  • 49
  • 76
1

Since you ask for the simplest possible solution, you can store a val lastUpdateByUser: Map[String, Long], which you would consult before allowing an update

if (lastUpdateByUser.getOrElse(userName, 0)+60*60*1000 < System.currentTimeMillis) updateLastConsultationDate(...)

and update when a user actually performs an update

lastUpdateByUser(userName) = System.currentTimeMillis
radumanolescu
  • 4,059
  • 2
  • 31
  • 44
  • @Sebastien Lorber Looking at your profile, you probably knew how to do what I've shown above - but then I am not sure what you are asking. You may need to provide more specific requirements for your question. – radumanolescu Nov 17 '16 at 17:18
  • this is a nice try :) I edited my question to make it clearer – Sebastien Lorber Nov 17 '16 at 17:38
  • Pretty decent idea, I'd use a TrieMap and the `getOrElseUpdate` or a similar primitive to handle concurrent calls correctly. – Reactormonk Nov 17 '16 at 17:49
  • that looks like rate limiting, not throttling. If sufficient time has not elapsed throttling would not drop the request, it would just "deduplicate" it – nafg Jan 26 '17 at 01:33
0

One way to throttle, would be to maintain a count in a redis instance. Doing so would ensure that the DB wouldn't be updated, no matter how many scala processes you were running, because the state is stored outside of the process.

David Weiser
  • 5,190
  • 4
  • 28
  • 35
  • As I said I'm looking for a simple in-memory solution which does not require a new DB just. I'm aware of the dangers of loosing in-memory state and I don't care because I want to store the state inside the process on purpose for simplicity – Sebastien Lorber Nov 17 '16 at 17:33