{"action":"created","comment":{"url":"https://api.github.com/repos/Particular/NServiceBus/pulls/comments/88938376","pull_request_review_id":9494247,"id":88938376,"diff_hunk":"@@ -3,203 +3,211 @@ namespace NServiceBus\n     using System;\n     using System.Collections.Concurrent;\n     using System.Collections.Generic;\n-    using System.Runtime.CompilerServices;\n     using System.Threading.Tasks;\n     using Extensibility;\n-    using Persistence;\n     using Sagas;\n-\n+    using Persistence;\n+    \n     class InMemorySagaPersister : ISagaPersister\n     {\n-        public Task Complete(IContainSagaData sagaData, SynchronizedStorageSession session, ContextBag context)\n+        static readonly CorrelationId NoCorrelationId = new CorrelationId(typeof(object), \"\", new object());\n+        readonly ConcurrentDictionary<Guid, Entry> _sagas = new ConcurrentDictionary<Guid, Entry>();\n+        readonly ConcurrentDictionary<CorrelationId, Guid> _byCorrelationId = new ConcurrentDictionary<CorrelationId, Guid>();\n+        static readonly JsonMessageSerializer serializer = new JsonMessageSerializer(null);\n+\n+        class Entry\n         {\n-            var inMemSession = (InMemorySynchronizedStorageSession) session;\n-            inMemSession.Enlist(() =>\n+            public CorrelationId CorrelationId { get; private set; }\n+            public IContainSagaData ContainSagaData { get; }\n+\n+            public Entry(IContainSagaData sagaData, CorrelationId correlationId)\n             {\n-                VersionedSagaEntity value;\n-                if (data.TryRemove(sagaData.Id, out value))\n-                {\n-                    object lockToken;\n-                    lockers.TryRemove(value.LockTokenKey, out lockToken);\n-                }\n-            });\n-            return TaskEx.CompletedTask;\n+                CorrelationId = correlationId;\n+                ContainSagaData = DeepClone(sagaData);\n+            }\n+\n+            static IContainSagaData DeepClone(IContainSagaData source)\n+            {\n+                var json = serializer.SerializeObject(source);\n+                return (IContainSagaData)serializer.DeserializeObject(json, source.GetType());\n+            }\n+\n+            public Entry UpdateTo(IContainSagaData sagaData)\n+            {\n+                return new Entry(sagaData, CorrelationId);\n+            }\n         }\n \n-        public Task<TSagaData> Get<TSagaData>(string propertyName, object propertyValue, SynchronizedStorageSession session, ContextBag context) where TSagaData : IContainSagaData\n+        public Task Complete(IContainSagaData sagaData, SynchronizedStorageSession session, ContextBag context)\n         {\n-            Guard.AgainstNull(nameof(propertyValue), propertyValue);\n-\n-            foreach (var entity in data.Values)\n+            ((InMemorySynchronizedStorageSession)session).Enlist(() =>\n             {\n-                if (!(entity.SagaData is TSagaData))\n+                var entry = GetEntry(sagaData, context);\n+\n+                if (_sagas.TryRemoveConditionally(sagaData.Id, entry) == false)\n                 {\n-                    continue;\n+                    throw new Exception(\"Saga can't be completed as it was updated by another process.\");\n                 }\n \n-                var prop = typeof(TSagaData).GetProperty(propertyName);\n-                if (prop == null)\n+                // saga removed\n+                // clean the index\n+                if (Equals(entry.CorrelationId, NoCorrelationId) == false)\n                 {\n-                    continue;\n+                    _byCorrelationId.TryRemoveConditionally(entry.CorrelationId, sagaData.Id);\n                 }\n-                var existingValue = prop.GetValue(entity.SagaData);\n+            });\n+\n+            return Task.FromResult(0);\n+        }\n+\n+        public Task<TSagaData> Get<TSagaData>(Guid sagaId, SynchronizedStorageSession session, ContextBag context)\n+            where TSagaData : IContainSagaData\n+        {\n+            Entry value;\n+\n+            if (_sagas.TryGetValue(sagaId, out value))\n+            {\n+                context.Set(sagaId.ToString(), value);","path":"src/NServiceBus.Core/Persistence/InMemory/SagaPersister/InMemorySagaPersister.cs","position":null,"original_position":92,"commit_id":"5056ba97f1ca4083129f1acf05d824ef78c0b456","original_commit_id":"adc8dd2d1123b32d4fc93ed29528971387ccb369","user":{"login":"timbussmann","id":3524870,"avatar_url":"https://avatars.githubusercontent.com/u/3524870?v=3","gravatar_id":"","url":"https://api.github.com/users/timbussmann","html_url":"https://github.com/timbussmann","followers_url":"https://api.github.com/users/timbussmann/followers","following_url":"https://api.github.com/users/timbussmann/following{/other_user}","gists_url":"https://api.github.com/users/timbussmann/gists{/gist_id}","starred_url":"https://api.github.com/users/timbussmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/timbussmann/subscriptions","organizations_url":"https://api.github.com/users/timbussmann/orgs","repos_url":"https://api.github.com/users/timbussmann/repos","events_url":"https://api.github.com/users/timbussmann/events{/privacy}","received_events_url":"https://api.github.com/users/timbussmann/received_events","type":"User","site_admin":false},"body":"> You can't use a constant key as the same message might be handled by multiple sagas.\r\n\r\nthat's afaik not possible?","created_at":"2016-11-21T16:55:28Z","updated_at":"2016-11-21T16:55:28Z","html_url":"https://github.com/Particular/NServiceBus/pull/4300#discussion_r88938376","pull_request_url":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300","_links":{"self":{"href":"https://api.github.com/repos/Particular/NServiceBus/pulls/comments/88938376"},"html":{"href":"https://github.com/Particular/NServiceBus/pull/4300#discussion_r88938376"},"pull_request":{"href":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300"}}},"pull_request":{"url":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300","id":93778033,"html_url":"https://github.com/Particular/NServiceBus/pull/4300","diff_url":"https://github.com/Particular/NServiceBus/pull/4300.diff","patch_url":"https://github.com/Particular/NServiceBus/pull/4300.patch","issue_url":"https://api.github.com/repos/Particular/NServiceBus/issues/4300","number":4300,"state":"open","locked":false,"title":"Faster InMemorySagaPersister","user":{"login":"Scooletz","id":519707,"avatar_url":"https://avatars.githubusercontent.com/u/519707?v=3","gravatar_id":"","url":"https://api.github.com/users/Scooletz","html_url":"https://github.com/Scooletz","followers_url":"https://api.github.com/users/Scooletz/followers","following_url":"https://api.github.com/users/Scooletz/following{/other_user}","gists_url":"https://api.github.com/users/Scooletz/gists{/gist_id}","starred_url":"https://api.github.com/users/Scooletz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Scooletz/subscriptions","organizations_url":"https://api.github.com/users/Scooletz/orgs","repos_url":"https://api.github.com/users/Scooletz/repos","events_url":"https://api.github.com/users/Scooletz/events{/privacy}","received_events_url":"https://api.github.com/users/Scooletz/received_events","type":"User","site_admin":false},"body":"This PR provides an alternative implementation of `InMemorySagaPersister` to #4290 and addresses issue #4158\r\nIt uses following approaches:\r\n\r\n## Two dictionaries\r\nTwo concurrent dictionaries are used:\r\n- first, for storing sagas entries (primary index)\r\n- second, for storing mapping `(correlation id)->sagaId` (secondary index)\r\n\r\nUsing these two greatly limits the locking and the overall over overhead of the data structure. \r\n \r\n|Saga Persister Operation| Locks\\Volatiles| Description|\r\n|---|---|---|\r\n|`Get (sagaId)` | 1x volatile read | single `TryGetValue` against the primary index |\r\n|`Get (propName, propValue)`| 2x volatile read |  `TryGetValue` against the secondary index followed by `TryGetValue` against primary|\r\n|`Save(saga)`| 2x fine grained lock | `TryAdd` used for the secondary and the primary |\r\n|`Update(saga)`| 1x fine grained lock | `TryUpdate` used for the primary index| \r\n|`Complete(saga)`| 2x fine grained lock | `TryRemoveConditionally` used for both indexes|\r\n\r\n## ContextBag\r\n\r\nAs the state transitions between getting, saving, updating, completing a saga are well defined, instead of using a `ConditionalWeakTable` the saga entry can be passed in the `ContextBag` to be later used for the optimistic concurrency purposes.\r\n\r\n## TryRemoveConditionally\r\n\r\nAn extension method has been provided for the `ConcurrentDictionary` to allow a conditional removal of a value (much like `Interlocked.CompareExchange`). This enables an optimistic concurrency for `Complete` method, again, shifting all the heavy lifting to the `ConcurrentDictionary` structure.","created_at":"2016-11-15T14:43:55Z","updated_at":"2016-11-21T16:55:28Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f948083586485bc6363a1535f1b745b38ba9c57d","assignee":{"login":"Scooletz","id":519707,"avatar_url":"https://avatars.githubusercontent.com/u/519707?v=3","gravatar_id":"","url":"https://api.github.com/users/Scooletz","html_url":"https://github.com/Scooletz","followers_url":"https://api.github.com/users/Scooletz/followers","following_url":"https://api.github.com/users/Scooletz/following{/other_user}","gists_url":"https://api.github.com/users/Scooletz/gists{/gist_id}","starred_url":"https://api.github.com/users/Scooletz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Scooletz/subscriptions","organizations_url":"https://api.github.com/users/Scooletz/orgs","repos_url":"https://api.github.com/users/Scooletz/repos","events_url":"https://api.github.com/users/Scooletz/events{/privacy}","received_events_url":"https://api.github.com/users/Scooletz/received_events","type":"User","site_admin":false},"assignees":[{"login":"Scooletz","id":519707,"avatar_url":"https://avatars.githubusercontent.com/u/519707?v=3","gravatar_id":"","url":"https://api.github.com/users/Scooletz","html_url":"https://github.com/Scooletz","followers_url":"https://api.github.com/users/Scooletz/followers","following_url":"https://api.github.com/users/Scooletz/following{/other_user}","gists_url":"https://api.github.com/users/Scooletz/gists{/gist_id}","starred_url":"https://api.github.com/users/Scooletz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Scooletz/subscriptions","organizations_url":"https://api.github.com/users/Scooletz/orgs","repos_url":"https://api.github.com/users/Scooletz/repos","events_url":"https://api.github.com/users/Scooletz/events{/privacy}","received_events_url":"https://api.github.com/users/Scooletz/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300/commits","review_comments_url":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300/comments","review_comment_url":"https://api.github.com/repos/Particular/NServiceBus/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Particular/NServiceBus/issues/4300/comments","statuses_url":"https://api.github.com/repos/Particular/NServiceBus/statuses/5056ba97f1ca4083129f1acf05d824ef78c0b456","head":{"label":"Particular:faster-in-memory-saga-persister","ref":"faster-in-memory-saga-persister","sha":"5056ba97f1ca4083129f1acf05d824ef78c0b456","user":{"login":"Particular","id":5038441,"avatar_url":"https://avatars.githubusercontent.com/u/5038441?v=3","gravatar_id":"","url":"https://api.github.com/users/Particular","html_url":"https://github.com/Particular","followers_url":"https://api.github.com/users/Particular/followers","following_url":"https://api.github.com/users/Particular/following{/other_user}","gists_url":"https://api.github.com/users/Particular/gists{/gist_id}","starred_url":"https://api.github.com/users/Particular/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Particular/subscriptions","organizations_url":"https://api.github.com/users/Particular/orgs","repos_url":"https://api.github.com/users/Particular/repos","events_url":"https://api.github.com/users/Particular/events{/privacy}","received_events_url":"https://api.github.com/users/Particular/received_events","type":"Organization","site_admin":false},"repo":{"id":1056713,"name":"NServiceBus","full_name":"Particular/NServiceBus","owner":{"login":"Particular","id":5038441,"avatar_url":"https://avatars.githubusercontent.com/u/5038441?v=3","gravatar_id":"","url":"https://api.github.com/users/Particular","html_url":"https://github.com/Particular","followers_url":"https://api.github.com/users/Particular/followers","following_url":"https://api.github.com/users/Particular/following{/other_user}","gists_url":"https://api.github.com/users/Particular/gists{/gist_id}","starred_url":"https://api.github.com/users/Particular/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Particular/subscriptions","organizations_url":"https://api.github.com/users/Particular/orgs","repos_url":"https://api.github.com/users/Particular/repos","events_url":"https://api.github.com/users/Particular/events{/privacy}","received_events_url":"https://api.github.com/users/Particular/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Particular/NServiceBus","description":"The most popular service bus for .NET","fork":false,"url":"https://api.github.com/repos/Particular/NServiceBus","forks_url":"https://api.github.com/repos/Particular/NServiceBus/forks","keys_url":"https://api.github.com/repos/Particular/NServiceBus/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Particular/NServiceBus/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Particular/NServiceBus/teams","hooks_url":"https://api.github.com/repos/Particular/NServiceBus/hooks","issue_events_url":"https://api.github.com/repos/Particular/NServiceBus/issues/events{/number}","events_url":"https://api.github.com/repos/Particular/NServiceBus/events","assignees_url":"https://api.github.com/repos/Particular/NServiceBus/assignees{/user}","branches_url":"https://api.github.com/repos/Particular/NServiceBus/branches{/branch}","tags_url":"https://api.github.com/repos/Particular/NServiceBus/tags","blobs_url":"https://api.github.com/repos/Particular/NServiceBus/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Particular/NServiceBus/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Particular/NServiceBus/git/refs{/sha}","trees_url":"https://api.github.com/repos/Particular/NServiceBus/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Particular/NServiceBus/statuses/{sha}","languages_url":"https://api.github.com/repos/Particular/NServiceBus/languages","stargazers_url":"https://api.github.com/repos/Particular/NServiceBus/stargazers","contributors_url":"https://api.github.com/repos/Particular/NServiceBus/contributors","subscribers_url":"https://api.github.com/repos/Particular/NServiceBus/subscribers","subscription_url":"https://api.github.com/repos/Particular/NServiceBus/subscription","commits_url":"https://api.github.com/repos/Particular/NServiceBus/commits{/sha}","git_commits_url":"https://api.github.com/repos/Particular/NServiceBus/git/commits{/sha}","comments_url":"https://api.github.com/repos/Particular/NServiceBus/comments{/number}","issue_comment_url":"https://api.github.com/repos/Particular/NServiceBus/issues/comments{/number}","contents_url":"https://api.github.com/repos/Particular/NServiceBus/contents/{+path}","compare_url":"https://api.github.com/repos/Particular/NServiceBus/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Particular/NServiceBus/merges","archive_url":"https://api.github.com/repos/Particular/NServiceBus/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Particular/NServiceBus/downloads","issues_url":"https://api.github.com/repos/Particular/NServiceBus/issues{/number}","pulls_url":"https://api.github.com/repos/Particular/NServiceBus/pulls{/number}","milestones_url":"https://api.github.com/repos/Particular/NServiceBus/milestones{/number}","notifications_url":"https://api.github.com/repos/Particular/NServiceBus/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Particular/NServiceBus/labels{/name}","releases_url":"https://api.github.com/repos/Particular/NServiceBus/releases{/id}","deployments_url":"https://api.github.com/repos/Particular/NServiceBus/deployments","created_at":"2010-11-06T12:54:52Z","updated_at":"2016-11-18T05:37:45Z","pushed_at":"2016-11-21T16:13:47Z","git_url":"git://github.com/Particular/NServiceBus.git","ssh_url":"git@github.com:Particular/NServiceBus.git","clone_url":"https://github.com/Particular/NServiceBus.git","svn_url":"https://github.com/Particular/NServiceBus","homepage":"https://particular.net/nservicebus","size":177391,"stargazers_count":1021,"watchers_count":1021,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":526,"mirror_url":null,"open_issues_count":203,"forks":526,"open_issues":203,"watchers":1021,"default_branch":"develop"}},"base":{"label":"Particular:develop","ref":"develop","sha":"4c5a2ffeea585422f700a2770d2c574c25557a0d","user":{"login":"Particular","id":5038441,"avatar_url":"https://avatars.githubusercontent.com/u/5038441?v=3","gravatar_id":"","url":"https://api.github.com/users/Particular","html_url":"https://github.com/Particular","followers_url":"https://api.github.com/users/Particular/followers","following_url":"https://api.github.com/users/Particular/following{/other_user}","gists_url":"https://api.github.com/users/Particular/gists{/gist_id}","starred_url":"https://api.github.com/users/Particular/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Particular/subscriptions","organizations_url":"https://api.github.com/users/Particular/orgs","repos_url":"https://api.github.com/users/Particular/repos","events_url":"https://api.github.com/users/Particular/events{/privacy}","received_events_url":"https://api.github.com/users/Particular/received_events","type":"Organization","site_admin":false},"repo":{"id":1056713,"name":"NServiceBus","full_name":"Particular/NServiceBus","owner":{"login":"Particular","id":5038441,"avatar_url":"https://avatars.githubusercontent.com/u/5038441?v=3","gravatar_id":"","url":"https://api.github.com/users/Particular","html_url":"https://github.com/Particular","followers_url":"https://api.github.com/users/Particular/followers","following_url":"https://api.github.com/users/Particular/following{/other_user}","gists_url":"https://api.github.com/users/Particular/gists{/gist_id}","starred_url":"https://api.github.com/users/Particular/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Particular/subscriptions","organizations_url":"https://api.github.com/users/Particular/orgs","repos_url":"https://api.github.com/users/Particular/repos","events_url":"https://api.github.com/users/Particular/events{/privacy}","received_events_url":"https://api.github.com/users/Particular/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Particular/NServiceBus","description":"The most popular service bus for .NET","fork":false,"url":"https://api.github.com/repos/Particular/NServiceBus","forks_url":"https://api.github.com/repos/Particular/NServiceBus/forks","keys_url":"https://api.github.com/repos/Particular/NServiceBus/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Particular/NServiceBus/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Particular/NServiceBus/teams","hooks_url":"https://api.github.com/repos/Particular/NServiceBus/hooks","issue_events_url":"https://api.github.com/repos/Particular/NServiceBus/issues/events{/number}","events_url":"https://api.github.com/repos/Particular/NServiceBus/events","assignees_url":"https://api.github.com/repos/Particular/NServiceBus/assignees{/user}","branches_url":"https://api.github.com/repos/Particular/NServiceBus/branches{/branch}","tags_url":"https://api.github.com/repos/Particular/NServiceBus/tags","blobs_url":"https://api.github.com/repos/Particular/NServiceBus/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Particular/NServiceBus/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Particular/NServiceBus/git/refs{/sha}","trees_url":"https://api.github.com/repos/Particular/NServiceBus/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Particular/NServiceBus/statuses/{sha}","languages_url":"https://api.github.com/repos/Particular/NServiceBus/languages","stargazers_url":"https://api.github.com/repos/Particular/NServiceBus/stargazers","contributors_url":"https://api.github.com/repos/Particular/NServiceBus/contributors","subscribers_url":"https://api.github.com/repos/Particular/NServiceBus/subscribers","subscription_url":"https://api.github.com/repos/Particular/NServiceBus/subscription","commits_url":"https://api.github.com/repos/Particular/NServiceBus/commits{/sha}","git_commits_url":"https://api.github.com/repos/Particular/NServiceBus/git/commits{/sha}","comments_url":"https://api.github.com/repos/Particular/NServiceBus/comments{/number}","issue_comment_url":"https://api.github.com/repos/Particular/NServiceBus/issues/comments{/number}","contents_url":"https://api.github.com/repos/Particular/NServiceBus/contents/{+path}","compare_url":"https://api.github.com/repos/Particular/NServiceBus/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Particular/NServiceBus/merges","archive_url":"https://api.github.com/repos/Particular/NServiceBus/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Particular/NServiceBus/downloads","issues_url":"https://api.github.com/repos/Particular/NServiceBus/issues{/number}","pulls_url":"https://api.github.com/repos/Particular/NServiceBus/pulls{/number}","milestones_url":"https://api.github.com/repos/Particular/NServiceBus/milestones{/number}","notifications_url":"https://api.github.com/repos/Particular/NServiceBus/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Particular/NServiceBus/labels{/name}","releases_url":"https://api.github.com/repos/Particular/NServiceBus/releases{/id}","deployments_url":"https://api.github.com/repos/Particular/NServiceBus/deployments","created_at":"2010-11-06T12:54:52Z","updated_at":"2016-11-18T05:37:45Z","pushed_at":"2016-11-21T16:13:47Z","git_url":"git://github.com/Particular/NServiceBus.git","ssh_url":"git@github.com:Particular/NServiceBus.git","clone_url":"https://github.com/Particular/NServiceBus.git","svn_url":"https://github.com/Particular/NServiceBus","homepage":"https://particular.net/nservicebus","size":177391,"stargazers_count":1021,"watchers_count":1021,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":526,"mirror_url":null,"open_issues_count":203,"forks":526,"open_issues":203,"watchers":1021,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300"},"html":{"href":"https://github.com/Particular/NServiceBus/pull/4300"},"issue":{"href":"https://api.github.com/repos/Particular/NServiceBus/issues/4300"},"comments":{"href":"https://api.github.com/repos/Particular/NServiceBus/issues/4300/comments"},"review_comments":{"href":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300/comments"},"review_comment":{"href":"https://api.github.com/repos/Particular/NServiceBus/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Particular/NServiceBus/pulls/4300/commits"},"statuses":{"href":"https://api.github.com/repos/Particular/NServiceBus/statuses/5056ba97f1ca4083129f1acf05d824ef78c0b456"}}}}
{"actor":{"display_login":"timbussmann"}}