Как я могу избежать этого тупика в DevForce при использовании перехватчиков

Недавно мы начали использовать нашу бизнес-логику, которая в значительной степени опирается на DevForce, и выставлять ее через веб-API. Мы были очень осторожны, чтобы избежать проблем с потоками, гарантируя, что каждый запрос имеет свой собственный набор сущностей, свой собственный EntityManagerи т. д. Однако мы начали замечать логические взаимоблокировки (в коде.net, а не в SQL) при большом количестве одновременных запросов.

Я отследил проблему с блокировкой, которая выполняется PropertyInterceptors, Мы используем их довольно широко и имеем случаи, когда перехватчик для одного свойства (свойство A) будет устанавливать другое свойство (свойство B), но обратное также верно (настройка B также устанавливает A). Точные причины для некоторых из этих случаев сложны, но основная идея заключается в том, что у нас есть некоторые свойства, которые мы хотим синхронизировать. Кажется, что есть замок внутри PropertyInterceptor логика, поэтому мы можем легко зайти в тупик, потому что порядок, в котором эти блокировки взяты, может варьироваться.

Ниже я создал простой воспроизводимый случай, который включает в себя сущность с двумя свойствами. Один из них является целочисленным свойством, а другой - строковым. я имею BeforeSet логика, чтобы держать эти два в синхронизации друг с другом. В простом случае настройки свойств по одному, все работает. Но так как мы имеем дело с веб-API, очень часто параллельные процессы выполняются. Если мы получим один запрос, который случится установить IntValue и еще один запрос, который случается установить StringValueМы зашли в тупик. Это правда, хотя мы говорим о двух разных сущностях в двух разных EntityManagers. С нашей точки зрения, мы все делаем потокобезопасным образом, но тогда у DevForce есть некоторые очень долговечные блокировки, которые, как мы знаем, могут быть опасными.

Вот код, который, надеюсь, объясняет вещи. Имейте в виду, что наш реальный код намного сложнее, но основной тупик такой же:

public static void ReproduceDeadlock()
{
    var e1 = new MyEntity();
    var e2 = new MyEntity();

    //This works - settings fields one at a time is fine
    e1.IntValue = 1;
    e2.StringValue = "2";

    //But if we introduce some concurrency, we'll become deadlocked
    Task.Run(() =>
    {
        //Wait a bit so e1.IntValue has a chance to start
        Thread.Sleep(1000);

        e2.StringValue = "22";
    });

    e1.IntValue = 11;

    //Execution will never make it hear...setting the IntValue will never complete
}

public class MyEntity : Entity
{
    [BeforeSet("StringValue")]
    public void BeforeSetStringValue(PropertyInterceptorArgs<MyEntity, string> args)
    {
        //When the string is set, 'sync' it to the IntValue property
        IntValue = int.Parse(args.Value);
    }

    [BeforeSet("IntValue")]
    public void BeforeSetIntValue(PropertyInterceptorArgs<MyEntity, int> args)
    {
        //When the int is set, 'sync' it to the StringValue property

        //Introduce a delay so the deadlock will obviously happen.  In our real app, we don't have
        //  a Thread.Sleep() but we do have non-trivial logic that can cause just enough delay for the deadlock
        //  to happen sometimes
        Thread.Sleep(2000);
        StringValue = args.Value.ToString();
    }

    #region PropertyMetadata stuff

    public class PropertyMetadata
    {
        public static readonly DataEntityProperty<MyEntity, string> StringValue =
            new DataEntityProperty<MyEntity, string>("StringValue", true, false,
                ConcurrencyStrategy.None, false, null,
                false);

        public static readonly DataEntityProperty<MyEntity, int> IntValue =
            new DataEntityProperty<MyEntity, int>("IntValue", true, false,
                ConcurrencyStrategy.None, false, null,
                false);
    }

    public string StringValue
    {
        get { return PropertyMetadata.StringValue.GetValue(this); }
        set { PropertyMetadata.StringValue.SetValue(this, value); }
    }

    public int IntValue
    {
        get { return PropertyMetadata.IntValue.GetValue(this); }
        set { PropertyMetadata.IntValue.SetValue(this, value); }
    }

    #endregion
}

}

1 ответ

Стивен, возможно у меня есть обходной путь для тебя. В действиях перехватчика вы можете использовать SetValueRaw, чтобы синхронизировать значение с другим свойством и избежать прохождения через его перехватчик (и проверку). Этот метод доступен в общедоступном интерфейсе IStructuralObject, который, хотя и задокументирован только для внутреннего использования, мы не планируем изменять. Оба класса EntityAspect и ComplexAspect реализуют этот интерфейс.

Итак, ваш пример будет выглядеть так:

[BeforeSet("StringValue")]
public void BeforeSetStringValue(PropertyInterceptorArgs<MyEntity, string> args)
{
    //When the string is set, 'sync' it to the IntValue property
    (this.EntityAspect as IStructuralObject).SetValueRaw(PropertyMetadata.IntValue, int.Parse(args.Value));
}

[BeforeSet("IntValue")]
public void BeforeSetIntValue(PropertyInterceptorArgs<MyEntity, int> args)
{
    //When the int is set, 'sync' it to the StringValue property

    //Introduce a delay so the deadlock will obviously happen.  In our real app, we don't have
    //  a Thread.Sleep() but we do have non-trivial logic that can cause just enough delay for the deadlock
    //  to happen sometimes
    Thread.Sleep(2000);

    (this.EntityAspect as IStructuralObject).SetValueRaw(PropertyMetadata.StringValue, args.Value.ToString());
}

Я отмечу еще один обходной путь. Блокировка находится в перехватчике, но вы можете использовать всю обычную логику проверки и получать уведомления об изменениях, просто без уровня перехвата. Один из способов сделать это - установить для флага EntityGroup.PropertyInterceptionEnabled значение false, но обычно включать и отключать его обычно довольно неуклюже. Другой вариант - это вспомогательная функция, которая делает именно то, что делает SetterInterceptor:

public static void SetValueWithVerification(IStructuralObject so, DataEntityProperty property, object newValue)
{
    if (so.VerifierEngine != null && so.VerifierEngine.Enabled && so.EntityGroup.VerificationEnabled)
    {
        if ((property.MemberMetadata.VerifierSetterOptions & VerifierSetterOptions.BeforeSet) > 0)
        {
            so.ValidatePropertyBeforeSet(property, newValue);
        }

        so.SetValueWithChangeNotification(property, newValue);

        if ((property.MemberMetadata.VerifierSetterOptions & VerifierSetterOptions.AfterSet) > 0)
        {
            so.ValidatePropertyAfterSet(property, newValue);
        }
    }
    else
    {
        so.SetValueWithChangeNotification(property, newValue);
    }
}

Затем вызовите это в этих тесно связанных действиях перехватчика:

SetValueWithVerification(this.EntityAspect, PropertyMetadata.StringValue, args.Value.ToString());
Другие вопросы по тегам