Skip to content

Commit 5e0519a

Browse files
feat: with polling disabled be able to fetch on demand (#75)
* feat: with polling disabled be able to fetch at least once * chore: make sure we poll only once and not twice while reacting to context set
1 parent 9ae9c79 commit 5e0519a

File tree

6 files changed

+73
-27
lines changed

6 files changed

+73
-27
lines changed

unleashandroidsdk/src/main/java/io/getunleash/android/DefaultUnleash.kt

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import io.getunleash.android.cache.ObservableToggleCache
1313
import io.getunleash.android.cache.ToggleCache
1414
import io.getunleash.android.data.ImpressionEvent
1515
import io.getunleash.android.data.Parser
16-
import io.getunleash.android.polling.ProxyResponse
1716
import io.getunleash.android.data.Toggle
1817
import io.getunleash.android.data.UnleashContext
1918
import io.getunleash.android.data.UnleashState
@@ -80,7 +79,7 @@ class DefaultUnleash(
8079
private val cache: ObservableToggleCache = ObservableCache(cacheImpl, coroutineScope)
8180
private var started = AtomicBoolean(false)
8281
private var ready = AtomicBoolean(false)
83-
private val fetcher: UnleashFetcher?
82+
private val fetcher: UnleashFetcher
8483
private val networkStatusHelper = NetworkStatusHelper(androidContext)
8584
private val impressionEventsFlow = MutableSharedFlow<ImpressionEvent>(
8685
replay = 1,
@@ -97,12 +96,11 @@ class DefaultUnleash(
9796
httpClientBuilder.build("metrics", unleashConfig.metricsStrategy)
9897
)
9998
else NoOpMetrics()
100-
fetcher = if (unleashConfig.pollingStrategy.enabled)
101-
UnleashFetcher(
99+
fetcher = UnleashFetcher(
102100
unleashConfig,
103101
httpClientBuilder.build("poller", unleashConfig.pollingStrategy),
104102
unleashContextState.asStateFlow()
105-
) else null
103+
)
106104
taskManager = LifecycleAwareTaskManager(
107105
dataJobs = buildDataJobs(metrics, fetcher),
108106
networkAvailable = networkStatusHelper.isAvailable(),
@@ -130,10 +128,10 @@ class DefaultUnleash(
130128
val localBackup = getLocalBackup()
131129
localBackup.subscribeTo(cache.getUpdatesFlow())
132130
}
133-
fetcher?.let {
134-
it.startWatchingContext()
135-
cache.subscribeTo(it.getFeaturesReceivedFlow())
131+
if (unleashConfig.pollingStrategy.enabled) {
132+
fetcher.startWatchingContext()
136133
}
134+
cache.subscribeTo(fetcher.getFeaturesReceivedFlow())
137135
lifecycle.addObserver(taskManager)
138136
if (bootstrapFile != null && bootstrapFile.exists()) {
139137
Log.i(TAG, "Using provided bootstrap file")
@@ -148,8 +146,8 @@ class DefaultUnleash(
148146
}
149147
}
150148

151-
private fun buildDataJobs(metricsSender: MetricsReporter, fetcher: UnleashFetcher?) = buildList {
152-
if (fetcher != null) {
149+
private fun buildDataJobs(metricsSender: MetricsReporter, fetcher: UnleashFetcher) = buildList {
150+
if (unleashConfig.pollingStrategy.enabled) {
153151
add(
154152
DataJob(
155153
"fetchToggles",
@@ -223,15 +221,15 @@ class DefaultUnleash(
223221
override fun refreshTogglesNow() {
224222
runBlocking {
225223
withContext(Dispatchers.IO) {
226-
fetcher?.refreshToggles()
224+
fetcher.refreshToggles()
227225
}
228226
}
229227
}
230228

231229
override fun refreshTogglesNowAsync() {
232230
coroutineScope.launch {
233231
withContext(Dispatchers.IO) {
234-
fetcher?.refreshToggles()
232+
fetcher.refreshToggles()
235233
}
236234
}
237235
}
@@ -271,7 +269,7 @@ class DefaultUnleash(
271269
if (started.get()) {
272270
runBlocking {
273271
withTimeout(timeout) {
274-
fetcher?.refreshToggles()
272+
fetcher.refreshToggles()
275273
}
276274
}
277275
}
@@ -306,7 +304,7 @@ class DefaultUnleash(
306304
}
307305
}
308306

309-
if (fetcher != null && listener is UnleashFetcherHeartbeatListener) coroutineScope.launch {
307+
if (listener is UnleashFetcherHeartbeatListener) coroutineScope.launch {
310308
fetcher.getHeartbeatFlow().collect { event ->
311309
if (event.status.isFailed()) {
312310
listener.onError(event)

unleashandroidsdk/src/main/java/io/getunleash/android/UnleashConfig.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import java.util.UUID
66

77
/**
88
* Represents configuration for Unleash.
9-
* @property proxyUrl HTTP(s) URL to the Unleash Proxy (Required).
10-
* @property clientKey the key added as the Authorization header sent to the unleash-proxy (Required)
9+
* @property proxyUrl HTTP(s) URL to the Unleash Proxy (Optional).
10+
* @property clientKey the key added as the Authorization header sent to the unleash-proxy (Optional)
1111
* @property appName: name of the underlying application. Will be used as default in the [io.getunleash.android.data.UnleashContext] call (Required).
1212
* @property pollingStrategy How to poll for features. (Optional - Defaults to [io.getunleash.android.data.DataStrategy] with poll interval set to 60 seconds).
1313
* @property metricsStrategy How to poll for metrics. (Optional - Defaults to [io.getunleash.android.data.DataStrategy] with poll interval set to 60 seconds).
1414
*/
1515
data class UnleashConfig(
16-
val proxyUrl: String,
17-
val clientKey: String,
16+
val proxyUrl: String?,
17+
val clientKey: String?,
1818
val appName: String,
1919
val localStorageConfig: LocalStorageConfig = LocalStorageConfig(),
2020
val pollingStrategy: DataStrategy = DataStrategy(
@@ -36,8 +36,9 @@ data class UnleashConfig(
3636
val instanceId: String get() = Companion.instanceId
3737

3838
fun getApplicationHeaders(strategy: DataStrategy): Map<String, String> {
39+
val auth = clientKey ?: ""
3940
return strategy.httpCustomHeaders.plus(mapOf(
40-
"Authorization" to clientKey,
41+
"Authorization" to auth,
4142
"Content-Type" to "application/json",
4243
"UNLEASH-APPNAME" to appName,
4344
"User-Agent" to appName,
@@ -66,8 +67,8 @@ data class UnleashConfig(
6667
throw IllegalStateException("You must either set proxyUrl and clientKey or disable both polling and metrics.")
6768
}
6869
return UnleashConfig(
69-
proxyUrl = proxyUrl ?: "",
70-
clientKey = clientKey ?: "",
70+
proxyUrl = proxyUrl,
71+
clientKey = clientKey,
7172
appName = appName,
7273
pollingStrategy = pollingStrategy.build(),
7374
metricsStrategy = metricsStrategy.build(),

unleashandroidsdk/src/main/java/io/getunleash/android/metrics/MetricsSender.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ class MetricsSender(
2626
companion object {
2727
private const val TAG: String = "MetricsSender"
2828
}
29-
private val metricsUrl = config.proxyUrl.toHttpUrl().newBuilder().addPathSegment("client")
30-
.addPathSegment("metrics").build()
29+
30+
private val metricsUrl = config.proxyUrl?.toHttpUrl()?.newBuilder()?.addPathSegment("client")
31+
?.addPathSegment("metrics")?.build()
3132
private var bucket: CountBucket = CountBucket(start = Date())
3233
private val throttler =
3334
Throttler(
@@ -37,6 +38,10 @@ class MetricsSender(
3738
)
3839

3940
override suspend fun sendMetrics() {
41+
if (metricsUrl == null) {
42+
Log.d(TAG, "No proxy URL configured, skipping metrics reporting")
43+
return
44+
}
4045
if (bucket.isEmpty()) {
4146
Log.d(TAG, "No metrics to report")
4247
return

unleashandroidsdk/src/main/java/io/getunleash/android/polling/UnleashFetcher.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ open class UnleashFetcher(
5252
private const val TAG = "UnleashFetcher"
5353
}
5454

55-
private val proxyUrl = unleashConfig.proxyUrl.toHttpUrl()
55+
private val proxyUrl = unleashConfig.proxyUrl?.toHttpUrl()
5656
private val applicationHeaders = unleashConfig.getApplicationHeaders(unleashConfig.pollingStrategy)
5757
private val appName = unleashConfig.appName
5858
private var etag: String? = null
@@ -123,6 +123,9 @@ open class UnleashFetcher(
123123
}
124124

125125
private suspend fun fetchToggles(ctx: UnleashContext): FetchResponse {
126+
if (proxyUrl == null) {
127+
return FetchResponse(Status.FAILED, error = IllegalStateException("Proxy URL is not set"))
128+
}
126129
val contextUrl = buildContextUrl(ctx)
127130
try {
128131
val request = Request.Builder().url(contextUrl)
@@ -208,7 +211,7 @@ open class UnleashFetcher(
208211
}
209212

210213
private fun buildContextUrl(ctx: UnleashContext): HttpUrl {
211-
var contextUrl = proxyUrl.newBuilder()
214+
var contextUrl = proxyUrl!!.newBuilder()
212215
.addQueryParameter("appName", appName)
213216
if (ctx.userId != null) {
214217
contextUrl.addQueryParameter("userId", ctx.userId)

unleashandroidsdk/src/test/java/io/getunleash/android/DefaultUnleashTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.assertj.core.groups.Tuple
1919
import org.awaitility.Awaitility.await
2020
import org.junit.Test
2121
import org.mockito.Mockito.mock
22+
import org.mockito.Mockito.verify
2223
import org.robolectric.shadows.ShadowLog
2324
import java.io.File
2425
import java.util.concurrent.TimeUnit
@@ -444,4 +445,42 @@ class DefaultUnleashTest : BaseTest() {
444445
await().atMost(2, TimeUnit.SECONDS).until { stateSet }
445446
assertThat(inspectableCache.toggles).hasSize(3)
446447
}
448+
449+
@Test
450+
fun `when polling is disable should still be able to poll on demand`() {
451+
val server = MockWebServer()
452+
server.enqueue(
453+
MockResponse().setBody(
454+
this::class.java.classLoader?.getResource("sample-response.json")!!.readText()
455+
)
456+
)
457+
458+
val inspectableCache = InspectableCache()
459+
val unleash = DefaultUnleash(
460+
androidContext = mock(Context::class.java),
461+
unleashConfig = UnleashConfig.newBuilder("test-android-app")
462+
.proxyUrl(server.url("").toString())
463+
.clientKey("key-123")
464+
.pollingStrategy.enabled(false)
465+
.metricsStrategy.enabled(false)
466+
.localStorageConfig.enabled(false)
467+
.build(),
468+
unleashContext = UnleashContext(userId = "123"),
469+
cacheImpl = inspectableCache,
470+
lifecycle = mock(Lifecycle::class.java),
471+
)
472+
473+
var readyState = false
474+
unleash.start(bootstrap = staticToggleList, eventListeners = listOf(object : UnleashReadyListener {
475+
override fun onReady() {
476+
readyState = true
477+
}
478+
}))
479+
await().atMost(3, TimeUnit.SECONDS).until { readyState }
480+
assertThat(inspectableCache.toggles).hasSize(staticToggleList.size)
481+
482+
unleash.refreshTogglesNow()
483+
await().atMost(2, TimeUnit.SECONDS).until { inspectableCache.toggles.size == 8 }
484+
assertThat(server.requestCount).isEqualTo(1)
485+
}
447486
}

unleashandroidsdk/src/test/java/io/getunleash/android/UnleashConfigTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class UnleashConfigTest {
4949
.metricsStrategy.enabled(false)
5050
.build()
5151

52-
assertThat(config.proxyUrl).isEmpty()
53-
assertThat(config.clientKey).isEmpty()
52+
assertThat(config.proxyUrl).isNull()
53+
assertThat(config.clientKey).isNull()
5454
}
5555
}

0 commit comments

Comments
 (0)