diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-11-01 10:38:26 +0100 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-11-27 20:49:33 +0000 |
| commit | 6bbfbd7aeb99475308a140222316f3e9006aeec3 (patch) | |
| tree | ab590fa208cb98edd419fa55873a01d17ee67433 /opendc-compute | |
| parent | 2ca72c0b62e08efd244eba1723bc4fc65d30eed2 (diff) | |
refactor(compute/api): Do not suspend in compute API
This change updates the API interface of the OpenDC Compute service to
not suspend execution using Kotlin Coroutines.
The suspending modifiers were introduced in case the ComputeClient would
communicate with the service over a network connection. However, the main
use-case has been together with the ComputeService, where the suspending
modifiers only frustrate the user experience when writing experiments.
Furthermore, with the advent of Project Loom, it is not necessarily a
problem to block the (virtual) thread during network communications.
Diffstat (limited to 'opendc-compute')
15 files changed, 177 insertions, 194 deletions
diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt index 577fbc73..c26d0b8b 100644 --- a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt +++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ComputeClient.kt @@ -31,14 +31,14 @@ public interface ComputeClient : AutoCloseable { /** * Obtain the list of [Flavor]s accessible by the requesting user. */ - public suspend fun queryFlavors(): List<Flavor> + public fun queryFlavors(): List<Flavor> /** * Obtain a [Flavor] by its unique identifier. * * @param id The identifier of the flavor. */ - public suspend fun findFlavor(id: UUID): Flavor? + public fun findFlavor(id: UUID): Flavor? /** * Create a new [Flavor] instance at this compute service. @@ -49,7 +49,7 @@ public interface ComputeClient : AutoCloseable { * @param labels The identifying labels of the image. * @param meta The non-identifying meta-data of the image. */ - public suspend fun newFlavor( + public fun newFlavor( name: String, cpuCount: Int, memorySize: Long, @@ -60,14 +60,14 @@ public interface ComputeClient : AutoCloseable { /** * Obtain the list of [Image]s accessible by the requesting user. */ - public suspend fun queryImages(): List<Image> + public fun queryImages(): List<Image> /** * Obtain a [Image] by its unique identifier. * * @param id The identifier of the image. */ - public suspend fun findImage(id: UUID): Image? + public fun findImage(id: UUID): Image? /** * Create a new [Image] instance at this compute service. @@ -76,7 +76,7 @@ public interface ComputeClient : AutoCloseable { * @param labels The identifying labels of the image. * @param meta The non-identifying meta-data of the image. */ - public suspend fun newImage( + public fun newImage( name: String, labels: Map<String, String> = emptyMap(), meta: Map<String, Any> = emptyMap() @@ -85,14 +85,14 @@ public interface ComputeClient : AutoCloseable { /** * Obtain the list of [Server]s accessible by the requesting user. */ - public suspend fun queryServers(): List<Server> + public fun queryServers(): List<Server> /** * Obtain a [Server] by its unique identifier. * * @param id The identifier of the server. */ - public suspend fun findServer(id: UUID): Server? + public fun findServer(id: UUID): Server? /** * Create a new [Server] instance at this compute service. @@ -104,7 +104,7 @@ public interface ComputeClient : AutoCloseable { * @param meta The non-identifying meta-data of the server. * @param start A flag to indicate that the server should be started immediately. */ - public suspend fun newServer( + public fun newServer( name: String, image: Image, flavor: Flavor, diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt index 5f511f91..d76e0fba 100644 --- a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt +++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Flavor.kt @@ -36,9 +36,4 @@ public interface Flavor : Resource { * The amount of RAM available to the server (in MB). */ public val memorySize: Long - - /** - * Delete the flavor instance. - */ - public suspend fun delete() } diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt index 83e63b81..c4a04b96 100644 --- a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt +++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Image.kt @@ -25,9 +25,4 @@ package org.opendc.compute.api /** * An image containing a bootable operating system that can directly be executed by physical or virtual server. */ -public interface Image : Resource { - /** - * Delete the image instance. - */ - public suspend fun delete() -} +public interface Image : Resource diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt index 08120848..58082130 100644 --- a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt +++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Resource.kt @@ -49,7 +49,12 @@ public interface Resource { public val meta: Map<String, Any> /** - * Refresh the local state of the resource. + * Reload the attributes of the resource. */ - public suspend fun refresh() + public fun reload() + + /** + * Delete the resource. + */ + public fun delete() } diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt index 64b73d0b..b4cc5129 100644 --- a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt +++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/Server.kt @@ -50,27 +50,13 @@ public interface Server : Resource { /** * Request the server to be started. - * - * This method is guaranteed to return after the request was acknowledged, but might return before the server was - * started. */ - public suspend fun start() + public fun start() /** * Request the server to be stopped. - * - * This method is guaranteed to return after the request was acknowledged, but might return before the server was - * stopped. - */ - public suspend fun stop() - - /** - * Request the server to be deleted. - * - * This method is guaranteed to return after the request was acknowledged, but might return before the server was - * deleted. */ - public suspend fun delete() + public fun stop() /** * Register the specified [ServerWatcher] to watch the state of the server. diff --git a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt index 48a17b30..cf995fc3 100644 --- a/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt +++ b/opendc-compute/opendc-compute-api/src/main/kotlin/org/opendc/compute/api/ServerWatcher.kt @@ -29,9 +29,6 @@ public interface ServerWatcher { /** * This method is invoked when the state of a [Server] changes. * - * Note that the state of [server] might not reflect the state as reported by the invocation, as a call to - * [Server.refresh] is required to update its state. - * * @param server The server whose state has changed. * @param newState The new state of the server. */ diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt index 4a8d3046..45a3d472 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientFlavor.kt @@ -46,12 +46,12 @@ internal class ClientFlavor(private val delegate: Flavor) : Flavor { override var meta: Map<String, Any> = delegate.meta.toMap() private set - override suspend fun delete() { + override fun delete() { delegate.delete() } - override suspend fun refresh() { - delegate.refresh() + override fun reload() { + delegate.reload() name = delegate.name cpuCount = delegate.cpuCount diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt index f0032acf..eb963f0e 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientImage.kt @@ -40,13 +40,13 @@ internal class ClientImage(private val delegate: Image) : Image { override var meta: Map<String, Any> = delegate.meta.toMap() private set - override suspend fun delete() { + override fun delete() { delegate.delete() - refresh() + reload() } - override suspend fun refresh() { - delegate.refresh() + override fun reload() { + delegate.reload() name = delegate.name labels = delegate.labels diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt index 6cd7d30f..f2248466 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ClientServer.kt @@ -59,19 +59,19 @@ internal class ClientServer(private val delegate: Server) : Server, ServerWatche override var launchedAt: Instant? = null private set - override suspend fun start() { + override fun start() { delegate.start() - refresh() + reload() } - override suspend fun stop() { + override fun stop() { delegate.stop() - refresh() + reload() } - override suspend fun delete() { + override fun delete() { delegate.delete() - refresh() + reload() } override fun watch(watcher: ServerWatcher) { @@ -90,8 +90,8 @@ internal class ClientServer(private val delegate: Server) : Server, ServerWatche } } - override suspend fun refresh() { - delegate.refresh() + override fun reload() { + delegate.reload() name = delegate.name flavor = delegate.flavor diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt index 77932545..ffebd5fa 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/ComputeServiceImpl.kt @@ -122,118 +122,7 @@ internal class ComputeServiceImpl( override fun newClient(): ComputeClient { check(!isClosed) { "Service is already closed" } - return object : ComputeClient { - private var isClosed: Boolean = false - - override suspend fun queryFlavors(): List<Flavor> { - check(!isClosed) { "Client is already closed" } - - return flavorById.values.map { ClientFlavor(it) } - } - - override suspend fun findFlavor(id: UUID): Flavor? { - check(!isClosed) { "Client is already closed" } - - return flavorById[id]?.let { ClientFlavor(it) } - } - - override suspend fun newFlavor( - name: String, - cpuCount: Int, - memorySize: Long, - labels: Map<String, String>, - meta: Map<String, Any> - ): Flavor { - check(!isClosed) { "Client is already closed" } - - val uid = UUID(clock.millis(), random.nextLong()) - val flavor = InternalFlavor( - this@ComputeServiceImpl, - uid, - name, - cpuCount, - memorySize, - labels, - meta - ) - - flavorById[uid] = flavor - - return ClientFlavor(flavor) - } - - override suspend fun queryImages(): List<Image> { - check(!isClosed) { "Client is already closed" } - - return imageById.values.map { ClientImage(it) } - } - - override suspend fun findImage(id: UUID): Image? { - check(!isClosed) { "Client is already closed" } - - return imageById[id]?.let { ClientImage(it) } - } - - override suspend fun newImage(name: String, labels: Map<String, String>, meta: Map<String, Any>): Image { - check(!isClosed) { "Client is already closed" } - - val uid = UUID(clock.millis(), random.nextLong()) - val image = InternalImage(this@ComputeServiceImpl, uid, name, labels, meta) - - imageById[uid] = image - - return ClientImage(image) - } - - override suspend fun newServer( - name: String, - image: Image, - flavor: Flavor, - labels: Map<String, String>, - meta: Map<String, Any>, - start: Boolean - ): Server { - check(!isClosed) { "Client is closed" } - - val uid = UUID(clock.millis(), random.nextLong()) - val server = InternalServer( - this@ComputeServiceImpl, - uid, - name, - requireNotNull(flavorById[flavor.uid]) { "Unknown flavor" }, - requireNotNull(imageById[image.uid]) { "Unknown image" }, - labels.toMutableMap(), - meta.toMutableMap() - ) - - serverById[uid] = server - servers.add(server) - - if (start) { - server.start() - } - - return ClientServer(server) - } - - override suspend fun findServer(id: UUID): Server? { - check(!isClosed) { "Client is already closed" } - - return serverById[id]?.let { ClientServer(it) } - } - - override suspend fun queryServers(): List<Server> { - check(!isClosed) { "Client is already closed" } - - return serverById.values.map { ClientServer(it) } - } - - override fun close() { - isClosed = true - } - - override fun toString(): String = "ComputeClient" - } + return Client(this) } override fun addHost(host: Host) { @@ -460,4 +349,120 @@ internal class ComputeServiceImpl( requestSchedulingCycle() } } + + /** + * Implementation of [ComputeClient] using a [ComputeService]. + */ + private class Client(private val service: ComputeServiceImpl) : ComputeClient { + private var isClosed: Boolean = false + + override fun queryFlavors(): List<Flavor> { + check(!isClosed) { "Client is already closed" } + + return service.flavorById.values.map { ClientFlavor(it) } + } + + override fun findFlavor(id: UUID): Flavor? { + check(!isClosed) { "Client is already closed" } + + return service.flavorById[id]?.let { ClientFlavor(it) } + } + + override fun newFlavor( + name: String, + cpuCount: Int, + memorySize: Long, + labels: Map<String, String>, + meta: Map<String, Any> + ): Flavor { + check(!isClosed) { "Client is already closed" } + + val uid = UUID(service.clock.millis(), service.random.nextLong()) + val flavor = InternalFlavor( + service, + uid, + name, + cpuCount, + memorySize, + labels, + meta + ) + + service.flavorById[uid] = flavor + + return ClientFlavor(flavor) + } + + override fun queryImages(): List<Image> { + check(!isClosed) { "Client is already closed" } + + return service.imageById.values.map { ClientImage(it) } + } + + override fun findImage(id: UUID): Image? { + check(!isClosed) { "Client is already closed" } + + return service.imageById[id]?.let { ClientImage(it) } + } + + override fun newImage(name: String, labels: Map<String, String>, meta: Map<String, Any>): Image { + check(!isClosed) { "Client is already closed" } + + val uid = UUID(service.clock.millis(), service.random.nextLong()) + val image = InternalImage(service, uid, name, labels, meta) + + service.imageById[uid] = image + + return ClientImage(image) + } + + override fun newServer( + name: String, + image: Image, + flavor: Flavor, + labels: Map<String, String>, + meta: Map<String, Any>, + start: Boolean + ): Server { + check(!isClosed) { "Client is closed" } + + val uid = UUID(service.clock.millis(), service.random.nextLong()) + val server = InternalServer( + service, + uid, + name, + requireNotNull(service.flavorById[flavor.uid]) { "Unknown flavor" }, + requireNotNull(service.imageById[image.uid]) { "Unknown image" }, + labels.toMutableMap(), + meta.toMutableMap() + ) + + service.serverById[uid] = server + service.servers.add(server) + + if (start) { + server.start() + } + + return ClientServer(server) + } + + override fun findServer(id: UUID): Server? { + check(!isClosed) { "Client is already closed" } + + return service.serverById[id]?.let { ClientServer(it) } + } + + override fun queryServers(): List<Server> { + check(!isClosed) { "Client is already closed" } + + return service.serverById.values.map { ClientServer(it) } + } + + override fun close() { + isClosed = true + } + + override fun toString(): String = "ComputeService.Client" + } } diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt index acd87dfc..077ec865 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalFlavor.kt @@ -50,11 +50,11 @@ internal class InternalFlavor( override val meta: MutableMap<String, Any> = meta.toMutableMap() - override suspend fun refresh() { + override fun reload() { // No-op: this object is the source-of-truth } - override suspend fun delete() { + override fun delete() { service.delete(this) } diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt index a0a35a55..b27ef33a 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalImage.kt @@ -40,11 +40,11 @@ internal class InternalImage( override val meta: MutableMap<String, Any> = meta.toMutableMap() - override suspend fun refresh() { + override fun reload() { // No-op: this object is the source-of-truth } - override suspend fun delete() { + override fun delete() { service.delete(this) } diff --git a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt index e3bae405..c1353ba1 100644 --- a/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt +++ b/opendc-compute/opendc-compute-service/src/main/kotlin/org/opendc/compute/service/internal/InternalServer.kt @@ -67,7 +67,7 @@ internal class InternalServer( */ private var request: ComputeServiceImpl.SchedulingRequest? = null - override suspend fun start() { + override fun start() { when (state) { ServerState.RUNNING -> { logger.debug { "User tried to start server but server is already running" } @@ -90,7 +90,7 @@ internal class InternalServer( } } - override suspend fun stop() { + override fun stop() { when (state) { ServerState.PROVISIONING -> { cancelProvisioningRequest() @@ -104,7 +104,7 @@ internal class InternalServer( } } - override suspend fun delete() { + override fun delete() { when (state) { ServerState.PROVISIONING, ServerState.TERMINATED -> { cancelProvisioningRequest() @@ -129,7 +129,7 @@ internal class InternalServer( watchers -= watcher } - override suspend fun refresh() { + override fun reload() { // No-op: this object is the source-of-truth } diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt index b5685aba..13b926e8 100644 --- a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt +++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt @@ -170,7 +170,7 @@ internal class ComputeServiceTest { server.start() delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.TERMINATED, server.state) } @@ -183,7 +183,7 @@ internal class ComputeServiceTest { server.start() delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.TERMINATED, server.state) } @@ -196,7 +196,7 @@ internal class ComputeServiceTest { server.start() delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.TERMINATED, server.state) } @@ -210,7 +210,7 @@ internal class ComputeServiceTest { server.start() server.stop() delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.TERMINATED, server.state) } @@ -231,7 +231,7 @@ internal class ComputeServiceTest { server.start() delay(10L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.PROVISIONING, server.state) verify { host.canFit(server) } @@ -262,7 +262,7 @@ internal class ComputeServiceTest { listeners.forEach { it.onStateChanged(host, HostState.UP) } delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.PROVISIONING, server.state) verify { host.canFit(server) } @@ -293,7 +293,7 @@ internal class ComputeServiceTest { server.start() delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.PROVISIONING, server.state) verify(exactly = 0) { host.canFit(server) } @@ -351,7 +351,7 @@ internal class ComputeServiceTest { listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.RUNNING) } - server.refresh() + server.reload() assertEquals(ServerState.RUNNING, server.state) verify { watcher.onStateChanged(server, ServerState.RUNNING) } @@ -359,7 +359,7 @@ internal class ComputeServiceTest { // Stop server listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.TERMINATED) } - server.refresh() + server.reload() assertEquals(ServerState.TERMINATED, server.state) verify { watcher.onStateChanged(server, ServerState.TERMINATED) } @@ -387,7 +387,7 @@ internal class ComputeServiceTest { server.start() delay(5L * 60 * 1000) - server.refresh() + server.reload() assertEquals(ServerState.PROVISIONING, server.state) } } diff --git a/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt b/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt index a496cc99..1734daf5 100644 --- a/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt +++ b/opendc-compute/opendc-compute-simulator/src/test/kotlin/org/opendc/compute/simulator/SimHostTest.kt @@ -305,11 +305,11 @@ internal class SimHostTest { override val labels: Map<String, String> = emptyMap() override val meta: Map<String, Any> = emptyMap() - override suspend fun delete() { + override fun delete() { throw NotImplementedError() } - override suspend fun refresh() { + override fun reload() { throw NotImplementedError() } } @@ -320,11 +320,11 @@ internal class SimHostTest { override val labels: Map<String, String>, override val meta: Map<String, Any> ) : Image { - override suspend fun delete() { + override fun delete() { throw NotImplementedError() } - override suspend fun refresh() { + override fun reload() { throw NotImplementedError() } } @@ -343,16 +343,16 @@ internal class SimHostTest { override val launchedAt: Instant? = null - override suspend fun start() {} + override fun start() {} - override suspend fun stop() {} + override fun stop() {} - override suspend fun delete() {} + override fun delete() {} override fun watch(watcher: ServerWatcher) {} override fun unwatch(watcher: ServerWatcher) {} - override suspend fun refresh() {} + override fun reload() {} } } |
