I need to retrieve multiple objects from an external system. The external system supports multiple simultaneous requests (i.e. threads), but it is possible to flood the external system - therefore I want to be able to retrieve multiple objects asynchronously, but I want to be able to throttle the number of simultaneous async requests. i.e. I need to retrieve 100 items, but don't want to be retrieving more than 25 of them at once. When each request of the 25 completes, I want to trigger another retrieval, and once they are all complete I want to return all of the results in the order they were requested (i.e. there is no point returning the results until the entire call is returned). Are there any recommended patterns for this sort of thing?
Would something like this be appropriate (pseudocode, obviously)?
private List<externalSystemObjects> returnedObjects = new List<externalSystemObjects>;
public List<externalSystemObjects> GetObjects(List<string> ids)
{
int callCount = 0;
int maxCallCount = 25;
WaitHandle[] handles;
foreach(id in itemIds to get)
{
if(callCount < maxCallCount)
{
WaitHandle handle = executeCall(id, callback);
addWaitHandleToWaitArray(handle)
}
else
{
int returnedCallId = WaitHandle.WaitAny(handles);
removeReturnedCallFromWaitHandles(handles);
}
}
WaitHandle.WaitAll(handles);
return returnedObjects;
}
public void callback(object result)
{
returnedObjects.Add(result);
}
Consider the list of items to process as a queue from which 25 processing threads dequeue tasks, process a task, add the result then repeat until the queue is empty:
class Program
{
class State
{
public EventWaitHandle Done;
public int runningThreads;
public List<string> itemsToProcess;
public List<string> itemsResponses;
}
static void Main(string[] args)
{
State state = new State();
state.itemsResponses = new List<string>(1000);
state.itemsToProcess = new List<string>(1000);
for (int i = 0; i < 1000; ++i)
{
state.itemsToProcess.Add(String.Format("Request {0}", i));
}
state.runningThreads = 25;
state.Done = new AutoResetEvent(false);
for (int i = 0; i < 25; ++i)
{
Thread t =new Thread(new ParameterizedThreadStart(Processing));
t.Start(state);
}
state.Done.WaitOne();
foreach (string s in state.itemsResponses)
{
Console.WriteLine("{0}", s);
}
}
private static void Processing(object param)
{
Debug.Assert(param is State);
State state = param as State;
try
{
do
{
string item = null;
lock (state.itemsToProcess)
{
if (state.itemsToProcess.Count > 0)
{
item = state.itemsToProcess[0];
state.itemsToProcess.RemoveAt(0);
}
}
if (null == item)
{
break;
}
// Simulate some processing
Thread.Sleep(10);
string response = String.Format("Response for {0} on thread: {1}", item, Thread.CurrentThread.ManagedThreadId);
lock (state.itemsResponses)
{
state.itemsResponses.Add(response);
}
} while (true);
}
catch (Exception)
{
// ...
}
finally
{
int threadsLeft = Interlocked.Decrement(ref state.runningThreads);
if (0 == threadsLeft)
{
state.Done.Set();
}
}
}
}
You can do the same using asynchronous callbacks, there is no need to use threads.
Having some queue-like structure to hold the pending requests is a pretty common pattern. In Web apps where there may be several layers of processing you see a "funnel" style approach with the early parts of the processing change having larger queues. There may also be some kind of prioritisation applied to queues, higher priority requests being shuffled to the top of the queue.
One important thing to consider in your solution is that if request arrival rate is higher than your processing rate (this might be due to a Denial of Service attack, or just that some part of the processing is unusually slow today) then your queues will increase without bound. You need to have some policy such as to refuse new requests immediately when the queue depth exceeds some value.
Related
To ensure thread-safety, I'm trying to find a generic cross-platform approach to
execute all delegates asynchronously in the main thread or ...
execute delegete in a background thread and pass result to the main one
Considering that console apps do not have synchronization context, I create new context when app is loading and then use one of the following methods.
Set and restore custom SC as described in Await, SynchronizationContext, and Console Apps article by Stephen Toub
Marshall all delegates to main thread using context.Post call as described in the article ExecutionContext vs SynchronizationContext by Stephen Toub
Using background thread with producer-consumer collection as described in Basic synchronization by Joe Albahari
Question
Ideas #1 and #2 set context correctly only if it's done synchronously. If they're called from inside Parallel.For(0, 100) then synchronization context starts using all threads available in a thread pool. Idea #3 always performs tasks within dedicated thread as expected, unfortunately, not in the main thread. Combining idea #3 with IOCompletionPortTaskScheduler, I can achieve asynchrony and single-threading, unfortunately, this approach will work only in Windows. Is there a way to combine these solutions to achieve requirements at the top of the post, including cross-platform.
Scheduler
public class SomeScheduler
{
public Task<T> RunInTheMainThread<T>(Func<T> action, SynchronizationContext sc)
{
var res = new TaskCompletionSource<T>();
SynchronizationContext.SetSynchronizationContext(sc); // Idea #1
sc.Post(o => res.SetResult(action()), null); // Idea #2
ThreadPool.QueueUserWorkItem(state => res.SetResult(action())); // Idea #3
return res.Task;
}
}
Main
var scheduler = new SomeScheduler();
var sc = SynchronizationContext.Current ?? new SynchronizationContext();
new Thread(async () =>
{
var res = await scheduler.ExecuteAsync(() => 5, sc);
});
You can use the lock/Monitor.Pulse/Monitor.Wait and a Queue
I know the title says lock-free. But my guess is that you want the UI updates to occur outside the locks or worker threads should be able to continue working without having to wait for main thread to update the UI (at least this is how I understand the requirement).
Here the locks are never during the producing of items, or updating the UI. They are held only during the short duration it takes to enqueue/dequeue item in the queue.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using static System.Threading.Thread;
namespace ConsoleApp1
{
internal static class Program
{
private class WorkItem
{
public string SomeData { get; init; }
}
private static readonly Queue<WorkItem> s_workQueue = new Queue<WorkItem>();
private static void Worker()
{
var random = new Random();
// Simulate some work
Sleep(random.Next(1000));
// Produce work item outside the lock
var workItem = new WorkItem
{
SomeData = $"data produced from thread {CurrentThread.ManagedThreadId}"
};
// Acquire lock only for the short time needed to add the work item to the stack
lock (s_workQueue)
{
s_workQueue.Enqueue(workItem);
// Notify the main thread that a new item is added to the queue causing it to wakeup
Monitor.Pulse(s_workQueue);
}
// work item is now queued, no need to wait for main thread to finish updating the UI
// Continue work here
}
private static WorkItem GetWorkItem()
{
// Acquire lock only for the duration needed to get the item from the queue
lock (s_workQueue)
{
WorkItem result;
// Try to get the item from the queue
while (!s_workQueue.TryDequeue(out result))
{
// Lock is released during Wait call
Monitor.Wait(s_workQueue);
// Lock is acquired again after Wait call
}
return result;
}
}
private static void Main(string[] args)
{
const int totalTasks = 10;
for (var i = 0; i < totalTasks; i++)
{
_ = Task.Run(Worker);
}
var remainingTasks = totalTasks;
// Main loop (similar to message loop)
while (remainingTasks > 0)
{
var item = GetWorkItem();
// Update UI
Console.WriteLine("Got {0} and updated UI on thread {1}.", item.SomeData, CurrentThread.ManagedThreadId);
remainingTasks--;
}
Console.WriteLine("Done");
}
}
}
Update
Since you don't want to have the main thread Wait for an event, you can change the code as follows:
private static WorkItem? GetWorkItem()
{
// Acquire lock only for the duration needed to get the item from the queue
lock (s_workQueue)
{
// Try to get the item from the queue
s_workQueue.TryDequeue(out var result);
return result;
}
}
private static void Main(string[] args)
{
const int totalTasks = 10;
for (var i = 0; i < totalTasks; i++)
{
_ = Task.Run(Worker);
}
var remainingTasks = totalTasks;
// Main look (similar to message loop)
while (remainingTasks > 0)
{
var item = GetWorkItem();
if (item != null)
{
// Update UI
Console.WriteLine("Got {0} and updated UI on thread {1}.", item.SomeData, CurrentThread.ManagedThreadId);
remainingTasks--;
}
else
{
// Queue is empty, so do some other work here then try again after the work is done
// Do some other work here
// Sleep to simulate some work being done by main thread
Thread.Sleep(100);
}
}
Console.WriteLine("Done");
}
The problem in the above solution is that the Main thread should do only part of the work it is supposed to do, then call GetWorkItem to check if the queue has something, before resuming whatever it was doing again. It is doable if you can divide that work into small pieces that don't take too long.
I don't know if my answer here is what you want. What do you imagine the main thread would be doing when there are no work items in the queue?
if you think it should be doing nothing (i.e. waiting) then the Wait solution should be fine.
If you think it should be doing something, then may be that work it should be doing can be queued as a Work item as well.
I'm using Chronicle Queue as a DataStore which will be written to once but read many many times. I'm trying to get the best performance (time to read x number of records).
My data set (for my test) is about 3 million records , where each record consists of a bunch of longs and doubles. I initially started with "Highest-level" API which was obviously slow , then self-describing" data as mentioned in this Chronicle Documentation and finally using "raw data" which gave the best performance.
Code as below:(Corresponding write() code is omitted for brevity)
public List<DomainObject> read()
{
final ExcerptTailer tailer = _cq.createTailer();
List<DomainObject> result = new ArrayList<>();
for (; ; ) {
try (final DocumentContext ctx = tailer.readingDocument()) {
Wire wire = ctx.wire();
if(wire != null) {
wire.readBytes(in -> {
final long var1= in.readLong();
final int var2= in.readInt();
final double var3= in.readDouble();
final int var4= in.readInt();
final double var5= in.readDouble();
final int var6= in.readInt();
final double var7= in.readDouble();
result.add(DomainObject.create(var1, var2, var3, var4, var5, var6, var7);
});
}else{
return result;
}
}
}
}
However to improve my Application performance ,I started using ByteBuffer instead of a "DomainObject" and thus modified by read method as below:
public List<ByteBuffer> read()
{
final ExcerptTailer tailer = _cq.createTailer();
List<ByteBuffer> result = new ArrayList<>();
for (; ; ) {
try (final DocumentContext ctx = tailer.readingDocument()) {
Wire wire = ctx.wire();
if(wire != null) {
ByteBuffer bb = ByteBuffer.allocate(56);
wire.readBytes(in -> {
in.read(bb); });
result.add(bb);
}else{
return result;
}
}
}
}
Above code listing took an average of 550 ms vs 270ms for the first listing.
I also tried using Bytes.elasticByteBuffer as mentioned in this post but it was way slower
I'm guessing the second code listing is slower because it has to loop through the entire byte array.
So my question is - Is there a more performant way to read bytes from Chronicle Queue into a ByteBuffer? My data will always be 56 bytes with 8 bytes for each data item.
I suggest you use Chronicle-Bytes instead of raw ByteBuffer. Chronicle's Bytes class is a wrapper on top of ByteBuffer but much easier to use.
The problem with your code is you create a bunch of objects instead of stream-processing. I suggest you read with something like:
public void read(Consumer<Bytes> consumer) {
final ExcerptTailer tailer = _cq.createTailer();
for (; ; ) {
try (final DocumentContext ctx = tailer.readingDocument()) {
if (ctx.isPresent()) {
consumer.accept(ctx.wire().bytes());
} else {
break;
}
}
}
}
And your writing method could look like:
public void write(BytesMarshallable o) {
try (DocumentContext dc = _cq.acquireAppender().writingDocument()) {
o.writeMarshallable(dc.wire().bytes());
}
}
And then your consumer could be like:
private BytesMarshallable reusable = new BusinessObject(); //your class here
public accept(Bytes b) {
reusable.readMarshallable(b);
// your business logic here
doSomething(reusable);
}
I am trying to get a proof of concept running with akka.net. I am sure that I am doing something terribly wrong, but I can't figure out what it is.
I want my actors to form a graph of nodes. Later, this will be a complex graph of business objekts, but for now I want to try a simple linear structure like this:
I want to ask a node for a neighbour that is 9 steps away. I am trying to implement this in a recursive manner. I ask node #9 for a neighbour that is 9 steps away, then I ask node #8 for a neighbour that is 8 steps away and so on. Finally, this should return node #0 as an answer.
Well, my code works, but it takes more than 4 seconds to execute. Why is that?
This is my full code listing:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Akka;
using Akka.Actor;
namespace AkkaTest
{
class Program
{
public static Stopwatch stopwatch = new Stopwatch();
static void Main(string[] args)
{
var system = ActorSystem.Create("MySystem");
IActorRef[] current = new IActorRef[0];
Console.WriteLine("Initializing actors...");
for (int i = 0; i < 10; i++)
{
var current1 = current;
var props = Props.Create<Obj>(() => new Obj(current1, Guid.NewGuid()));
var actorRef = system.ActorOf(props, i.ToString());
current = new[] { actorRef };
}
Console.WriteLine("actors initialized.");
FindNeighboursRequest r = new FindNeighboursRequest(9);
stopwatch.Start();
var response = current[0].Ask(r);
FindNeighboursResponse result = (FindNeighboursResponse)response.Result;
stopwatch.Stop();
foreach (var d in result.FoundNeighbours)
{
Console.WriteLine(d);
}
Console.WriteLine("Search took " + stopwatch.ElapsedMilliseconds + "ms.");
Console.ReadLine();
}
}
public class FindNeighboursRequest
{
public FindNeighboursRequest(int distance)
{
this.Distance = distance;
}
public int Distance { get; private set; }
}
public class FindNeighboursResponse
{
private IActorRef[] foundNeighbours;
public FindNeighboursResponse(IEnumerable<IActorRef> descendants)
{
this.foundNeighbours = descendants.ToArray();
}
public IActorRef[] FoundNeighbours
{
get { return this.foundNeighbours; }
}
}
public class Obj : ReceiveActor
{
private Guid objGuid;
readonly List<IActorRef> neighbours = new List<IActorRef>();
public Obj(IEnumerable<IActorRef> otherObjs, Guid objGuid)
{
this.neighbours.AddRange(otherObjs);
this.objGuid = objGuid;
Receive<FindNeighboursRequest>(r => handleFindNeighbourRequest(r));
}
public Obj()
{
}
private async void handleFindNeighbourRequest (FindNeighboursRequest r)
{
if (r.Distance == 0)
{
FindNeighboursResponse response = new FindNeighboursResponse(new IActorRef[] { Self });
Sender.Tell(response, Self);
return;
}
List<FindNeighboursResponse> responses = new List<FindNeighboursResponse>();
foreach (var actorRef in neighbours)
{
FindNeighboursRequest req = new FindNeighboursRequest(r.Distance - 1);
var response2 = actorRef.Ask(req);
responses.Add((FindNeighboursResponse)response2.Result);
}
FindNeighboursResponse response3 = new FindNeighboursResponse(responses.SelectMany(rx => rx.FoundNeighbours));
Sender.Tell(response3, Self);
}
}
}
The reason of such slow behavior is the way you use Ask (an that you use it, but I'll cover this later). In your example, you're asking each neighbor in a loop, and then immediately executing response2.Result which is actively blocking current actor (and thread it resides on). So you're essentially making synchronous flow with blocking.
The easiest thing to fix that, is to collect all tasks returned from Ask and use Task.WhenAll to collect them all, without waiting for each one in a loop. Taking this example:
public class Obj : ReceiveActor
{
private readonly IActorRef[] _neighbours;
private readonly Guid _id;
public Obj(IActorRef[] neighbours, Guid id)
{
_neighbours = neighbours;
_id = id;
Receive<FindNeighboursRequest>(async r =>
{
if (r.Distance == 0) Sender.Tell(new FindNeighboursResponse(new[] {Self}));
else
{
var request = new FindNeighboursRequest(r.Distance - 1);
var replies = _neighbours.Select(neighbour => neighbour.Ask<FindNeighboursResponse>(request));
var ready = await Task.WhenAll(replies);
var responses = ready.SelectMany(x => x.FoundNeighbours);
Sender.Tell(new FindNeighboursResponse(responses.ToArray()));
}
});
}
}
This one is much faster.
NOTE: In general you shouldn't use Ask inside of an actor:
Each ask is allocating a listener inside current actor, so in general using Ask is A LOT heavier than passing messages with Tell.
When sending messages through chain of actors, cost of ask is additionally transporting message twice (one for request and one for reply) through each actor. One of the popular patterns is that, when you are sending request from A⇒B⇒C⇒D and respond from D back to A, you can reply directly D⇒A, without need of passing the message through whole chain back. Usually combination of Forward/Tell works better.
In general don't use async version of Receive if it's not necessary - at the moment, it's slower for an actor when compared to sync version.
I have created application using Lync client side SDK 2013 and UCMA 4.0 . Now I test my application with large number of users. How can I simulate large number of client using UCMA or Lync client side SDK?
It depends on what exactly what you want to "simulate".
If you just want call traffic there is sipp, but that is just simple sip calls and doesn't really reflect an actual Microsoft Lync Client.
As far as I know, Microsoft doesn't provide any load testing tools in Lync. You will have to generate them yourself base on what exactly you want to "simulate".
With a UCMA trusted application, you should be able to startup and use a large number of user endpoints to "simulate" common lync services (like randomly changing presence, making calls, send IM's, etc). You would have to create such an app yourself.
I created a tool in UCMA to do my stress test for all my applications than I have made.
It is simple to make, and it is composed of two parts.
This example is a stress tester for calls. Of course, you can easily make a different one by using this example.
We create our platform, follow our Set-CsTrustedApplication.
var platformSettings = new ProvisionedApplicationPlatformSettings("InnixiTester", "urn:application:innixitester");
var collabPlatform = new CollaborationPlatform(platformSettings);
collabPlatform.EndStartup(collabPlatform.BeginStartup(null, null));
Ok, I know what I am doing here is a wrong chaining together, the Begin and the End into one line of code. However, this is just a code exemple. I invite you to read the article of Tom Morgan, he explains why it is not good to do it like me.
We use here a Parallel loop to create all our users-endpoint. In that way, it goes faster.
/*
* Proprieties of the class
*/
private AutoResetEvent _waitForStressTestToFinish = new AutoResetEvent(false);
private List<UserEndpoint> _listUserEndpoints = new List<UserEndpoint>();
private int _maxUsers = 200;
private int _tickTotal;
private int _tickCount;
private int _nbrCallsByIntervall;
/*
* End
*/
_maxUsers = 200; // Nbr max of users
const var callsTotal = 200; // Nbr of total call
const var timeToTest = 30; // Total time to test
const var intervalOfCalls = 5; // We want to make our calls between specific intervals
Parallel.For(0, _maxUsers, i =>
{
CreateUserEndpoint(collabPlatform, i.ToString());
});
You simply create your UserEndpoint here. The scenario is that my users in the active directory are stressuser0 to stressuser200. With extension starting from +14250 to +1425200
private void CreateUserEndpoint(CollaborationPlatform cp, string iteration)
{
try
{
UserEndpointSettings settingsUser = new UserEndpointSettings($"sip:stressuser{iteration}#pferde.net", "pool2010.pferde.net", 5061);
settingsUser = InitializePublishAlwaysOnlineSettings(settingsUser);
var userEndpoint = new UserEndpoint(cp, settingsUser);
userEndpoint.EndEstablish(userEndpoint.BeginEstablish(null, null));
PublishOnline(userEndpoint);
_listUserEndpoints.Add(userEndpoint);
Console.WriteLine($"The User Endpoint owned by URI: {userEndpoint.OwnerUri} was created\n");
}
catch (Exception)
{
Console.WriteLine($"failed to create for --> sip:stressuser{iteration}#pferde.net");
throw;
}
}
private UserEndpointSettings InitializePublishAlwaysOnlineSettings(UserEndpointSettings settings)
{
settings.AutomaticPresencePublicationEnabled = true;
settings.Presence.PreferredServiceCapabilities.AudioSupport = CapabilitySupport.Supported;
return (settings);
}
Now time to place the calls! We are going to code a simple algorithm with a timer. Is going to calculate how many calls it needs to make for X time and for Y Calls and for Z intervals.
Console.WriteLine("Tape a key to place calls...");
Console.ReadKey();
PlaceCalls(callsTotal, timeToTest, intervalOfCalls);
_waitForStressTestToFinish.WaitOne();
}
catch (Exception ex)
{
Console.WriteLine($"Shutting down platform due to error {ex}");
ShutdownPlatform(collabPlatform);
}
ShutdownPlatform(collabPlatform);
}
private void PlaceCalls(int callsMax, int timeMax, int timeIntervall)
{
_tickTotal = timeMax / timeIntervall;
_nbrCallsByIntervall= callsMax / _tickTotal;
Console.WriteLine($"_nbrCallsByIntervall --> {_nbrCallsByIntervall}");
var timeIntervalTimespan = new TimeSpan(0, 0, 0, timeIntervall);
_timer = new Timer(timeIntervalTimespan.TotalMilliseconds);
_timer.Elapsed += new ElapsedEventHandler(_timer_Elapsed);
_timer.Enabled = true;
}
void _timer_Elapsed(object sender, ElapsedEventArgs e)
{
if (_tickCount < _tickTotal)
{
Console.WriteLine($"\n Pause Timer | On {_tickCount} to {_tickTotal}\n");
_timer.Enabled = false;
for (var i = 0; i <= _nbrCallsByIntervall - 1; ++i)
{
ConversationSettings convSettings = new ConversationSettings();
Conversation conversation = new Conversation(_listUserEndpoints[generateNumber(0, _listUserEndpoints.Count)], convSettings);
var audioVideoCall = new AudioVideoCall(conversation);
CallEstablishOptions options = new CallEstablishOptions();
var gNbr = generateNumber(0, _listUserEndpoints.Count);
try
{
// Here I'm calling a single phone number. You can use GenerateNumber to call stressusers each others. But you have to extend your code to accept the calls coming.
audioVideoCall.BeginEstablish($"3322", options, null, audioVideoCall);
}
catch (Exception)
{
Console.WriteLine("Fail to Call the remote user...");
throw;
}
Console.WriteLine($"Call--> +1425{gNbr}.Counter--> {_tickCount} Ticket--> {_tickTotal} and thread id {Thread.CurrentThread.ManagedThreadId}");
}
_tickCount++;
_timer.Enabled = true;
Console.WriteLine("\n reStart Timer \n");
}
else
{
Console.WriteLine("\n!!! END Stress test !!!\n");
_timer.Enabled = false;
_waitForCallToEstablish.Set();
}
}
private int generateNumber(int min, int max)
{
var r = new Random();
Thread.Sleep(200);
return (r.Next(min, max));
}
I am using Redis with StackExchange.Redis. I have multiple threads that will at some point access and edit the value of the same key, so I need to synchronize the manipulation of the data.
Looking at the available functions, I see that there are two functions, TakeLock and ReleaseLock. However, these functions take both a key and a value parameter rather than the expected single key to be locked. The intellisene documentation and source on GitHub don't explain how to use the LockTake and LockRelease functions or what to pass in for the key and value parameters.
Q: What is the correct usage of LockTake and LockRelease in StackExchange.Redis?
Pseudocode example of what I'm aiming to do:
//Add Items Before Parallel Execution
redis.StringSet("myJSONKey", myJSON);
//Parallel Execution
Parallel.For(0, 100, i =>
{
//Some work here
//....
//Lock
redis.LockTake("myJSONKey");
//Manipulate
var myJSONObject = redis.StringGet("myJSONKey");
myJSONObject.Total++;
Console.WriteLine(myJSONObject.Total);
redis.StringSet("myJSONKey", myNewJSON);
//Unlock
redis.LockRelease("myJSONKey");
//More work here
//...
});
There are 3 parts to a lock:
the key (the unique name of the lock in the database)
the value (a caller-defined token which can be used both to indicate who "owns" the lock, and to check that releasing and extending the lock is being done correctly)
the duration (a lock intentionally is a finite duration thing)
If no other value comes to mind, a guid might make a suitable "value". We tend to use the machine-name (or a munged version of the machine name if multiple processes could be competing on the same machine).
Also, note that taking a lock is speculative, not blocking. It is entirely possible that you fail to obtain the lock, and hence you may need to test for this and perhaps add some retry logic.
A typical example might be:
RedisValue token = Environment.MachineName;
if(db.LockTake(key, token, duration)) {
try {
// you have the lock do work
} finally {
db.LockRelease(key, token);
}
}
Note that if the work is lengthy (a loop, in particular), you may want to add some occasional LockExtend calls in the middle - again remembering to check for success (in case it timed out).
Note also that all individual redis commands are atomic, so you don't need to worry about two discreet operations competing. For more complexing multi-operation units, transactions and scripting are options.
There is my part of code for lock->get->modify(if required)->unlock actions with comments.
public static T GetCachedAndModifyWithLock<T>(string key, Func<T> retrieveDataFunc, TimeSpan timeExpiration, Func<T, bool> modifyEntityFunc,
TimeSpan? lockTimeout = null, bool isSlidingExpiration=false) where T : class
{
int lockCounter = 0;//for logging in case when too many locks per key
Exception logException = null;
var cache = Connection.GetDatabase();
var lockToken = Guid.NewGuid().ToString(); //unique token for current part of code
var lockName = key + "_lock"; //unique lock name. key-relative.
T tResult = null;
while ( lockCounter < 20)
{
//check for access to cache object, trying to lock it
if (!cache.LockTake(lockName, lockToken, lockTimeout ?? TimeSpan.FromSeconds(10)))
{
lockCounter++;
Thread.Sleep(100); //sleep for 100 milliseconds for next lock try. you can play with that
continue;
}
try
{
RedisValue result = RedisValue.Null;
if (isSlidingExpiration)
{
//in case of sliding expiration - get object with expiry time
var exp = cache.StringGetWithExpiry(key);
//check ttl.
if (exp.Expiry.HasValue && exp.Expiry.Value.TotalSeconds >= 0)
{
//get only if not expired
result = exp.Value;
}
}
else //in absolute expiration case simply get
{
result = cache.StringGet(key);
}
//"REDIS_NULL" is for cases when our retrieveDataFunc function returning null (we cannot store null in redis, but can store pre-defined string :) )
if (result.HasValue && result == "REDIS_NULL") return null;
//in case when cache is epmty
if (!result.HasValue)
{
//retrieving data from caller function (from db from example)
tResult = retrieveDataFunc();
if (tResult != null)
{
//trying to modify that entity. if caller modifyEntityFunc returns true, it means that caller wants to resave modified entity.
if (modifyEntityFunc(tResult))
{
//json serialization
var json = JsonConvert.SerializeObject(tResult);
cache.StringSet(key, json, timeExpiration);
}
}
else
{
//save pre-defined string in case if source-value is null.
cache.StringSet(key, "REDIS_NULL", timeExpiration);
}
}
else
{
//retrieve from cache and serialize to required object
tResult = JsonConvert.DeserializeObject<T>(result);
//trying to modify
if (modifyEntityFunc(tResult))
{
//and save if required
var json = JsonConvert.SerializeObject(tResult);
cache.StringSet(key, json, timeExpiration);
}
}
//refresh exiration in case of sliding expiration flag
if(isSlidingExpiration)
cache.KeyExpire(key, timeExpiration);
}
catch (Exception ex)
{
logException = ex;
}
finally
{
cache.LockRelease(lockName, lockToken);
}
break;
}
if (lockCounter >= 20 || logException!=null)
{
//log it
}
return tResult;
}
and usage :
public class User
{
public int ViewCount { get; set; }
}
var cachedAndModifiedItem = GetCachedAndModifyWithLock<User>(
"MyAwesomeKey", //your redis key
() => // callback to get data from source in case if redis's store is empty
{
//return from db or kind of that
return new User() { ViewCount = 0 };
},
TimeSpan.FromMinutes(10), //object expiration time to pass in Redis
user=> //modify object callback. return true if you need to save it back to redis
{
if (user.ViewCount< 3)
{
user.ViewCount++;
return true; //save it to cache
}
return false; //do not update it in cache
},
TimeSpan.FromSeconds(10), //lock redis timeout. if you will have race condition situation - it will be locked for 10 seconds and wait "get_from_db"/redis read/modify operations done.
true //is expiration should be sliding.
);
That code can be improved (for example, you can add transactions for less count call to cache and etc), but i glad it will be helpfull for you.