public void TaskTimer_Shutdown() { var shouldFire = new ManualResetEvent(false); var neverFires = new ManualResetEvent(false); TaskTimer.New( TimeSpan.FromSeconds(2), delegate { _log.DebugFormat("this task timer should never have fired"); neverFires.Set(); }, null, TaskEnv.None); TaskTimer.New( TimeSpan.FromSeconds(1), delegate { _log.DebugFormat("this task timer should fire before we try shutdown"); shouldFire.Set(); }, null, TaskEnv.None); _log.DebugFormat("waiting for first task"); Assert.IsTrue(shouldFire.WaitOne(2000, false)); _log.DebugFormat("starting shutdown"); TaskTimerFactory.Current.Dispose(); _log.DebugFormat("shutdown complete"); Assert.IsFalse(neverFires.WaitOne(2000, false)); }
private XDoc FetchSchema(Plug adoNetPlug) { XDoc ret = null; string key = adoNetPlug.At(METADATA_PATH).ToString(); lock (_cache) { _cache.TryGetValue(key, out ret); } if (ret == null) { string temp = adoNetPlug.At(METADATA_PATH).Get().AsTextReader().ReadToEnd(); //HACKHACKHACK to workaround ns issue temp = temp.Replace("xmlns=\"http://schemas.microsoft.com/ado/2006/04/edm\"", ""); ret = XDocFactory.From(temp, MimeType.XML); // add result to cache and start a clean-up timer lock (_cache) { _cache[key] = ret; } TaskTimer.New(TimeSpan.FromSeconds(CACHE_TTL), RemoveCachedEntry, key, TaskEnv.None); } //TODO: throw exception if schema is invalid somehow (or if the schema changed) return(ret); }
private static string CachedWebGet(XUri uri, double?ttl, bool?nilIfMissing) { // fetch message from cache or from the web string result; lock (_webTextCache) { if (_webTextCache.TryGetValue(uri, out result)) { return(result); } } // do the web request Result <DreamMessage> response = new Result <DreamMessage>(); Plug.New(uri).WithTimeout(DEFAULT_WEB_TIMEOUT).InvokeEx("GET", DreamMessage.Ok(), response); DreamMessage message = response.Wait(); try { // check message status if (!message.IsSuccessful) { if (nilIfMissing.GetValueOrDefault()) { return(null); } return(message.Status == DreamStatus.UnableToConnect ? string.Format("(unable to fetch text document from uri [status: {0} ({1}), message: \"{2}\"])", (int)message.Status, message.Status, message.ToDocument()["message"].AsText) : string.Format("(unable to fetch text document from uri [status: {0} ({1})])", (int)message.Status, message.Status)); } // check message size Result resMemorize = message.Memorize(InsertTextLimit, new Result()).Block(); if (resMemorize.HasException) { return(nilIfMissing.GetValueOrDefault() ? null : "(text document is too large)"); } // detect encoding and decode response var stream = message.AsStream(); var encoding = stream.DetectEncoding() ?? message.ContentType.CharSet; result = encoding.GetString(stream.ReadBytes(-1)); } finally { message.Close(); } // start timer to clean-up cached result lock (_webTextCache) { _webTextCache[uri] = result; } double timeout = Math.Min(60 * 60 * 24, Math.Max(ttl ?? MinCacheTtl, 60)); TaskEnv.ExecuteNew(() => TaskTimer.New(TimeSpan.FromSeconds(timeout), timer => { lock (_webTextCache) { _webTextCache.Remove((XUri)timer.State); } }, uri, TaskEnv.None)); return(result); }
private XDoc GetFeed(XUri uri, string format, Result <DreamMessage> result, int?max) { if (result.HasException) { return(new XDoc("html").Start("body").Value(string.Format("An error occurred while retrieving the feed ({0}).", result.Exception.Message)).End()); } XDoc rss; if (result.Value.ContentType.IsXml) { rss = result.Value.ToDocument(); } else { // NOTE (steveb): some servers return the wrong content-type (e.g. text/html or text/plain); ignore the content-type then and attempt to parse the document try { rss = XDocFactory.From(result.Value.AsTextReader(), MimeType.XML); } catch (Exception e) { return(new XDoc("html").Start("body").Value("An error occurred while retrieving the feed (invalid xml).").End()); } } if (rss.IsEmpty) { return(new XDoc("html").Start("body").Value("An error occurred while retrieving the feed (no results returned).").End()); } // check if result needs to be cached if (_cached > 0) { lock (_feeds) { if (!_feeds.ContainsKey(uri)) { _feeds[uri] = rss; TaskTimer.New(TimeSpan.FromSeconds(_cached), OnTimeout, uri, TaskEnv.None); } } } // Create format style parameter XsltArgumentList xslArg = new XsltArgumentList(); xslArg.AddParam("format", String.Empty, format); if (max.HasValue) { xslArg.AddParam("max", String.Empty, max.ToString()); } StringWriter writer = new StringWriter(); _xslt.Transform(rss.AsXmlNode, xslArg, writer); return(AddTableStyles(XDocFactory.From("<html><body>" + writer.ToString() + "</body></html>", MimeType.HTML))); }
//--- Methods --- private Yield FetchResult(string name, XUri input, Hashtable args, Result <XDoc> response) { // build uri XUri uri = new XUri("http://www.dapper.net/RunDapp?v=1").With("dappName", name); if (input != null) { uri = uri.With("applyToUrl", input.ToString()); } if (args != null) { foreach (DictionaryEntry entry in args) { uri = uri.With(VARIABLE_PREFIX + SysUtil.ChangeType <string>(entry.Key), SysUtil.ChangeType <string>(entry.Value)); } } // check if we have a cached result XDoc result; string key = uri.ToString(); lock (_cache) { if (_cache.TryGetValue(key, out result)) { response.Return(result); yield break; } } // fetch result Result <DreamMessage> res; yield return(res = Plug.New(uri).GetAsync()); if (!res.Value.IsSuccessful) { throw new DreamInternalErrorException(string.Format("Unable to process Dapp: ", input)); } if (!res.Value.HasDocument) { throw new DreamInternalErrorException(string.Format("Dapp response is not XML: ", input)); } // add result to cache and start a clean-up timer lock (_cache) { _cache[key] = res.Value.ToDocument(); } TaskTimer.New(TimeSpan.FromSeconds(CACHE_TTL), RemoveCachedEntry, key, TaskEnv.None); response.Return(res.Value.ToDocument()); yield break; }
private void SetupCacheTimer(CacheEntry cacheEntry) { TaskTimer.New(cacheEntry.Expires, timer => { var entry = (CacheEntry)timer.State; _log.DebugFormat("removing '{0}' from cache", entry.Id); lock (entry) { // removing from lookup first, since a false return value on Remove indicates that we shouldn't // try to delete the file from disk if (_cacheLookup.Remove(entry.Id)) { DeleteCacheEntry(entry.Guid); } } }, cacheEntry, TaskEnv.None); }
//--- Constructors --- public PageChangeCache(Plug deki, TimeSpan ttl) : this(deki, (key, clearAction) => TaskTimer.New(ttl, timer => clearAction(), null, TaskEnv.None)) { }
//--- Methods --- protected override Yield Start(XDoc config, Result result) { yield return(Coroutine.Invoke(base.Start, config, new Result())); // set up cache reaper _ttl = TimeSpan.FromSeconds(Config["news-ttl"].AsDouble ?? 60 * 60); double?checkInterval = Config["check-interval"].AsDouble; if (checkInterval.HasValue) { _checkInterval = TimeSpan.FromSeconds(checkInterval.Value); } else { double checkInterval2 = _ttl.TotalSeconds / 10; if (_ttl.TotalSeconds < 30 || checkInterval2 < 30) { checkInterval2 = 30; } _checkInterval = TimeSpan.FromSeconds(checkInterval2); } TaskTimer.New(_ttl, delegate(TaskTimer timer) { lock (_pageViews) { View next; do { if (_pageViews.Count == 0) { break; } next = _pageViews.Peek(); if (next != null) { if (next.Time.Add(_ttl) < DateTime.UtcNow) { _pageViews.Dequeue(); } else { break; } } } while(next != null); } timer.Change(_checkInterval, TaskEnv.None); }, null, TaskEnv.None); // get the apikey, which we will need as a subscription auth token for subscriptions not done on behalf of a user _apikey = config["apikey"].AsText; // build ignore list string ignore = config["ignore"].AsText; if (!string.IsNullOrEmpty(ignore)) { foreach (string page in ignore.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)) { _ignore[page] = null; } } // set up subscription for page views XDoc subscription = new XDoc("subscription-set") .Elem("uri.owner", Self.Uri.AsServerUri().ToString()) .Start("subscription") .Elem("channel", "event://*/deki/pages/view") .Add(DreamCookie.NewSetCookie("service-key", InternalAccessKey, Self.Uri).AsSetCookieDocument) .Start("recipient") .Attr("authtoken", _apikey) .Elem("uri", Self.Uri.AsServerUri().At("notify", "view").ToString()) .End() .End(); Result <DreamMessage> subscribe; yield return(subscribe = PubSub.At("subscribers").PostAsync(subscription)); string accessKey = subscribe.Value.ToDocument()["access-key"].AsText; XUri location = subscribe.Value.Headers.Location; Cookies.Update(DreamCookie.NewSetCookie("access-key", accessKey, location), null); _subscriptionLocation = Plug.New(location.AsLocalUri().WithoutQuery()); _log.DebugFormat("set up initial subscription location at {0}", _subscriptionLocation.Uri); result.Return(); }
//--- Methods --- private XDoc PerformQuery(XUri dataservice, string resource, string expand, string filter, string orderby, int?skip, int?top, bool fetchSchema, out XDoc schema) { Plug p = _adoNetPlug; if (dataservice != null) { p = Plug.New(dataservice); } else if (p == null) { throw new ArgumentException("Missing field", "dataservice"); } if (fetchSchema) { schema = FetchSchema(p); } else { schema = null; } if (!string.IsNullOrEmpty(resource)) { //HACKHACKHACK: +'s aren't treated the same way as '%20' when uri is decoded on the server side string s = XUri.Encode(resource).Replace("+", "%20"); p = p.At(s); } if (!string.IsNullOrEmpty(expand)) { p = p.With("$expand", expand); } if (!string.IsNullOrEmpty(filter)) { p = p.With("$filter", filter); } if (!string.IsNullOrEmpty(orderby)) { p = p.With("$orderby", orderby); } if (skip != null) { p = p.With("$skip", skip.Value); } if (top != null) { p = p.With("$top", top.Value); } XDoc ret = null; string key = p.ToString(); lock (_cache) { _cache.TryGetValue(key, out ret); } if (ret == null) { ret = p.Get().ToDocument(); // add result to cache and start a clean-up timer lock (_cache) { _cache[key] = ret; } TaskTimer.New(TimeSpan.FromSeconds(CACHE_TTL), RemoveCachedEntry, key, TaskEnv.None); } return(ret); }
//--- Methods --- protected override Yield Start(XDoc config, Result result) { yield return(Coroutine.Invoke(base.Start, config, new Result())); // get the apikey, which we will need as a subscription auth token for subscriptions not done on behalf of a user _apikey = config["apikey"].AsText; // capture the wikiId of the wiki starting us string wikiId = config["wikiid"].AsText ?? "default"; // set up plug deki, so we can validate users XUri dekiUri = config["uri.deki"].AsUri ?? new XUri("http://localhost:8081/deki"); _deki = Plug.New(dekiUri).With("apikey", _apikey).WithHeader("X-Deki-Site", "id=" + wikiId); // get ajax polling interval _pollInterval = TimeSpan.FromSeconds(config["poll-interval"].AsDouble ?? 60); // set up subscription reaper TaskTimer.New(TimeSpan.FromSeconds(60), timer => { lock (_subscriptions) { var staleSubs = new List <uint>(); foreach (KeyValuePair <uint, Subscription> pair in _subscriptions) { if (pair.Value.LastTouched.Add(_pollInterval).Add(TimeSpan.FromSeconds(10)) < DateTime.UtcNow) { staleSubs.Add(pair.Key); } } foreach (uint pageId in staleSubs) { _log.DebugFormat("removing subscription for {0}", pageId); _subscriptions.Remove(pageId); } } timer.Change(TimeSpan.FromSeconds(60), TaskEnv.None); }, null, TaskEnv.None); // set up subscription for pubsub XDoc subscription = new XDoc("subscription-set") .Elem("uri.owner", Self.Uri.AsServerUri().ToString()) .Start("subscription") .Elem("channel", string.Format("event://{0}/deki/pages/update", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/revert", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/tags/update", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/comments/create", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/comments/update", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/comments/delete", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/files/create", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/files/update", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/files/delete", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/files/properties/*", wikiId)) .Elem("channel", string.Format("event://{0}/deki/pages/dependentschanged/files/restore", wikiId)) .Add(DreamCookie.NewSetCookie("service-key", InternalAccessKey, Self.Uri).AsSetCookieDocument) .Start("recipient") .Attr("authtoken", _apikey) .Elem("uri", Self.Uri.AsServerUri().At("notify").ToString()) .End() .End(); Result <DreamMessage> subscribe; yield return(subscribe = PubSub.At("subscribers").PostAsync(subscription)); string accessKey = subscribe.Value.ToDocument()["access-key"].AsText; XUri location = subscribe.Value.Headers.Location; Cookies.Update(DreamCookie.NewSetCookie("access-key", accessKey, location), null); _subscriptionLocation = Plug.New(location.AsLocalUri().WithoutQuery()); _log.DebugFormat("set up initial subscription location at {0}", _subscriptionLocation.Uri); result.Return(); }