StackExchange.Redis ConnectionMultiplexer - handling disconnects - c#

What is the correct way to handle socket failure in a ConnectionMultiplexer? I know it will try again silently in the background, but is there any accepted canonical way to handle the time between such disconnects? Since I wrap this up in our own client anyway, I was thinking something like the following:
private async Task<IDatabase> GetDb(int dbToGet)
{
int numberOfRetries = 0;
while (!multiplexer.IsConnected && numberOfRetries < MAX_RETRIES)
{
await Task.Delay(20);
numberOfRetries++;
}
if (!multiplexer.IsConnected)
{
// Panic, die, etc.
}
// Continue as though connected here
}
It seems a bit clumsy, though, so I'm wondering if there's a better way to handle this.
(This is all in version 1.0.414 of StackExchange.Redis, the latest version from NuGet)

I just wrapped multiplexer,
by default it has auto reconnect definition,
the real problem is that you have subscribe/Psubscribe to Redis with current socket connection,
therefore I used the ConnectionRestored Event to re-Register the subscribe patterns to the relevant channels/actions.
Class Example:
public class RedisInstanceManager
{
public RedisInstanceCredentials m_redisInstanceCredentials { get; set; }
public DateTime? m_lastUpdatedDate { get; set; }
public ConnectionMultiplexer redisClientsFactory { get; set; }
public Timer _ConnectedTimer;
public Action _reconnectAction;
public RedisInstanceManager(ConnectionMultiplexer redisClients, Action _reconnectActionIncoming)
{
string host,port;
string[] splitArray = redisClients.Configuration.Split(':');
host = splitArray[0];
port = splitArray[1];
this.redisClientsFactory = redisClients;
this.m_redisInstanceCredentials = new RedisInstanceCredentials(host, port);
this.m_lastUpdatedDate = null;
_ConnectedTimer = new Timer(connectedTimer, null, 1500, 1500);
_reconnectAction = _reconnectActionIncoming;
this.redisClientsFactory.ConnectionRestored += redisClientsFactory_ConnectionRestored;
}
private void connectedTimer(object o)
{
_ConnectedTimer.Change(Timeout.Infinite, Timeout.Infinite);
if (!this.redisClientsFactory.IsConnected)
{
Console.WriteLine("redis dissconnected");
}
_ConnectedTimer.Change(1500,1500);
}
void redisClientsFactory_ConnectionRestored(object sender, ConnectionFailedEventArgs e)
{
Console.WriteLine("Redis Connected again");
if (_reconnectAction != null)
_reconnectAction();
}
public ConnectionMultiplexer GetClient()
{
return this.redisClientsFactory;
}
}

Related

What is the equivalent of Kotlin WebSocketSession object in c#?

For a few months now I have been using the ktor framework to create servers that expose rest calls and communication via webSockets. For now I have always used clients using kotlin as a programming language (or Android App, or App Desktop).
Specifically, I had a class that was injected with the HttpClient object (from the documentation = Asynchronous client to perform HTTP requests).
Within this class I have 4 methods:
start the session: instantiate the WebSocketSession object (Represents a web socket session between two peers)
send Frame
receives Frame
close the session
In Ktor my class is something that looks a lot like this:
class WebSocketServiceImpl(
private val client: HttpClient
){
private var socket: WebSocketSession? = null
//1)
suspend fun initSession(username: String): Resource<Unit>{
socket = client.webSocketSession {
url("ws://xxx.xxx.xxx.xxx:xxxx/myRoute?username=$username")
}
//2)
suspend fun send(myObj: MyObj) {
try {
val myObjSerialized = Json.encodeToString(myObj)
socket?.send(Frame.Text(myObjSerialized ))
} catch (e: Exception) {
e.printStackTrace()
}
}
//3)
fun observePrintableMessages(): Flow<MyObj> {
return try {
socket?.incoming
?.receiveAsFlow()
?.filter { it is Frame.Text }
?.map {
val myObjString = (it as? Frame.Text)?.readText() ?: ""
val printableMessageDto = Json.decodeFromString<MyObj>(myObjString)
} ?: flow { }
} catch (e: Exception) {
e.printStackTrace()
flow { }
}
}
//4)
suspend fun closeSession() {
socket?.close()
}
}
From the C # documentation instead, I found these examples on how to use Client-side WebSockets:
//1)
const exampleSocket = new WebSocket("wss://www.example.com/socketserver", "protocolOne");
//2)
exampleSocket.send("Here's some text that the server is urgently awaiting!");
//3)
exampleSocket.onmessage = (event) => {
console.log(event.data);
}
//4)
exampleSocket.close();
Admitted and not granted that the methods I found in C # really work, to make the WebSocket object used in C # be equivalent to the WebSocketSession object in Kotlin is enough for me to do so? :
public void initSession(string username)
{
exampleSocket = new WebSocket($"wss://www.example.com/socketserver?username={username}", "");
}
Or is it some other type of object to use?
If for any reason you don't know the answer, you don't need to vote negative, you can just move on.
I used the Websocket.Client library (by Mariusz Kotas) found on NuGet
public class WebSocketService : IWebSocketService
{
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
private void FireMessageReceivedEvent(Message message) => MessageReceived?.Invoke(this, new MessageReceivedEventArgs(message));
public string Url { get => "ws://192.168.1.202:8082/chat-socket"; }
private WebsocketClient webSocketClient;
public async Task<SessionResoult> InitSession(string username)
{
string usernameSession = $"?username={username}";
string urlWithUsername = $"{Url}{usernameSession}";
try
{
webSocketClient = new WebsocketClient(new Uri(urlWithUsername));
await webSocketClient.Start();
if (webSocketClient.IsRunning)
{
SubscribeNewMessages();
return new SessionResoult.Success();
}
else
{
return new SessionResoult.Error("webSocketClient is not running");
}
}
catch(Exception ex)
{
return new SessionResoult.Error(ex.Message);
}
}
private void SubscribeNewMessages()
{
webSocketClient.MessageReceived.Subscribe(m =>
{
MessageDto message = JsonConvert.DeserializeObject<MessageDto>(m.Text);
FireMessageReceivedEvent(message.ToMessage());
});
}
public async Task SendMessageAsync(string message)
{
await Task.Run(() => webSocketClient.Send(message));
}
public void CloseSession()
{
webSocketClient.Dispose();
}
}
In the code, the interesting parts are:
1) initialization of the WebsocketClient object
2) the subscription of receiving messages ( Start() method immediately after initialization)
3) observation of message subscription -> webSocketClient.MessageReceived.Subscribe
4) the 'Fire' of the event linked to the observation of messages -> FireMessageReceivedEvent
5) those who use the class must subscribe to the event of the latter ->
webSocketService.MessageReceived + = (sender, e) => {OnMessageReceived (e.MessageReceived); };
MessageReceivedEventArgs -> Class describing the Arguments of the event
SessionResoult -> Class similar to an Enum but with the possibility of passing a string or not based on which subclass it is

How to make a "fire & forget" async FIFO queue in c#?

I'm trying to process documents asynchronously. The idea is that the user sends documents to a service, which takes time, and will look at the results later (about 20-90 seconds per document).
Ideally, I would like to just fill some kind of observable collection that would be emptied by the system as fast as it can. When there is an item, process it and produce the expected output in another object, and when there is no item just do nothing. When the user checks the output collection, he will find the items that are already processed.
Ideally all items would be visible from the start and would have a state (completed, ongoing or in queue), but once I know how to do the first, I should be able to handle the states.
I'm not sure which object to use for that, right now I'm looking at BlockingCollection but I don't think it's suited for the job, as I can't fill it while it's being emptied from the other end.
private BlockingCollection<IDocument> _jobs = new BlockingCollection<IDocument>();
public ObservableCollection<IExtractedDocument> ExtractedDocuments { get; }
public QueueService()
{
ExtractedDocuments = new ObservableCollection<IExtractedDocument>();
}
public async Task Add(string filePath, List<Extra> extras)
{
if (_jobs.IsAddingCompleted || _jobs.IsCompleted)
_jobs = new BlockingCollection<IDocument>();
var doc = new Document(filePath, extras);
_jobs.Add(doc);
_jobs.CompleteAdding();
await ProcessQueue();
}
private async Task ProcessQueue()
{
foreach (var document in _jobs.GetConsumingEnumerable(CancellationToken.None))
{
var resultDocument = await service.ProcessDocument(document);
ExtractedDocuments.Add(resultDocument );
Debug.WriteLine("Job completed");
}
}
This is how I'm handling it right now. If I remove the CompleteAdding call, it hangs on the second attempt. If I have that statement, then I can't just fill the queue, I have to empty it first which defeats the purpose.
Is there a way of having what I'm trying to achieve? A collection that I would fill and the system would process asynchronously and autonomously?
To summarize, I need :
A collection that I can fill, that would be processed gradually and asynchronously. A document or series or document can be added while some are being processed.
An ouput collection that would be filled after the process is complete
The UI thread and app to still be responsive while everything is running
I don't need to have multiple processes in parallel, or one document at a time. Whichever is easiest to put in place and maintain will do (small scale application). I'm assuming one at a time is simpler.
A common pattern here is to have a callback method that executes upon a document state change. With a background task running, it will chew threw documents as fast as it can. Call Dispose to shutdown the processor.
If you need to process the callback on a gui thread, you'll need to synchornize the callback to your main thread some how. Windows forms has methods to do this if that's what you are using.
This example program implements all the necessary classes and interfaces, and you can fine tune and tweak things as you need.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
private static Task Callback(IExtractedDocument doc, DocumentProcessor.DocState docState)
{
Console.WriteLine("Processing doc {0}, state: {1}", doc, docState);
return Task.CompletedTask;
}
public static void Main()
{
using DocumentProcessor docProcessor = new DocumentProcessor(Callback);
Console.WriteLine("Processor started, press any key to end processing");
for (int i = 0; i < 100; i++)
{
if (Console.KeyAvailable)
{
break;
}
else if (i == 5)
{
// make an error
docProcessor.Add(null);
}
else
{
docProcessor.Add(new Document { Text = "Test text " + Guid.NewGuid().ToString() });
}
Thread.Sleep(500);
}
Console.WriteLine("Doc processor shut down, press ENTER to quit");
Console.ReadLine();
}
public interface IDocument
{
public string Text { get; }
}
public class Document : IDocument
{
public string Text { get; set; }
}
public interface IExtractedDocument : IDocument
{
public IDocument OriginalDocument { get; }
public Exception Error { get; }
}
public class ExtractedDocument : IExtractedDocument
{
public override string ToString()
{
return $"Orig text: {OriginalDocument?.Text}, Extracted Text: {Text}, Error: {Error}";
}
public IDocument OriginalDocument { get; set; }
public string Text { get; set; }
public Exception Error { get; set; }
}
public class DocumentProcessor : IDisposable
{
public enum DocState { Processing, Completed, Error }
private readonly BlockingCollection<IDocument> queue = new BlockingCollection<IDocument>();
private readonly Func<IExtractedDocument, DocState, Task> callback;
private CancellationTokenSource cancelToken = new CancellationTokenSource();
public DocumentProcessor(Func<IExtractedDocument, DocState, Task> callback)
{
this.callback = callback;
Task.Run(() => StartQueueProcessor()).GetAwaiter();
}
public void Dispose()
{
if (!cancelToken.IsCancellationRequested)
{
cancelToken.Cancel();
}
}
public void Add(IDocument doc)
{
if (cancelToken.IsCancellationRequested)
{
throw new InvalidOperationException("Processor is disposed");
}
queue.Add(doc);
}
private void ProcessDocument(IDocument doc)
{
try
{
// do processing
DoCallback(new ExtractedDocument { OriginalDocument = doc }, DocState.Processing);
if (doc is null)
{
throw new ArgumentNullException("Document to process was null");
}
IExtractedDocument successExtractedDocument = DoSomeDocumentProcessing(doc);
DoCallback(successExtractedDocument, DocState.Completed);
}
catch (Exception ex)
{
DoCallback(new ExtractedDocument { OriginalDocument = doc, Error = ex }, DocState.Error);
}
}
private IExtractedDocument DoSomeDocumentProcessing(IDocument originalDocument)
{
return new ExtractedDocument { OriginalDocument = originalDocument, Text = "Extracted: " + originalDocument.Text };
}
private void DoCallback(IExtractedDocument result, DocState docState)
{
if (callback != null)
{
// send callbacks in background
callback(result, docState).GetAwaiter();
}
}
private void StartQueueProcessor()
{
try
{
while (!cancelToken.Token.IsCancellationRequested)
{
if (queue.TryTake(out IDocument doc, 1000, cancelToken.Token))
{
// can chance to Task.Run(() => ProcessDocument(doc)).GetAwaiter() for parallel execution
ProcessDocument(doc);
}
}
}
catch (OperationCanceledException)
{
// ignore, don't need to throw or worry about this
}
while (queue.TryTake(out IDocument doc))
{
DoCallback(new ExtractedDocument { Error = new ObjectDisposedException("Processor was disposed") }, DocState.Error);
}
}
}
}
}

Rx - Reactive extensions - conditional switch from first Observable to second

I have 2 data sources: online and offline (cached). Both of them returns IObservable of object which contains 2 flags - IsSuccess and IsCached. I would like to get data from online source but only when IsSuccess=true. If this fail I would like to get data from offline source. Additionally I want to save new data in cache for future. I am not sure how to do it best in RX.
Here is my implementation of that but I think it can be done much better
public IObservable<Result<SampleModel>> GetSampleModel()
{
IObservable<Result<SampleModel>> onlineObservable = _onlineSource.GetData<SampleModel>();
IObservable<Result<SampleModel>> offlineObservable = _offlineSource.GetData<SampleModel>();
var subject = new Subject<Result<SampleModel>>();
onlineObservable.Do(async (result) =>
{
if (result.IsSuccess)
{
await _offlineSource.CacheData(result.Data).ConfigureAwait(false);
}
}).Subscribe((result) =>
{
if (result.IsSuccess)
{
subject.OnNext(result);
}
subject.OnCompleted();
});
return subject.Concat(offlineObservable).Take(1);
}
Result class - wrapper for data:
public class Result<T>
{
public Result(Exception exception)
{
Exception = exception;
}
public Result(T data, bool isCached = false)
{
IsCached = isCached;
IsSuccess = true;
Data = data;
}
public bool IsSuccess { get; private set; }
public bool IsCached { get; private set; }
public T Data { get; private set; }
public Exception Exception { get; private set; }
}
Your implementation will not work reliably, because there is a race condition in there. Consider this:
var temp = GetSampleModel(); // #1
// Do some long operation here
temp.Subscribe(p => Console.WriteLine(p)); // #2
In this case, fetching data will start in #1, and if the data is received and pushed to subject before #2 executes, nothing will be printed no matter how long you wait.
Usually, you should avoid subscribing inside a function returning IObservable to avoid such issues. Using Do is also a bad smell. You could fix the code using ReplaySubject or AsyncSubject, but in such cases I generally prefer Observable.Create. Here is my rewrite:
public IObservable<SampleModel> GetSampleModel(IScheduler scheduler = null)
{
scheduler = scheduler ?? TaskPoolScheduler.Default;
return Observable.Create<SampleModel>(observer =>
{
return scheduler.ScheduleAsync(async (s, ct) =>
{
var onlineResult = await _onlineSource.GetData<SampleModel>().FirstAsync();
if (onlineResult.IsSuccess)
{
observer.OnNext(onlineResult.Data);
await _offlineSource.CacheData(onlineResult.Data);
observer.OnCompleted();
}
else
{
var offlineResult = await _offlineSource.GetData<SampleModel>().FirstAsync();
if (offlineResult.IsSuccess)
{
observer.OnNext(offlineResult.Data);
observer.OnCompleted();
}
else
{
observer.OnError(new Exception("Could not receive model"));
}
}
return Disposable.Empty;
});
});
}
You can see that it still isn't terribly pretty. I think that it's because you chose not to use natural Rx system of handling errors, but instead to wrap your values in Result type. If you alter your repository methods to handle errors in Rx way, resulting code is much more concise. (Note that I changed your Result type to MaybeCached, and I assume that now both sources return IObservable<SampleModel>, which is a cold observable either returning a single result or an error):
public class MaybeCached<T>
{
public MaybeCached(T data, bool isCached)
{
IsCached = isCached;
IsSuccess = true;
}
public bool IsCached { get; private set; }
public T Data { get; private set; }
}
public IObservable<SampleModel> GetSampleModel()
{
_onlineSource
.GetData<SampleModel>()
.Select(d => new MaybeCached(d, false))
.Catch(_offlineSource
.GetData<SampleModel>()
.Select(d => new MaybeCached(d, true))
.SelectMany(data => data.IsCached ? Observable.Return(data.Data) : _offlineSource.CacheData(data.Data).Select(_ => data.Data));
}
Catch is used here in order to obtain a conditional switch you asked for.

How can I improve and/or modularize my handling of event based tasks?

So I have a server and I'm making calls to it through a wrapped up WebSocket (WebSocket4Net) and one of the requirements of the library I'm building is the ability to await on the return of the request. So I have a class MessageEventHandler that contains events that are triggered by the class MessageHandler as messages come in.
MessageEventHandler ex.
public class MessageEventHandler : IMessageEventHandler
{
public delegate void NodeNameReceived(string name);
public event Interfaces.NodeNameReceived OnNodeNameReceived;
public void NodeNameReceive(string name)
{
if (this.OnNodeNameReceived != null)
{
this.OnNodeNameReceived(name);
}
}
}
MessageHandler ex.
public class MessageHandler : IMessageHandler
{
private IMessageEventHandler eventHandler;
public MessageHandler(IMessageEventHandler eventHandler)
{
this.eventHandler = eventHandler;
}
public void ProcessDataCollectorMessage(string message)
{
var serviceMessage = JsonConvert.DeserializeObject<ServiceMessage>(message);
switch (message.MessageType)
{
case MessageType.GetNodeName:
{
var nodeName = serviceMessage.Data as string;
if (nodeName != null)
{
this.eventHandler.NodeNameReceive(nodeName);
}
break;
}
default:
{
throw new NotImplementedException();
}
}
}
Now building upon those classes I have the class containing my asynchronous function that handles the call to get the node name.
public class ClientServiceInterface : IClientServiceInterface
{
public delegate void RequestReady(ServiceMessage serviceMessage);
public event Interfaces.RequestReady OnRequestReady;
public int ResponseTimeout { get; private set; }
private IMessageEventHandler messageEventHandler;
public ClientServiceInterface(IMessageEventHandler messageEventHandler, int responseTimeout = 5000)
{
this.messageEventHandler = messageEventHandler;
this.ResponseTimeout = responseTimeout;
}
public Task<string> GetNodeNameAsync()
{
var taskCompletionSource = new TaskCompletionSource<string>();
var setHandler = default(NodeNameReceived);
setHandler = name =>
{
taskCompletionSource.SetResult(name);
this.messageEventHandler.OnNodeNameReceived -= setHandler;
};
this.messageEventHandler.OnNodeNameReceived += setHandler;
var ct = new CancellationTokenSource(this.ResponseTimeout);
var registration = new CancellationTokenRegistration();
registration = ct.Token.Register(
() =>
{
taskCompletionSource.TrySetCanceled();
this.messageEventHandler.OnNodeNameReceived -= setHandler;
registration.Dispose();
},
false);
var serviceMessage = new ServiceMessage() { Type = MessageType.GetNodeName };
this.ReadyMessage(serviceMessage);
return taskCompletionSource.Task;
}
}
As you can see I wouldn't call it pretty and I apologize if anyone threw up a little reading it. But this is my first attempt at wrapping a Task with Asynchronous Event. So with that on the table I could use some help.
Is there a better way to accomplish what I'm trying to achieve here? Remembering that I want a user of the library to either subscribe to the event and listen for all callbacks OR they can simply await the return depending on
their needs.
var nodeName = await GetNodeNameAsync();
Console.WriteLine(nodeName);
or
messageEventHandler.OnNodeNameReceived += (name) => Console.WriteLine(name);
GetNodeNameAsync();
Alternatively if my approach is actually 'good' can anyone provide any advice as to how I can write a helper function to abstract out setting up each function in this way? Any help would be greatly appreciated.
So I've written a couple classes to solve the problem I was having. The first of which is my CallbackHandle class which contains the task inside the TaskCompletionSource so each time that a request is made in my example a new callback handle is created.
public class CallbackHandle<T>
{
public CallbackHandle(int timeout)
{
this.TaskCompletionSource = new TaskCompletionSource<T>();
var cts = new CancellationTokenSource(timeout);
cts.Token.Register(
() =>
{
if (this.Cancelled != null)
{
this.Cancelled();
}
});
this.CancellationToken = cts;
}
public event Action Cancelled;
public CancellationTokenSource CancellationToken { get; private set; }
public TaskCompletionSource<T> TaskCompletionSource { get; private set; }
}
Then I have a 'handler' that manages the handles and their creation.
public class CallbackHandler<T>
{
private readonly IList<CallbackHandle<T>> callbackHandles;
private readonly object locker = new object();
public CallbackHandler()
{
this.callbackHandles = new List<CallbackHandle<T>>();
}
public CallbackHandle<T> AddCallback(int timeout)
{
var callback = new CallbackHandle<T>(timeout);
callback.Cancelled += () =>
{
this.callbackHandles.Remove(callback);
callback.TaskCompletionSource.TrySetResult("Error");
};
lock (this.locker)
{
this.callbackHandles.Add(callback);
}
return callback;
}
public void EventTriggered(T eventArgs)
{
lock (this.locker)
{
if (this.callbackHandles.Count > 0)
{
CallbackHandle<T> callback =
this.callbackHandles.First();
if (callback != null)
{
this.callbackHandles.Remove(callback);
callback.TaskCompletionSource.SetResult(eventArgs);
}
}
}
}
}
This is a simplified version of my actual implementation but it should get someone started if they need something similar. So to use this on my ClientServiceInterface class in my example I would start by creating a class level handler and using it like this:
public class ClientServiceInterface : IClientServiceInterface
{
private readonly CallbackHandler<string> getNodeNameHandler;
public ClientServiceInterface(IMessageEventHandler messageEventHandler, int responseTimeout = 5000)
{
this.messageEventHandler = messageEventHandler;
this.ResponseTimeout = responseTimeout;
this.getNodeNameHandler = new
CallbackHandler<string>();
this.messageEventHandler.OnNodeNameReceived += args => this.getNodeNameHandler.EventTriggered(args);
}
public Task<string> GetNodeNameAsync()
{
CallbackHandle<string> callbackHandle = this.getNodeNameHandler.AddCallback(this.ResponseTimeout);
var serviceMessage = new ServiceMessage
{
Type = MessageType.GetNodeName.ToString()
};
this.ReadyMessage(serviceMessage);
return callbackHandle.TaskCompletionSource.Task;
}
// Rest of class declaration removed for brevity
}
Which is much better looking than what I had before (at least in my opinion) and it's easy to extend.
For starters follow a thread-safe pattern:
public void NodeNameReceive(string name)
{
var evt = this.OnNodeNameReceived;
if (evt != null)
{
evt (name);
}
}
If you do not take a reference to the event object it can be set to null between the time you check null and call the method.

Return data from a call to a client

I have a system connected to a digital scale and I'm trying to use SignalR to pass the weight into other requesting clients.
My Hub looks like this:
public class ScaleHub : Hub
{
private static string ScaleClientId { get; set; }
// the weight object for the client making the request
private static Dictionary<string, WeightDTO> scaleWeights =
new Dictionary<string, WeightDTO>();
public void RegisterScale()
{
ScaleClientId = Context.ConnectionId;
}
public WeightDTO GetWeight()
{
// clear the scale weight for the client making the request
scaleWeights[Context.ConnectionId] = null;
Task updateWeightTask = Clients[ScaleClientId].UpdateWeight(Context.ConnectionId);
// this doesn't wait :-(
updateWeightTask.Wait();
return scaleWeights[Context.ConnectionId];
}
public void UpdateWeight(WeightDTO weight, string clientId)
{
// update the weight for the client making the request
scaleWeights[clientId] = weight;
}
}
The important parts of the client are:
scaleHub.On<string>("UpdateWeight", UpdateWeight);
private void UpdateWeight(string clientId)
{
// this is replaced with code that talks to the scale hardware
var newWeight = new WeightDTO(123, WeightUnitTypes.LB);
scaleHub.Invoke("UpdateWeight", newWeight, clientId).Wait();
}
public Task<WeightDTO> GetWeight()
{
return scaleHub.Invoke<WeightDTO>("GetWeight");
}
I'm still new to SignalR so I'm not sure I'm going about this the right way.
Adding Thread.Sleep(2000) instead of updateWeightTask.Wait() solves the issues, since it gives enough time for the round-trip calls to UpdateWeight to finish. I'd prefer not to make my clients wait 2 seconds to get the weight.
Any suggestions?

Categories

Resources