/// <summary> /// Executes the given RPC command locally on the client immediately and returns the result. /// No exception is thrown, but a <see cref="RpcFailure"/> result is set in case of a failure. /// </summary> public async Task <RpcCommandResult> ExecuteLocallyNow(RpcCommand command) { // Do not run the same command twice. If the command with this ID was already // executed, return the cached result. If the cache is not available any more, return a // obsolete function call failure. if (serverCache.GetCachedResult(command.ID) is RpcCommandResult result) { return(result); } // Execute the command try { var runner = new RpcCommandRunner(clientMethods(), null); result = await runner.Execute(clientConfig.ClientID, command); } catch (Exception ex) { result = RpcCommandResult.FromFailure(command.ID, new RpcFailure(RpcFailureType.RemoteException, ex.Message)); } // Cache result, if there was no network problem if (false == (result.Failure?.IsNetworkProblem == true)) { serverCache.CacheResult(result); } return(result); }
/// <summary> /// Call this method when the client called the "/rpc/push"-endpoint. /// It executes the given RPC command immediately and returns the result. /// No exception is thrown, but a <see cref="RpcFailure"/> result is set in case of a failure. /// </summary> public async Task <RpcCommandResult> OnClientPush(string clientID, RpcCommand command, RpcCommandRunner runner) { // Do not run the same command twice. If the command with this ID was already // executed, return the cached result. If the cache is not available any more, return a // obsolete function call failure. var client = clients.GetClient(clientID, commandBacklog); if (client.GetCachedResult(command.ID) is RpcCommandResult result) { return(result); } // Execute the command try { result = await runner.Execute(clientID, command); } catch (Exception ex) { result = RpcCommandResult.FromFailure(command.ID, new RpcFailure(RpcFailureType.RemoteException, ex.Message)); } // Cache result, if there was no network problem if (false == (result.Failure?.IsNetworkProblem == true)) { client.CacheResult(result); } return(result); }
/// <summary> /// Caches the given command result, so that repeated calls of the same /// command ID can be answered without executing the command again. /// </summary> public void CacheResult(RpcCommandResult result) { lastCachedResultCommandID = Math.Max(lastCachedResultCommandID, result.CommandID); cachedResults.Enqueue(result); while (cachedResults.Count > maxQueueSize) { cachedResults.TryDequeue(out _); } }
/// <summary> /// Runs the given command on the client ID. /// </summary> public async Task <RpcCommandResult> Execute(string clientID, RpcCommand command) { foreach (var rpc in rpcFunctions) { rpc.Context = new RpcContext(clientID, serviceScopeFactory); if (rpc.Execute(command) is Task <string?> task) { // Method found in this class return(RpcCommandResult.FromSuccess(command.ID, await task)); } } // Called method is not implemented in any registered class throw new Exception("Unknown method name: " + command.MethodName); }
/// <summary> /// Gets the cached result of the command with the given ID, if it was executed already, /// or null, if it is a new command which has to be executed now. /// If the command was already executed, but is too old so that there is no cached result /// any more, a failure result with <see cref="RpcFailureType.ObsoleteCommandID"/> is returned. /// </summary> public RpcCommandResult?GetCachedResult(ulong commandID) { // New command? if (commandID > lastCachedResultCommandID) { return(null); } // It is an old command. Find the cached result. var cachedResults = this.cachedResults.ToList(); // Defensive copy var result = cachedResults.Find(it => it.CommandID == commandID); return(result ?? RpcCommandResult.FromFailure(commandID, new RpcFailure( RpcFailureType.ObsoleteCommandID, $"Command ID {commandID} already executed too long ago " + (ClientID != null ? $"on the client {ClientID}" : "for the server")))); }
/// <summary> /// Sends the given command to the server now, reads the result or catches the /// exception, and sets the command's state accordingly. /// </summary> private async Task ExecuteOnServerNow(RpcCommand command) { RpcCommandResult result; var bodyJson = JsonLib.ToJson(command); try { var httpResponse = await httpPush.PostAsync(clientConfig.ServerUrl + "/push", new StringContent(bodyJson, Encoding.UTF8, "application/json")); if (httpResponse.IsSuccessStatusCode) { // Response (either success or remote failure) received. result = JsonLib.FromJson <RpcCommandResult>(await httpResponse.Content.ReadAsStringAsync()); } else { // The server did not respond with 200 (which it should do even in case of // a remote exception). So there is a communication error. result = RpcCommandResult.FromFailure(command.ID, new RpcFailure(RpcFailureType.RpcError, "Remote side problem with RPC call. HTTP status code " + (int)httpResponse.StatusCode)); } } catch { // Could not reach server. result = RpcCommandResult.FromFailure(command.ID, new RpcFailure(RpcFailureType.Timeout, "Could not reach the server")); } // When a result was received (i.e. when there was no network problem), the command is finished if (false == (result.Failure?.IsNetworkProblem == true) && command.ID == serverCache.CurrentCommand?.ID) { serverCache.FinishCurrentCommand(); } // Finish command command.Finish(result); }