Skip to content

Commit a470087

Browse files
author
Christopher Kolstad
authored
feat: TogglesCheckedListener and ReadyListener (#56)
* Add bin folder to ignore * feat: Add two new listeners. The TogglesCheckedListener which will be notified each time the poller checks for updates, regardless if there are updates or not or if the check failed. The ReadyListener which will be notified once the RefreshPolicy completes its initialisation process. Adds support for pollInterval = 0 to disable polling, it will only fetch once. Co-authored-by: Gastón Fournier <[email protected]> fixes: #47, #49
1 parent 770b220 commit a470087

File tree

12 files changed

+235
-49
lines changed

12 files changed

+235
-49
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ val unleashClient = UnleashClient(config = unleashConfig, context = myAppContext
126126
#### PollingModes
127127
##### Autopolling
128128
If you'd like for changes in toggles to take effect for you; use AutoPolling.
129-
You can configure the pollInterval and a listener that gets notified when toggles are updated in the background thread
129+
You can configure the pollInterval and a listener that gets notified when toggles are updated in the background thread.
130+
If you set the poll interval to 0, the SDK will fetch once, but not set up polling.
130131

131132
The listener is a no-argument lambda that gets called by the RefreshPolicy for every poll that
132133
1. Does not return `304 - Not Modified`
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
package io.getunleash.polling
22

3-
class AutoPollingMode(val pollRateDuration: Long, val togglesUpdatedListener: TogglesUpdatedListener = TogglesUpdatedListener { }, val erroredListener: TogglesErroredListener = TogglesErroredListener { }, val pollImmediate: Boolean = true) : PollingMode {
3+
/**
4+
* @param pollRateDuration - How long (in seconds) between each poll
5+
* @param togglesUpdatedListener - A listener that will be notified each time a poll actually updates the evaluation result
6+
* @param erroredListener - A listener that will be notified each time a poll fails. The notification will include the Exception
7+
* @param togglesCheckedListener - A listener that will be notified each time a poll completed. Will be called regardless of the check succeeded or failed.
8+
* @param readyListener - A listener that will be notified after the poller is done instantiating, i.e. has an evaluation result in its cache. Each ready listener will receive only one notification
9+
* @param pollImmediate - Set to true, the poller will immediately poll for configuration and then call the ready listener. Set to false, you will need to call [startPolling()) to actually talk to proxy/Edge
10+
*/
11+
class AutoPollingMode(val pollRateDuration: Long,
12+
val togglesUpdatedListener: TogglesUpdatedListener? = null,
13+
val erroredListener: TogglesErroredListener? = null,
14+
val togglesCheckedListener: TogglesCheckedListener? = null,
15+
val readyListener: ReadyListener? = null,
16+
val pollImmediate: Boolean = true) : PollingMode {
417
override fun pollingIdentifier(): String = "auto"
518

619
}

src/main/kotlin/io/getunleash/polling/AutoPollingPolicy.kt

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,37 @@ class AutoPollingPolicy(
2828
private val initFuture = CompletableFuture<Unit>()
2929
private var timer: Timer? = null
3030
init {
31-
autoPollingConfig.togglesUpdatedListener.let { listeners.add(it) }
32-
autoPollingConfig.erroredListener.let { errorListeners.add(it) }
31+
autoPollingConfig.togglesUpdatedListener?.let { listeners.add(it) }
32+
autoPollingConfig.togglesCheckedListener?.let { checkListeners.add(it) }
33+
autoPollingConfig.erroredListener?.let { errorListeners.add(it) }
34+
autoPollingConfig.readyListener?.let { readyListeners.add(it) }
3335
if (autoPollingConfig.pollImmediate) {
34-
timer =
35-
timer(
36-
name = "unleash_toggles_fetcher",
37-
initialDelay = 0L,
38-
daemon = true,
39-
period = autoPollingConfig.pollRateDuration
40-
) {
41-
updateToggles()
42-
if (!initialized.getAndSet(true)) {
43-
initFuture.complete(null)
44-
}
36+
if (autoPollingConfig.pollRateDuration > 0) {
37+
timer =
38+
timer(
39+
name = "unleash_toggles_fetcher",
40+
initialDelay = 0L,
41+
daemon = true,
42+
period = autoPollingConfig.pollRateDuration
43+
) {
44+
updateToggles()
45+
if (!initialized.getAndSet(true)) {
46+
super.broadcastReady()
47+
initFuture.complete(null)
48+
}
49+
}
50+
} else {
51+
updateToggles()
52+
if (!initialized.getAndSet(true)) {
53+
super.broadcastReady()
54+
initFuture.complete(null)
4555
}
56+
}
4657
}
4758
}
4859

60+
override val isReady: AtomicBoolean
61+
get() = initialized
4962

5063
override fun getConfigurationAsync(): CompletableFuture<Map<String, Toggle>> {
5164
return if (this.initFuture.isDone) {
@@ -56,13 +69,20 @@ class AutoPollingPolicy(
5669
}
5770

5871
override fun startPolling() {
59-
this.timer?.cancel()
60-
this.timer = timer(
61-
name = "unleash_toggles_fetcher",
62-
initialDelay = 0L,
63-
daemon = true,
64-
period = autoPollingConfig.pollRateDuration
65-
) {
72+
if (autoPollingConfig.pollRateDuration > 0) {
73+
this.timer?.cancel()
74+
this.timer = timer(
75+
name = "unleash_toggles_fetcher",
76+
initialDelay = 0L,
77+
daemon = true,
78+
period = autoPollingConfig.pollRateDuration
79+
) {
80+
updateToggles()
81+
if (!initialized.getAndSet(true)) {
82+
initFuture.complete(null)
83+
}
84+
}
85+
} else {
6686
updateToggles()
6787
if (!initialized.getAndSet(true)) {
6888
initFuture.complete(null)
@@ -79,37 +99,25 @@ class AutoPollingPolicy(
7999
val response = super.fetcher().getTogglesAsync(context).get()
80100
val cached = super.readToggleCache()
81101
if (response.isFetched() && cached != response.toggles) {
82-
super.writeToggleCache(response.toggles)
83-
this.broadcastTogglesUpdated()
102+
logger.trace("Content was not equal")
103+
super.writeToggleCache(response.toggles) // This will also broadcast updates
84104
} else if (response.isFailed()) {
85-
response?.error?.let(::broadcastTogglesErrored)
105+
response?.error?.let { e -> super.broadcastTogglesErrored(e) }
86106
}
87107
} catch (e: Exception) {
88-
this.broadcastTogglesErrored(e)
108+
super.broadcastTogglesErrored(e)
89109
logger.warn("Exception in AutoPollingCachePolicy", e)
90110
}
111+
logger.info("Done checking. Broadcasting check result")
112+
super.broadcastTogglesChecked()
91113
}
92-
93-
private fun broadcastTogglesErrored(e: Exception) {
94-
synchronized(errorListeners) {
95-
errorListeners.forEach {
96-
it.onError(e)
97-
}
98-
}
99-
}
100-
101-
private fun broadcastTogglesUpdated() {
102-
synchronized(listeners) {
103-
listeners.forEach {
104-
it.onTogglesUpdated()
105-
}
106-
}
107-
}
108-
109114
override fun close() {
110115
super.close()
111116
this.timer?.cancel()
112117
this.listeners.clear()
118+
this.errorListeners.clear()
119+
this.checkListeners.clear()
120+
this.readyListeners.clear()
113121
this.timer = null
114122
}
115123
}

src/main/kotlin/io/getunleash/polling/FilePollingMode.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import java.io.File
66
/**
77
* Configuration for FilePollingPolicy. Sets up where the policy loads the toggles from
88
* @param fileToLoadFrom
9+
* @param readyListener - Will broadcast a ready event (Once the File is loaded and the toggle cache is populated)
910
* @since 0.2
1011
*/
11-
class FilePollingMode(val fileToLoadFrom: File) : PollingMode {
12+
class FilePollingMode(val fileToLoadFrom: File, val readyListener: ReadyListener? = null) : PollingMode {
1213
override fun pollingIdentifier(): String = "file"
1314
}

src/main/kotlin/io/getunleash/polling/FilePollingPolicy.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.getunleash.data.ProxyResponse
99
import io.getunleash.data.Toggle
1010
import org.slf4j.LoggerFactory
1111
import java9.util.concurrent.CompletableFuture
12+
import java.util.concurrent.atomic.AtomicBoolean
1213

1314
/**
1415
* Allows loading a proxy response from file.
@@ -36,9 +37,13 @@ class FilePollingPolicy(
3637
config = config,
3738
context = context
3839
) {
40+
override val isReady: AtomicBoolean = AtomicBoolean(false)
3941
init {
4042
val togglesInFile: ProxyResponse = Parser.jackson.readValue(filePollingConfig.fileToLoadFrom)
43+
filePollingConfig.readyListener?.let { r -> addReadyListener(r) }
4144
super.writeToggleCache(togglesInFile.toggles.groupBy { it.name }.mapValues { (_, v) -> v.first() })
45+
super.broadcastReady()
46+
isReady.getAndSet(true)
4247
}
4348

4449
override fun startPolling() {

src/main/kotlin/io/getunleash/polling/PollingModes.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ object PollingModes {
3939
return AutoPollingMode(pollRateDuration = autoPollIntervalSeconds * 1000, togglesUpdatedListener = listener, pollImmediate = false)
4040
}
4141

42+
/**
43+
* Creates a configured poller that fetches once at initialisation and then never polls
44+
* @param listener - What should the poller call when toggles are updated?
45+
* @param readyListener - What should the poller call when it has initialised its toggles cache
46+
*/
47+
fun fetchOnce(listener: TogglesUpdatedListener? = null, readyListener: ReadyListener? = null): PollingMode {
48+
return AutoPollingMode(pollRateDuration = 0, togglesUpdatedListener = listener, readyListener = readyListener)
49+
}
50+
4251
/**
4352
* Creates a configured auto polling config with a listener which receives updates when/if toggles get updated
4453
* @param intervalInMs - Sets intervalInMs for how often this policy should refresh the cache
@@ -60,8 +69,8 @@ object PollingModes {
6069
return AutoPollingMode(pollRateDuration = intervalInMs, togglesUpdatedListener = listener, pollImmediate = false)
6170
}
6271

63-
fun fileMode(toggleFile: File): PollingMode {
64-
return FilePollingMode(toggleFile)
72+
fun fileMode(toggleFile: File, readyListener: ReadyListener? = null): PollingMode {
73+
return FilePollingMode(toggleFile, readyListener)
6574
}
6675

6776

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.getunleash.polling
2+
3+
fun interface ReadyListener {
4+
fun onReady(): Unit
5+
}

src/main/kotlin/io/getunleash/polling/RefreshPolicy.kt

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import java.io.Closeable
99
import java.math.BigInteger
1010
import java.security.MessageDigest
1111
import java9.util.concurrent.CompletableFuture
12+
import java.util.concurrent.atomic.AtomicBoolean
1213

1314
/**
1415
* Used to define how to Refresh and serve toggles
@@ -27,6 +28,8 @@ abstract class RefreshPolicy(
2728
) : Closeable {
2829
internal val listeners: MutableList<TogglesUpdatedListener> = mutableListOf()
2930
internal val errorListeners: MutableList<TogglesErroredListener> = mutableListOf()
31+
internal val checkListeners: MutableList<TogglesCheckedListener> = mutableListOf()
32+
internal val readyListeners: MutableList<ReadyListener> = mutableListOf()
3033
private var inMemoryConfig: Map<String, Toggle> = emptyMap()
3134
private val cacheKey: String by lazy { sha256(cacheBase.format(this.config.clientKey)) }
3235

@@ -40,6 +43,8 @@ abstract class RefreshPolicy(
4043
}
4144
}
4245

46+
abstract val isReady: AtomicBoolean
47+
4348
fun readToggleCache(): Map<String, Toggle> {
4449
return try {
4550
this.cache.read(cacheKey)
@@ -52,6 +57,7 @@ abstract class RefreshPolicy(
5257
try {
5358
this.inMemoryConfig = value
5459
this.cache.write(cacheKey, value)
60+
broadcastTogglesUpdated()
5561
} catch (e: Exception) {
5662
}
5763
}
@@ -78,6 +84,38 @@ abstract class RefreshPolicy(
7884
}
7985
}
8086

87+
fun broadcastTogglesUpdated(): Unit {
88+
synchronized(listeners) {
89+
listeners.forEach {
90+
it.onTogglesUpdated()
91+
}
92+
}
93+
}
94+
95+
fun broadcastTogglesChecked() {
96+
synchronized(checkListeners) {
97+
checkListeners.forEach {
98+
it.onTogglesChecked()
99+
}
100+
}
101+
}
102+
103+
fun broadcastTogglesErrored(e: Exception) {
104+
synchronized(errorListeners) {
105+
errorListeners.forEach {
106+
it.onError(e)
107+
}
108+
}
109+
}
110+
111+
fun broadcastReady() {
112+
synchronized(readyListeners) {
113+
readyListeners.forEach {
114+
it.onReady()
115+
}
116+
}
117+
}
118+
81119
/**
82120
* Subclasses should override this to implement their way of manually starting polling after context is updated.
83121
* Typical usage would be to use [PollingModes.manuallyStartPolling] or [PollingModes.manuallyStartedPollMs] to create/configure your polling mode,
@@ -97,10 +135,26 @@ abstract class RefreshPolicy(
97135
}
98136

99137
fun addTogglesUpdatedListener(listener: TogglesUpdatedListener): Unit {
100-
listeners.add(listener)
138+
synchronized(listener) {
139+
listeners.add(listener)
140+
}
101141
}
102142

103143
fun addTogglesErroredListener(errorListener: TogglesErroredListener): Unit {
104-
errorListeners.add(errorListener)
144+
synchronized(errorListeners) {
145+
errorListeners.add(errorListener)
146+
}
147+
}
148+
149+
fun addTogglesCheckedListener(checkListener: TogglesCheckedListener) {
150+
synchronized(checkListeners) {
151+
checkListeners.add(checkListener)
152+
}
153+
}
154+
155+
fun addReadyListener(readyListener: ReadyListener) {
156+
synchronized(readyListeners) {
157+
readyListeners.add(readyListener)
158+
}
105159
}
106160
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.getunleash.polling
2+
3+
fun interface TogglesCheckedListener {
4+
fun onTogglesChecked(): Unit
5+
}

src/test/kotlin/io/getunleash/metrics/MetricsTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class MetricsTest {
105105

106106

107107
@Test
108-
fun `getVariant calls also records "yes" and "no"`() {
108+
fun `getVariant calls also records yes and no`() {
109109
val reporter = TestReporter()
110110
val client = UnleashClient(config, context, metricsReporter = reporter)
111111
repeat(100) {

0 commit comments

Comments
 (0)