Objectify stores its context as a thread-local variable. However, Kotlin's "coroutines" can jump threads as necessary when a "suspend" function waits for something. Whatever context was created for the original thread is then inaccessible, leading to failures.
The following KTOR 3.3.1 handler demonstrates this but it isn't specific to that system:
get("/test") {
val id = 5853572849213031L
val objectifyCloser = ObjectifyService.begin()
var user = ObjectifyService.ofy().load().type(UserInfo::class.java).id(id).now()
try {
for (i in 1..1000000000) {
kotlinx.coroutines.delay(1000)
RWLog.d(LOGTAG) { "${user.accountId}: ${i}" }
user = ObjectifyService.ofy().load().type(UserInfo::class.java).id(id).now()
}
} catch (e: Throwable) {
RWLog.d(LOGTAG) { "${user.accountId}: ${e.toString()}" }
}
objectifyCloser.close()
}
Older KTOR libraries didn't jump threads and would run just fine, spitting out one log message per second. With 3.3.1, it fails immediately:
2025.11.04 21:45:20.271 Main: 5853572849213031: 1
2025.11.04 21:45:20.278 Main: 5853572849213031: java.lang.IllegalStateException: You have not started an Objectify context. You are missing a call to run() or you do not have the ObjectifyFilter installed.
Objectify stores its context as a thread-local variable. However, Kotlin's "coroutines" can jump threads as necessary when a "suspend" function waits for something. Whatever context was created for the original thread is then inaccessible, leading to failures.
The following KTOR 3.3.1 handler demonstrates this but it isn't specific to that system:
Older KTOR libraries didn't jump threads and would run just fine, spitting out one log message per second. With 3.3.1, it fails immediately: