-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathInMemoryRpcClient.kt
More file actions
145 lines (133 loc) · 5.42 KB
/
Copy pathInMemoryRpcClient.kt
File metadata and controls
145 lines (133 loc) · 5.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/**
* SPDX-FileCopyrightText: 2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.eclipse.uprotocol.communication
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.eclipse.uprotocol.transport.*
import org.eclipse.uprotocol.uri.factory.UUriFactory
import org.eclipse.uprotocol.v1.*
import org.eclipse.uprotocol.v1.UUID
/**
* The following is an example implementation of the [RpcClient] interface that
* wraps the [UTransport] for implementing the RPC pattern to send
* RPC requests and receive RPC responses. This implementation uses an in-memory
* map to store the futures that needs to be completed when the response comes in from the server.
*
* *NOTE:* Developers are not required to use these APIs, they can implement their own
* or directly use the [UTransport] to send RPC requests and register listeners that
* handle the RPC responses.
*
* @param transport the transport to use for sending the RPC requests
*/
class InMemoryRpcClient(
private val transport: UTransport,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) : RpcClient {
// Map to store the futures that needs to be completed when the response comes in
private val mRequests = HashMap<UUID, CompletableDeferred<UMessage>>()
// Generic listener to handle all RPC response messages
private val mResponseHandler = UListener { response: UMessage ->
this.handleResponses(response)
}
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
private val mutex = Mutex()
init {
scope.launch {
transport.registerListener(
UUriFactory.ANY,
transport.getSource(), mResponseHandler
)
}
}
/**
* Invoke a method (send an RPC request) and receive the response
* the returned [UPayload] wrapped in [Result].
*
* @param methodUri The method URI to be invoked.
* @param requestPayload The request message to be sent to the server.
* @param options RPC method invocation call options, see [CallOptions]
* @return Returns the [Result] with the response [UPayload] or exception with the failure
* reason as [UStatus].
*/
override suspend fun invokeMethod(
methodUri: UUri,
requestPayload: UPayload,
options: CallOptions
): Result<UPayload> {
try {
val request = uMessage {
forRequest(transport.getSource(), methodUri, options.timeout)
if (options.token.isNotBlank()) {
setToken(options.token)
}
setPayload(requestPayload)
}
transport.send(request).takeIf { it.code != UCode.OK }?.let {
throw UStatusException(it)
}
val result = withTimeout(request.attributes.ttl.toLong()) {
mutex.withLock {
val currentRequest = mRequests[request.attributes.id]
if (currentRequest != null) {
throw UStatusException(UCode.ALREADY_EXISTS, "Duplicated request found")
}
val response = CompletableDeferred<UMessage>()
mRequests[request.attributes.id] = response
response
}.await()
}
return Result.success(UPayload.pack(result.payload, result.attributes.payloadFormat))
} catch (e: Exception) {
return when (e) {
is UStatusException -> {
Result.failure(e)
}
is TimeoutCancellationException -> {
Result.failure(UStatusException(UCode.DEADLINE_EXCEEDED, "Request timed out"))
}
else -> {
Result.failure(UStatusException(UCode.UNKNOWN, e.message))
}
}
}
}
/**
* Close the RPC client and clean up any resources
*/
fun close() {
mRequests.clear()
scope.launch {
transport.unregisterListener(UUriFactory.ANY, transport.getSource(), mResponseHandler)
}
}
/**
* Handle the responses coming back from the server
* @param response The response message from the server
*/
private suspend fun handleResponses(response: UMessage) {
// Only handle responses messages, ignore all other messages like notifications
if (response.attributes.type != UMessageType.UMESSAGE_TYPE_RESPONSE) {
return
}
// Check if the response is for a request we made, if not then ignore it
val responseDeferred = mutex.withLock { mRequests.remove(response.attributes.reqid) } ?: return
// Check if the response has a commstatus and if it is not OK then complete the future with an exception
if (response.attributes.hasCommstatus()) {
val code = response.attributes.commstatus
responseDeferred.completeExceptionally(UStatusException(code, "Communication error [$code]"))
} else {
responseDeferred.complete(response)
}
}
}