i've written generic cache class designed return in-memory object , evaluate src (an iqueryable, or function returning iqueryable) occasionally. used in couple of places in app fetching lot of data via entity framework expensive.
it called
//class level private static cacheddata<foo> foocache= new cacheddata<foo>(); //method level var results = foocache.getdata("foos", foorepo.include("bars"));
although appeared work ok in testing, running on busy web server i'm seeing issues "collection modified; enumeration operation may not execute." errors in code consumes results.
this must because 1 thread overwriting results object inside lock, while using them, outside lock.
i'm guessing solution return copy of results each consumer rather original, , cannot allow copy occur while inside fetch lock, multiple copies occur simultaneously.
can suggest better way, or locking strategy please?
public class cacheddata<t> t:class { private static dictionary<string, ienumerable<t>> datacache { get; set; } public static dictionary<string, datetime> expire { get; set; } public int ttl { get; set; } private object lo = new object(); public cacheddata() { ttl = 600; expire = new dictionary<string, datetime>(); datacache = new dictionary<string, ienumerable<t>>(); } public ienumerable<t> getdata(string key, func<iqueryable<t>> src) { var bc = brandkey(key); if (!datacache.containskey(bc)) fetch(bc, src); if (datetime.now > expire[bc]) fetch(bc, src); return datacache[bc]; } public ienumerable<t> getdata(string key, iqueryable<t> src) { var bc = brandkey(key); if ((!datacache.containskey(bc)) || (datetime.now > expire[bc])) fetch(bc, src); return datacache[bc]; } private void fetch(string key, iqueryable<t> src ) { lock (lo) { if ((!datacache.containskey(key)) || (datetime.now > expire[key])) executefetch(key, src); } } private void fetch(string key, func<iqueryable<t>> src) { lock (lo) { if ((!datacache.containskey(key)) || (datetime.now > expire[key])) executefetch(key, src()); } } private void executefetch(string key, iqueryable<t> src) { if (!datacache.containskey(key)) datacache.add(key, src.tolist()); else datacache[key] = src.tolist(); if (!expire.containskey(key)) expire.add(key, datetime.now.addseconds(ttl)); else expire[key] = datetime.now.addseconds(ttl); } private string brandkey(string key, int? brandid = null) { return string.format("{0}/{1}", brandid ?? config.brandid, key); } }
i use concurrentdictionary<tkey, lazy<tvalue>>
. gives lock per key. makes strategy hold lock while fetching viable. avoids cache-stampeding. guarantees 1 evaluation per key ever happen. lazy<t>
automates locking entirely.
regarding expiration logic: set timer cleans dictionary (or rewrites entirely) every x seconds.
Comments
Post a Comment