dave08
10/13/2024, 10:28 AMout { }
? I'd maybe make it into a flow... I know I could probably use the cache's progress, but that's less finely grained (unless I couple caching into my generator...).dave08
10/13/2024, 10:33 AMprogressive { }
... I couldn't find it in the kdocs... but thanks to intellij's ctrl-click, I found it...CLOVIS
10/13/2024, 11:05 AMdave08
10/13/2024, 11:06 AMdave08
10/13/2024, 11:06 AMdave08
10/13/2024, 11:06 AMCLOVIS
10/13/2024, 11:06 AMdave08
10/13/2024, 11:07 AMCLOVIS
10/13/2024, 11:08 AMsuspend fun
?dave08
10/13/2024, 11:09 AMCLOVIS
10/13/2024, 11:09 AMcache {}
builder will automatically capture calls to report
and transform them into a Flow
CLOVIS
10/13/2024, 11:12 AMprivate val imageCache = cache {
// generate your image,
// use 'report' to report progress
}.cachedInMemory(…)
.expiresAfter(…)
fun getImage(id: String): ProgressiveFlow<Image> = imageCache[id]
.map {
if (it is ProgressiveOutcome.Loading) {
yourDefaultImage()
} else it
}
dave08
10/13/2024, 11:15 AMdave08
10/13/2024, 11:16 AMCLOVIS
10/13/2024, 11:18 AMthere's no indication in the function's declaration that there's progress being managedYes, that's the goal. If you don't register something that wants to know about progress events, they're just ignored. This way, you can insert progress events anywhere and you know it won't break existing code.
CLOVIS
10/13/2024, 11:18 AMAlso, the context might change when using another withContext or a new coroutine and mess up the whole thing, no?Worst case, you lose progress events. But the actual data (the result of the operation) is safe.
dave08
10/13/2024, 11:21 AMOutcome
? So I'd just use a simple Outcome
instead of a ProgressiveOutcome
? Would the cache track that too?CLOVIS
10/13/2024, 11:22 AMSo there's no real way to do this more explicitly?You can pass
ProgressReporter
around (as a receiver or context parameter) if you prefer.dave08
10/13/2024, 11:26 AMreport(...)
inside the progressive { }
block?CLOVIS
10/13/2024, 11:36 AMprogressive {}
block is very new, it's not stable yetCLOVIS
10/13/2024, 11:36 AMCLOVIS
10/13/2024, 11:37 AMcache {}
builder and you won't need to create progressive values yourselfdave08
10/13/2024, 11:41 AMreport(...)
on it would be useful, since the current implementation requires to call a function that returns an Outcome or ProgressiveOutcome, and passes a Progress in the bind function... sometimes part of the progress is in the function with the progressive { } block itself.|
I have multiple stages to the generation of images, and any one of those steps can fail, that's why I'm trying not to do it straight in the cache block.
Progress isn't as critical here, but while I'm at it (I do need loading failed or done at the very least), why not track that too for debugging and performance.CLOVIS
10/13/2024, 11:42 AMdave08
10/13/2024, 11:44 AMclass ImageFormatProviderImpl(
private val targetBucket: String,
private val formattedFolder: String,
private val formatGenerator: FormatGenerator,
private val formatStorage: ImageFormatStorage,
) : ImageFormatProvider {
private fun String.toName(imageFormatting: ImageFormatting) =
"$formattedFolder/${imageFormatting.folderName}/$this.png"
@OptIn(ExperimentalProgressiveRaiseApi::class)
override suspend fun provideFormats(
source: OriginalSource,
formats: Set<ImageFormatRequest>
): ProgressiveOutcome<Nothing, GeneratedImageFormats> {
return progressive {
val original = formatStorage.getOriginal(source)
val generated = formatGenerator.generateFormat(original, formats)
val originalContentSha1 = "ext/" + (original.toByteString().sha1().hex())
generated.associate {
val newKey = originalContentSha1.toName(it.first.imageFormatting)
formatStorage.putGenerated(targetBucket, newKey, it.second)
it.first.name to GeneratedImageFormat(targetBucket, newKey, it.first)
}.let(::GeneratedImageFormats)
}
}
}
CLOVIS
10/13/2024, 11:52 AMCLOVIS
10/13/2024, 11:52 AMProgressiveOutcome<Nothing, T>
is the same as Progressive<T>
, since there are no possible errorsCLOVIS
10/13/2024, 11:54 AMProgressiveOutcome
, I don't see anything that comes from Pedestal here 🤔dave08
10/13/2024, 11:59 AMThis doesn't use the cache, right?not yet.
I didn't yet integrate error handling, I'm trying to figure out progress and caching first, so I started with Nothing to keep the project compiling.is the same as...ProgressiveOutcome<Nothing, T>
dave08
10/13/2024, 12:00 PMraise(...)
in all the places that can cause errors and bind()
the results of all those functions.dave08
10/13/2024, 12:06 PMdave08
10/13/2024, 12:14 PMcache { }
doesn't take ProgressiveOutcome, only Outcome...CLOVIS
10/13/2024, 12:14 PMreport
itselfdave08
10/13/2024, 12:14 PMdave08
10/13/2024, 12:15 PMCLOVIS
10/13/2024, 12:15 PMOutcome<Error, Success>
and use implicit progress reporting where it makes sense toCLOVIS
10/13/2024, 12:18 PMOutcome<Error, Success>
at all, you'll be able to just use Raise<Error>
!
Now:
suspend fun foo(id: Int): Outcome<String, Int> = out {
report(loading(0.0))
ensure(id > 0) { "Too small" }
report(loading(0.5))
id.toString()
}
In the future:
context(_: Raise<String>)
suspend fun foo(id: Int): String {
report(loading(0.0))
ensure(id > 0) { "Too small" }
report(loading(0.5))
return id.toString
}
dave08
10/13/2024, 12:20 PMdave08
10/13/2024, 12:23 PMCLOVIS
10/13/2024, 12:25 PMI guess it would be nice to have the same for the ProgressReporter... to be able to declare an explicit one in the the context parameters.That's what
ProgressReporter
is!
context(_: Raise<String>, reporter: ProgressReporter)
suspend fun foo(id: Int): String {
reporter.report(loading(0.0))
ensure(id > 0) { "Too small" }
reporter.report(loading(0.5))
return id.toString
}
This is possible with just
dev.opensavvy.pedestal:progress
Personally, I prefer having it be implicit, which is provided by the bonus module
dev.opensavvy.pedestal:progress-coroutines
CLOVIS
10/13/2024, 12:26 PMI really don't know why report doesn't ONLY take a loading progress.You don't need to
report(done())
basically ever, because all builders will do it for youdave08
10/13/2024, 12:28 PMCLOVIS
10/13/2024, 12:29 PMdone()
. I haven't really thought of it much.CLOVIS
10/13/2024, 12:29 PMCLOVIS
10/13/2024, 12:30 PMProgressReporter
, because the builders need to access thatdave08
10/14/2024, 11:37 AMcache.get(source)
.map {
when (it) {
is ProgressiveOutcome.Unsuccessful<*> -> TODO()
is ProgressiveOutcome.Success -> it.value.toImageVars(expiry)
}
}
.firstOrNull()
I put in delay in the generation in my fake, and it's still not crashing from the TODO()...dave08
10/14/2024, 11:38 AMdave08
10/14/2024, 11:39 AMCLOVIS
10/14/2024, 2:13 PMdave08
10/14/2024, 5:58 PMCLOVIS
10/14/2024, 7:32 PMMemoryCache
: https://gitlab.com/opensavvy/groundwork/pedestal/-/blob/main/cache/src/commonMain/kotlin/MemoryCache.kt?ref_type=heads#L76-L81dave08
10/15/2024, 2:57 AMCLOVIS
10/15/2024, 7:21 AMdave08
10/15/2024, 7:29 AMdave08
10/15/2024, 7:38 AMdave08
10/15/2024, 7:40 AMdave08
10/15/2024, 7:41 AMfirstOrNull()
doesn't help here, I need to get the current result or stop the flow...dave08
10/15/2024, 7:42 AMdave08
10/15/2024, 7:43 AMCLOVIS
10/15/2024, 7:59 AMFlow
, you can use the .timeout
operator, etcCLOVIS
10/15/2024, 7:59 AMFlow
operator, everything that works with Flow
usually should continue to workdave08
10/15/2024, 8:00 AMdave08
10/15/2024, 8:03 AMdave08
10/15/2024, 8:48 AMdave08
10/15/2024, 8:48 AMdave08
10/15/2024, 8:49 AM.onStart { emit(ProgressiveOutcome.Incomplete()) }
otherwise the cache wouldn't return it at all..,.dave08
10/15/2024, 8:53 AMlatestValueWithin
(with another name...) as an alternative to now()
that waits for the first successful value, here users can wait until a timeout for a successful value, otherwise they get Incomplete and do some kind of default like I did...CLOVIS
10/15/2024, 9:03 AMCLOVIS
10/15/2024, 9:03 AM.onStart
is a great workaround until I fix this upstreamdave08
10/15/2024, 9:04 AMdave08
10/15/2024, 9:08 AMnow()
function...)CLOVIS
10/15/2024, 9:26 AMnow
is just first { it.progress == done() }
, it does nothing complex (you can see its code)dave08
10/15/2024, 9:30 AMnow()
... but now that I think about it more, it might be better to have successfulNow()
since now()
implies whatever I have so far...dave08
10/15/2024, 9:30 AMdave08
10/15/2024, 9:32 AMsuccessfulNow(timeout...)
which waits for a successful result until the timeout otherwise return whatever's there.dave08
10/15/2024, 9:53 AMclass ConcurrencyLimiter<I, F, V>(
val upstream: Cache<I, F, V>,
val maxRequests: Int,
) : Cache<I, F, V> by upstream {
val semaphore = Semaphore(maxRequests)
override fun get(id: I): ProgressiveFlow<F, V> = flow {
semaphore.withPermit {
val result = upstream.get(id)
emitAll(result)
}
}
}
fun <I, F, V> Cache<I, F, V>.withConcurrencyLimit(maxRequests: Int) =
ConcurrencyLimiter(this, maxRequests)
dave08
10/15/2024, 9:54 AMCLOVIS
10/15/2024, 10:02 AM