Testable Kotlin objects that uses a dispatcher in init block? - kotlin-coroutines

I have a class that looks like this:
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
init {
GlobalScope.launch(Dispatchers.IO) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
}
It is recommended to inject dispatchers in tests.
Also, there is a risk to get weird problems such as AppNotIdleException if not following this practice.
One option would be to make these into variables and add setters, but not very nice. Also it creates a race condition (init-block vs setters):
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
private var coroutineScope: CoroutineScope = GlobalScope
private var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
dispatcher.launch(coroutineScope) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
#VisibleForTesting
fun setDispatcher(dispatcher: CoroutineDispatcher) {
this.dispatcher = dispatcher
}
#VisibleForTesting
fun setCoroutineScope(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}
}
Version that avoids the race condition (test needs to explicitly invoke init()):
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
private var coroutineScope: CoroutineScope = GlobalScope
private var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
// var isInTesting = Build.FINGERPRINT == "robolectric"
if (!isInTesting) {
init()
}
}
#VisibleForTesting
fun init() {
dispatcher.launch(coroutineScope) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
#VisibleForTesting
fun setDispatcher(dispatcher: CoroutineDispatcher) {
this.dispatcher = dispatcher
}
#VisibleForTesting
fun setCoroutineScope(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}
}
How can I inject a coroutine scope and dispatcher to my tests and avoid variables?

Related

Ktor: Bearer token injected by Dagger Hilt fails at first

Calls made by Ktor fails first most of the time and then use the refreshTokens to get a token and re-try the calls..
This is not acceptable as I see a LOT of Unauthenticated calls, therefore most calls I get is made 2x, this effectively almost double my network call count.
The ONLY time the calls is authenticated properly is when the previous call was made to the same end point.
Question:
How can I improve this situation so that the token is provided ONCE to all Ktor end-points and
until the token is replaced (via valid login)
I use androidx.datastore:datastore-preferences to store my token and settingsRepository.getAuthToken() correctly retreive it
My DI
I use Dagger Hilt to inject my Bearer token this way
#Provides
#Singleton
fun provideBearerAuthProvider(
settingsRepository: SettingsRepository
) = BearerAuthProvider(
realm = null,
loadTokens = {
val token = settingsRepository.getAuthToken()
if (token == null) {
BearerTokens(accessToken = "fake", refreshToken = "")
} else {
BearerTokens(accessToken = token, refreshToken = "")
}
},
refreshTokens = {
val token = settingsRepository.requireAuthToken()
token?.let { BearerTokens(accessToken = it, refreshToken = "") }
},
sendWithoutRequestCallback = { httpRequestBuilder ->
httpRequestBuilder.url.host == USER_LOGIN
}
).also { bearerAuthProvider ->
settingsRepository.addOnClearListener(bearerAuthProvider::clearToken)
}
#Provides
#Singleton
fun provideApiClient(
bearerAuthProvider: BearerAuthProvider,
) = HttpClient(Android) {
install(Logging) {
logger = Logger.ANDROID
LogLevel.ALL
}
install(ContentNegotiation) {
gson()
}
install(Auth) {
providers += bearerAuthProvider
}
}
My ApiService
This is where the HttpClient is injected
abstract class BaseApiService(
protected val httpClient: HttpClient,
protected val baseUrl: String,
) {
protected suspend inline fun <reified T> get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T {
return httpClient.get(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> post(endpoint: String, block: HttpRequestBuilder.() -> Unit): T {
return httpClient.post(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> put(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T {
return httpClient.put(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> patch(endpoint: String, block: HttpRequestBuilder.() -> Unit): T {
return httpClient.patch(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> delete(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T {
return httpClient.delete(urlString = baseUrl + endpoint, block = block).body()
}
}
UserApiService
class UserApiService #Inject constructor(
httpClient: HttpClient
) : BaseApiService(httpClient, BASE_URL) {
suspend fun authenticate(): BasicApiResponse<Unit> =
get(endpoint = USER_AUTHENTICATE)
suspend fun login(loginRequest: LoginRequest): BasicApiResponse<AuthApiResponse> =
post(endpoint = USER_LOGIN) {
contentType(ContentType.Application.Json)
setBody(loginRequest)
}
suspend fun updateUserProfile(updateProfileRequest: UpdateProfileRequest): BasicApiResponse<UserApiResponse> =
patch(endpoint = USER) {
contentType(ContentType.Application.Json)
setBody(updateProfileRequest)
}
}
Notes:
I use data classes passing variables to the json payloads e.g.
data class LoginRequest(
val email: String? = null,
val password: String
)
In above code I also included the sendWithoutRequestCallback endpoint
sendWithoutRequestCallback = { httpRequestBuilder ->
httpRequestBuilder.url.host == USER_LOGIN
}

Method addObserver must be called on the main thread Exception, While inserting data to room database

I am trying to insert data into the room database using the kotlin coroutine. But I always get an exception java.lang.IllegalStateException: Method addObserver must be called on the main thread
But I don't have an observer in this code, the insert call is called from launch with Dispatchers IO
DocumentDao.kt
#Dao
interface DocumentDao {
#Insert
suspend fun insertDocument(document: Document): Long
}
Repository.kt
class Repository#Inject constructor(val db: MyDB) {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
val newDoc = Document(0, albumId, rawUri = uri)
return db.documentDao().insertDocument(newDoc)
}
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
return repo.demoInsert(uri, albumId)
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
....
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
}
........
.......
}
}
what is wrong with this code? please help me
I'm not sure about this but you can launch coroutine inside listener with Main Dispatcher and later use withContext inside DB function, to change context.
I was facing the same issue and I solved yhis way :
private fun insertAllItemsInDb(data : List<PostResponse>){
val listPost = data.map { it.toUI() }
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
localViewModel.insertAllPosts(listPost)
}
}
ViewModel:
fun insertAllPosts(posts: List<PostItem>) {
viewModelScope.launch {
dbRepository.insertAllPosts(posts)
}
}
Creating view model with:
val viewModel: MyViewModel by viewModels()
Will result in lazy creating. Creation of real object will be performed when you access your object for first time. This happens inside:
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
And since implementation of method viewModels<>() looks like this:
#MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
You are getting
Method addObserver must be called on the main thread
You should be able to fix this with something like this.
lifecycleScope.launch(Dispatchers.IO) {
val res = withContext(Dispatchers.Main + lifecycleScope.coroutineContext){}.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
viewModelScope.launch {
repo.demoInsert(uri, albumId)
}
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
viewModel.demoInsert("test", Random.nextLong(500))
}
}
}
}

How to resolve GOOGLE_APPLICATION_CREDENTIALS when running app in test, Spring Boot?

I have a Spring Boot application dependent on Google PubSub. I want to run it with a Google Cloud PubSub emulator. How can I resolve GOOGLE_APPLICATION_CREDENTIALS, so the app will start and consume messages from the local emulator, not an external project?
At the moment, if I set GOOGLE_APPLICATION_CREDENTIALS to dev.json, PubSub doesn't get invoked if I don't set the variable, test crashes. How can I overcome it? I cannot put puzzles together.
NOTE: I am writing an integration test with a full Spring boot startup.
My PubSub implementation:
import com.github.dockerjava.api.exception.DockerClientException
import com.google.api.gax.core.NoCredentialsProvider
import com.google.api.gax.grpc.GrpcTransportChannel
import com.google.api.gax.rpc.FixedTransportChannelProvider
import com.google.api.gax.rpc.TransportChannelProvider
import com.google.cloud.pubsub.v1.*
import com.google.cloud.pubsub.v1.stub.GrpcSubscriberStub
import com.google.cloud.pubsub.v1.stub.SubscriberStubSettings
import com.google.protobuf.ByteString
import com.google.pubsub.v1.*
import com.greenbird.server.contracts.TestServer
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import org.testcontainers.containers.PubSubEmulatorContainer
import org.testcontainers.utility.DockerImageName
import java.util.concurrent.TimeUnit
class PubSubTestServer(private val projectName: ProjectName, private val ports: Array<Int> = arrayOf(8085)) :
TestServer {
constructor(projectId: String): this(ProjectName.of(projectId))
private val projectId = projectName.project
var emulator: PubSubEmulatorContainer = PubSubEmulatorContainer(
DockerImageName.parse("gcr.io/google.com/cloudsdktool/cloud-sdk:latest")
)
private var channels: MutableList<ManagedChannel> = mutableListOf()
private fun channel(): ManagedChannel {
return if (channels.isEmpty()) {
val endpoint = emulator.emulatorEndpoint
val channel = ManagedChannelBuilder
.forTarget(endpoint)
.usePlaintext()
.build()
channels.add(channel)
channel
} else {
channels.first()
}
}
private val channelProvider: TransportChannelProvider
get() {
return FixedTransportChannelProvider
.create(
GrpcTransportChannel.create(channel())
)
}
private val credentialsProvider: NoCredentialsProvider = NoCredentialsProvider.create()
private val topicAdminSettings: TopicAdminSettings
get() {
when {
emulator.isRunning -> {
return buildTopicAdminSettings()
}
else -> {
throw DockerClientException("Topic admin settings attempted to initialize before starting PubSub emulator")
}
}
}
private fun buildTopicAdminSettings(): TopicAdminSettings {
return TopicAdminSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(credentialsProvider)
.build()
}
private val subscriptionAdminSettings: SubscriptionAdminSettings
get() {
when {
emulator.isRunning -> {
return buildSubscriptionAdminSettings()
}
else -> {
throw DockerClientException("Subscription admin settings attempted to initialize before starting PubSub emulator")
}
}
}
private fun buildSubscriptionAdminSettings(): SubscriptionAdminSettings {
return SubscriptionAdminSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(credentialsProvider)
.build()
}
override fun start() {
emulator.withExposedPorts(*ports).start()
}
override fun stop() {
terminate()
emulator.stop()
}
private fun terminate() {
for (channel in channels) {
channel.shutdownNow()
channel.awaitTermination(5, TimeUnit.SECONDS)
}
}
fun createTopic(topicId: String) {
TopicAdminClient.create(topicAdminSettings).use { topicAdminClient ->
val topicName = TopicName.of(projectId, topicId)
topicAdminClient.createTopic(topicName)
}
}
fun listTopics(): List<String> {
return TopicAdminClient.create(topicAdminSettings)
.listTopics(projectName)
.iterateAll()
.map { it.name }
.toList()
}
fun createSubscription(subscriptionId: String, topicId: String) {
val subscriptionName = ProjectSubscriptionName.of(projectId, subscriptionId)
SubscriptionAdminClient.create(subscriptionAdminSettings).createSubscription(
subscriptionName,
TopicName.of(projectId, topicId),
PushConfig.getDefaultInstance(),
10
)
}
fun listSubscriptions(): List<String> {
return SubscriptionAdminClient.create(subscriptionAdminSettings)
.listSubscriptions(projectName)
.iterateAll()
.map { it.name }
.toList()
}
fun push(topicId: String, message: String) {
val publisher: Publisher = Publisher.newBuilder(TopicName.of(projectId, topicId))
.setChannelProvider(channelProvider)
.setCredentialsProvider(credentialsProvider)
.build()
val pubsubMessage: PubsubMessage = PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8(message)).build()
publisher.publish(pubsubMessage).get()
}
fun poll(size: Int, subscriptionId: String): List<String> {
val subscriberStubSettings: SubscriberStubSettings = SubscriberStubSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(credentialsProvider)
.build()
GrpcSubscriberStub.create(subscriberStubSettings).use { subscriber ->
val pullRequest: PullRequest = PullRequest.newBuilder()
.setMaxMessages(size)
.setSubscription(ProjectSubscriptionName.format(projectId, subscriptionId))
.build()
val pullResponse: PullResponse = subscriber.pullCallable().call(pullRequest)
return pullResponse.receivedMessagesList
.map { it.message.data.toStringUtf8() }
.toList()
}
}
}
I couldn't find the answer to my question as it was asked.
I found a workaround for Junit5 with junit-pioneer it's possible to set the env variable to something before the actual test run.
Therefore, the code will be the same as before but annotated with #SetEnvironmentVariable
#SetEnvironmentVariable(key="GOOGLE_APPLICATION_CREDENTIALS", value="dev.json")
class PubSubTestServer {
...
}
JUnit-pioneer: Maven central.

Spring reactor parallel flux is stuck

I am using reactor to create an infinite flux,
once I make it parallel, the stream gets stuck after the first passed value, can't figure out why
val source = source().parallel().runOn(Schedulers.parallel())
.map(this::toUpperCase)
.subscribe(sink())
private fun sink() = SimpleSink<SimpleDaoModel>()
private fun toUpperCase(simpleDaoModel: SimpleDaoModel) = simpleDaoModel.copy(stringValue = simpleDaoModel.stringValue.toUpperCase())
private fun source() = Flux.create { sink: FluxSink<SimpleDaoModel> ->
fun getNextAsync(): Job = GlobalScope.launch(Dispatchers.Default) {
val task = customSimpleModelRepository.getNextTask()
if (task != null) {
logger.info("emitting next task")
sink.next(task)
} else {
logger.info("No more tasks")
Timer("nextTaskBackoff", false).schedule(1000) {
getNextAsync()
}
}
}
sink.onRequest { getNextAsync() }
}
class SimpleSink<T> : BaseSubscriber<T>() {
public override fun hookOnSubscribe(subscription: Subscription) {
println("Subscribed")
request(1)
}
public override fun hookOnNext(value: T) {
println(value)
request(1)
}
}
If I remove the parallel operator, everything works like a charm.
Note: getNextTask is a suspended function

Customize SLF4J Logger

I'm trying to find a nice way to add a prefix to my logs without passing it on every calls, without instanciate Logger again.
The purpose is to trace Rest calls individually.
(The prefix would be re-generated on each call using UUID)
This would be like
#RestController
class MyClass {
//Here the prefix is initialise once
//default value is X
Logger LOG = LoggerFactory.getLogger(MyClass.class);
#RequestMapping("/a")
void methodA() {
LOG.debug("foo");
}
#RequestMapping("/b")
void methodB() {
LOG.setPrefix("B");
LOG.debug("bar");
}
with this output
[...] [prefix X] foo
[...] [prefix B] bar
As you've said you're using Logback, here's a couple options to do the kind of thing you're trying to do:
Markers
Each log entry can have a "marker" established for it. (The best documentation I've seen for it is in the SLF4J FAQ.) Something like:
class MyClass {
Marker methodBMarker = MarkerFactory.getMarker("B");
Logger logger = LoggerFactory.getLogger(MyClass.class);
…
void methodB() {
logger.debug(methodBMarker, "bar");
}
}
You would need to update all log entries in each method to use the appropriate marker. You can then put %marker in your layout to put the log entry's marker into the log.
MDC
The other option is to use the "Mapped Diagnostic Context" functionality to specify the current "context" for each log entry.
class MyClass {
Logger logger = LoggerFactory.getLogger(MyClass.class);
…
void methodB() {
MDC.put("method", "b");
try {
…
logger.debug("bar");
…
} finally {
MDC.clear();
}
}
}
You would then use %mdc{method} in your layout to output that particular MDC value. Note that MDC is really intended to be used for per-thread values like something web-connection-specific, so it's important to ensure that it's cleared out of what you don't want when you're leaving the context you want the value logged in.
Please see http://www.slf4j.org/extensions.html#event_logger for an example of how to use the MDC. You do not have to use the EventLogger. Once you set things in the MDC they are present in every log record.
A Marker does not meet your criteria since it has to be specified on every call.
Here's my MDC implementation explained to share my experiments with MDC.
//In this abstract class i'm defining initLogData methods to set MDC context
//It would be inherited by Controller and other classes who needs logging with traced transactions
public abstract class AbstractService {
protected LogData initLogData() {
return LogData.init();
}
protected LogData initLogData(String tName) {
return LogData.init(tName);
}
}
//LogData holds the MDC logic
public class LogData {
private final static int nRandom = 8;
//this keys are defined in logback pattern (see below)
private final static String tIdKey = "TID";
private final static String tNameKey = "TNAME";
//Transaction id
private String tId;
//Transaction name
private String tName;
public String getTId() {
return tId;
}
public void setTId(String tId) {
this.tId = tId;
}
public String gettName() {
return tName;
}
public void settName(String tName) {
this.tName = tName;
}
//random transaction id
//I'm not using uuid since its too longs and perfect unicity is not critical here
public String createTId(){
Random r = new Random();
StringBuilder sb = new StringBuilder();
while(sb.length() < nRandom){
sb.append(Integer.toHexString(r.nextInt()));
}
return sb.toString().substring(0, nRandom);
}
//private constructors (use init() methods to set LogData)
private LogData(String tId, String tName) {
this.tId = tId;
this.tName = tName;
}
private LogData(String tName) {
this.tId = createTId();
this.tName = tName;
}
private LogData() {
this.tId = createTId();
}
//init MDC with cascading calls processing (using same id/name within same context
//even if init() is called again)
public static LogData init(String tName) {
String previousTId = MDC.get(tIdKey);
String previousTName = MDC.get(tNameKey);
MDC.clear();
LogData logData = null;
if(previousTId != null) {
logData = new LogData(previousTId, previousTName);
} else {
logData = new LogData(tName);
}
MDC.put(tIdKey, logData.getTId());
MDC.put(tNameKey, logData.gettName());
return logData;
}
//init MDC without cascading calls management (new keys are generated for each init() call)
public static LogData init() {
MDC.clear();
LogData logData = new LogData();
MDC.put(tIdKey, logData.getTId());
return logData;
}
}
//logback.xml : values to include in log pattern
[%X{TID}] [%X{TNAME}]
#RestController
#RequestMapping("/test")
public class RestControllerTest extends AbstractRestService {
private final Logger LOG = LoggerFactory.getLogger(ServiceRestEntrypointStatus.class);
#RequestMapping(value="/testA")
public void testA() {
initLogData("testA");
LOG.debug("This is A");
}
#RequestMapping(value="/testB")
public void testB() {
initLogData("testA");
LOG.debug("This is B");
}
#RequestMapping(value="/testC")
public void testC() {
initLogData("testC");
LOG.debug("This is C");
testA();
testB();
}
}
Calling RestControllerTest mapped /test/testA produces :
[fdb5d310] [testA] This is A
Calling /test/testC produces (id and name are kept even if initLogData is called in sub methods):
[c7b0af53] [testC] This is C
[c7b0af53] [testC] This is A
[c7b0af53] [testC] This is B

Resources