Skip to content

Commit 13f878f

Browse files
authored
Merge pull request #223 from servicetitan/DelayedQueryCaptureTest
Issue: Cached query captures object in closure
2 parents 29d716d + ba7648e commit 13f878f

File tree

3 files changed

+130
-8
lines changed

3 files changed

+130
-8
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (C) 2022 Xtensive LLC.
2+
// This code is distributed under MIT license terms.
3+
// See the License.txt file in the project root for more information.
4+
5+
using System;
6+
using System.Collections;
7+
using System.Diagnostics;
8+
using System.Linq;
9+
using System.Linq.Dynamic;
10+
using System.Reflection;
11+
using System.Threading;
12+
using NUnit.Framework;
13+
using Xtensive.Caching;
14+
using Xtensive.Orm.Configuration;
15+
using Xtensive.Orm.Tests.Issues.IssueGithub0224_DelayedQueryCapture_Model;
16+
17+
namespace Xtensive.Orm.Tests.Issues
18+
{
19+
namespace IssueGithub0224_DelayedQueryCapture_Model
20+
{
21+
[HierarchyRoot]
22+
class Item : Entity
23+
{
24+
[Field, Key]
25+
public int Id { get; private set; }
26+
27+
[Field]
28+
public int Tag { get; set; }
29+
}
30+
}
31+
32+
[Serializable]
33+
public class IssueGithub0224_DelayedQueryCapture : AutoBuildTest
34+
{
35+
public class OtherService
36+
{
37+
public static volatile int InstanceCount;
38+
39+
public int N;
40+
41+
public OtherService()
42+
{
43+
Interlocked.Increment(ref InstanceCount);
44+
}
45+
46+
~OtherService()
47+
{
48+
Interlocked.Decrement(ref InstanceCount);
49+
}
50+
}
51+
52+
53+
protected override DomainConfiguration BuildConfiguration()
54+
{
55+
var config = base.BuildConfiguration();
56+
config.Types.Register(typeof(Item).Assembly, typeof(Item).Namespace);
57+
return config;
58+
}
59+
60+
[Test]
61+
public void DelayedQueryCapture()
62+
{
63+
using (var session = Domain.OpenSession())
64+
using (var t = session.OpenTransaction()) {
65+
var item = new Item() { Tag = 10 };
66+
DelayedQuery(session);
67+
t.Complete();
68+
}
69+
GC.Collect();
70+
Thread.Sleep(1000);
71+
GC.Collect();
72+
Assert.AreEqual(0, OtherService.InstanceCount);
73+
}
74+
75+
private void DelayedQuery(Session session)
76+
{
77+
var ids = new[] { 1, 2 };
78+
var otherService = new OtherService();
79+
80+
var items = session.Query.CreateDelayedQuery(q =>
81+
from t in q.All<Item>()
82+
where t.Id.In(ids)
83+
select t).ToArray();
84+
85+
var bb1 = items
86+
.Select(a => new {
87+
a.Id,
88+
A = new {
89+
B = otherService.N == a.Id
90+
},
91+
});
92+
}
93+
}
94+
}

Orm/Xtensive.Orm/Orm/Internals/CompiledQueryRunner.cs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2012-2021 Xtensive LLC.
1+
// Copyright (C) 2012-2022 Xtensive LLC.
22
// This code is distributed under MIT license terms.
33
// See the License.txt file in the project root for more information.
44
// Created by: Denis Krjuchkov
@@ -7,6 +7,7 @@
77
using System;
88
using System.Linq;
99
using System.Linq.Expressions;
10+
using System.Reflection;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Xtensive.Caching;
@@ -19,6 +20,8 @@ namespace Xtensive.Orm.Internals
1920
{
2021
internal class CompiledQueryRunner
2122
{
23+
private static readonly Func<FieldInfo, bool> FieldIsSimple = fieldInfo => IsSimpleType(fieldInfo.FieldType);
24+
2225
private readonly Domain domain;
2326
private readonly Session session;
2427
private readonly QueryEndpoint endpoint;
@@ -108,7 +111,7 @@ private DelayedQuery<TElement> CreateDelayedSequenceQuery<TElement>(
108111
private ParameterizedQuery GetScalarQuery<TResult>(
109112
Func<QueryEndpoint, TResult> query, bool executeAsSideEffect, out TResult result)
110113
{
111-
AllocateParameterAndReplacer();
114+
var cacheable = AllocateParameterAndReplacer();
112115

113116
var parameterContext = new ParameterContext(outerContext);
114117
parameterContext.SetValue(queryParameter, queryTarget);
@@ -123,7 +126,9 @@ private ParameterizedQuery GetScalarQuery<TResult>(
123126
throw new NotSupportedException(Strings.ExNonLinqCallsAreNotSupportedWithinQueryExecuteDelayed);
124127
}
125128

126-
PutCachedQuery(parameterizedQuery);
129+
if (cacheable) {
130+
PutCachedQuery(parameterizedQuery);
131+
}
127132
return parameterizedQuery;
128133
}
129134

@@ -135,24 +140,26 @@ private ParameterizedQuery GetSequenceQuery<TElement>(
135140
return parameterizedQuery;
136141
}
137142

138-
AllocateParameterAndReplacer();
143+
var cacheable = AllocateParameterAndReplacer();
139144
var scope = new CompiledQueryProcessingScope(queryParameter, queryParameterReplacer);
140145
using (scope.Enter()) {
141146
var result = query.Invoke(endpoint);
142147
var translatedQuery = endpoint.Provider.Translate(result.Expression);
143148
parameterizedQuery = (ParameterizedQuery) translatedQuery;
144149
}
145150

146-
PutCachedQuery(parameterizedQuery);
151+
if (cacheable) {
152+
PutCachedQuery(parameterizedQuery);
153+
}
147154
return parameterizedQuery;
148155
}
149156

150-
private void AllocateParameterAndReplacer()
157+
private bool AllocateParameterAndReplacer()
151158
{
152159
if (queryTarget == null) {
153160
queryParameter = null;
154161
queryParameterReplacer = new ExtendedExpressionReplacer(e => e);
155-
return;
162+
return true;
156163
}
157164

158165
var closureType = queryTarget.GetType();
@@ -192,6 +199,25 @@ private void AllocateParameterAndReplacer()
192199
}
193200
return null;
194201
});
202+
203+
return !TypeHelper.IsClosure(closureType)
204+
|| closureType.GetFields().All(FieldIsSimple);
205+
}
206+
207+
private static bool IsSimpleType(Type type)
208+
{
209+
var typeInfo = type.GetTypeInfo();
210+
if (typeInfo.IsGenericType) {
211+
var genericDef = typeInfo.GetGenericTypeDefinition();
212+
return (genericDef == WellKnownTypes.NullableOfT || genericDef.IsAssignableTo(WellKnownTypes.IReadOnlyListOfT))
213+
&& IsSimpleType(typeInfo.GetGenericArguments()[0]);
214+
}
215+
else if (typeInfo.IsArray) {
216+
return IsSimpleType(typeInfo.GetElementType());
217+
}
218+
else {
219+
return typeInfo.IsPrimitive || typeInfo.IsEnum || type == WellKnownTypes.String || type == WellKnownTypes.Decimal;
220+
}
195221
}
196222

197223
private ParameterizedQuery GetCachedQuery() =>

Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
// Copyright (C) 2020 Xtensive LLC.
1+
// Copyright (C) 2020-2022 Xtensive LLC.
22
// This code is distributed under MIT license terms.
33
// See the License.txt file in the project root for more information.
44

55
using System;
6+
using System.Collections.Generic;
67
using System.Linq;
78
using System.Linq.Expressions;
89
using System.Reflection;
@@ -73,6 +74,7 @@ internal static class WellKnownTypes
7374

7475
public static readonly Type ByteArray = typeof(byte[]);
7576
public static readonly Type ObjectArray = typeof(object[]);
77+
public static readonly Type IReadOnlyListOfT = typeof(IReadOnlyList<>);
7678

7779
public static readonly Type DefaultMemberAttribute = typeof(DefaultMemberAttribute);
7880
}

0 commit comments

Comments
 (0)