Base solution for your next web application
Open Closed

Concurrency control on a table #4187


User avatar
0
soonjoo created

Hello team,

I have a table that is similar to Organization Units, where there is a code column to store the sequence of the record. So for each record inserted under a root, it will be 00001.00001, 00001.00002, 00001.00003, 00001.00004 etc... however, if I call my insert API multiple times in quick succession, the code wont be sequential... meaning it will 00001.00001, 00001.00001, 00001.00001, 00001.00002 is there a way to control the concurrency on a table?

Thank you


4 Answer(s)
  • User Avatar
    0
    aaron created
    Support Team

    Concurrency control is usually about reading or writing the same record (Optimistic Locking: #4146@6c148c17-0f2f-42c9-a386-dd70810dd348).

    In your case, you are inserting a new record. Can you show related code on how you determine and set the next "code"?

    Looks like you need to implement a singleton lock.

  • User Avatar
    0
    soonjoo created

    Hello aaron, thank you for your reply,

    my entity is Sample, this is my application service to create a new Sample

    public async Task<SampleDto> CreateNewSample(CreateNewSampleInput input)
            {
                var sample = ObjectMapper.Map<Sample>(input);
                var insertedSample = await _sampleManager.CreateNewSample(sample);
    
                return ObjectMapper.Map<SampleDto>(insertedSample);
            }
    

    This is the sample manager (i left out some unrelated business logic) Sample's Line is similar to the Organization Unit's Code

    public async Task<Sample> CreateNewSample(Sample sample)
            {
         
                ....
    
                sample.Line = await GetNextChildCodeAsync(sample.ParentSampleId);
    
                ....
    
                var insertedSample = await _sampleRepository.InsertAsync(sample);
                await CurrentUnitOfWork.SaveChangesAsync();
    
                return insertedSample;
            }
    

    This is taken from organization unit manager's codes

    public virtual async Task<string> GetNextChildCodeAsync(long? parentId)
            {
                var lastChild = await GetLastChildOrNullAsync(parentId);
                if (lastChild == null)
                {
                    var parentCode = parentId != null ? await GetCodeAsync(parentId.Value) : null;
                    return Sample.AppendCode(parentCode, Sample.CreateCode(1));
                }
    
                return Sample.CalculateNextCode(lastChild.Line);
            }
    
            public virtual async Task<Sample> GetLastChildOrNullAsync(long? parentId)
            {
                var children = await _sampleRepository.GetAllListAsync(ou => ou.ParentSampleId == parentId);
                return children.OrderBy(c => c.Line).LastOrDefault();
            }
    
            public virtual async Task<string> GetCodeAsync(long id)
            {
                return (await _sampleRepository.GetAsync(id)).Line;
            }
    

    this is my sample entity model, most of it taken from organization unit

    public class Sample : FullAuditedEntity<long>, IMustHaveTenant
        {
    
            /// <summary>
            /// Maximum depth of an UO hierarchy.
            /// </summary>
            public const int MaxDepth = 16;
    
            /// <summary>
            /// Length of a code unit between dots.
            /// </summary>
            public const int CodeUnitLength = 6;
    
            /// <summary>
            /// Maximum length of the <see cref="Line"/> property.
            /// </summary>
            public const int MaxCodeLength = MaxDepth * (CodeUnitLength + 1) - 1;
    
    
            public Sample()
            {
                this.ChildSamples = new HashSet<Sample>();
            }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="Sample"/> class.
            /// </summary>
            /// <param name="tenantId">Tenant's Id or null for host.</param>
            /// <param name="sampleDefinitionId">Sample Definition Id.</param>
            /// <param name="parentId">Parent's Id or null if OU is a root.</param>
            public Sample(int tenantId, long? sampleDefinitionId, long? parentId = null)
            {
                TenantId = tenantId;
                SampleDefinitionId = sampleDefinitionId;
                ParentSampleId = parentId;
            }
    
             .....
    
            [ForeignKey("ParentSampleId")]
            public Sample ParentSample { get; set; }
            public long? ParentSampleId { get; set; }
    
           .....
    
            /// <summary>
            /// Hierarchical Lineage of this sample.
            /// Example: "00001.00042.00005".
            /// It's changeable if OU hierarch is changed.
            /// </summary>
            [StringLength(MaxCodeLength)]
            public string Line { get; set; }
    
           .....
    
            public virtual ICollection<Sample> ChildSamples { get; set; }
    
            public int TenantId { get; set; }
    
    
            /// <summary>
            /// Gets user friendly code to be displayed.
            /// Example: if code = "00019.00055.00001" then returns "19.55.1".
            /// </summary>
            /// <param name="code">The code.</param>
            /// <param name="parentCode">The parent code.</param>
            public static string GetUserFriendlyCode(string code)
            {
                List<string> friendlyCodeList = new List<string>();
    
                var codeList = code.Split(".");
                foreach (var codePart in codeList)
                {
                    friendlyCodeList.Add(codePart.TrimStart('0'));
                }
    
                return String.Join(".", friendlyCodeList);
            }
    
            /// <summary>
            /// Creates code for given numbers.
            /// Example: if numbers are 4,2 then returns "00004.00002";
            /// </summary>
            /// <param name="numbers">Numbers</param>
            public static string CreateCode(params int[] numbers)
            {
                if (numbers.IsNullOrEmpty())
                {
                    return null;
                }
    
                return numbers.Select(number => number.ToString(new string('0', CodeUnitLength))).JoinAsString(".");
            }
    
            /// <summary>
            /// Appends a child code to a parent code. 
            /// Example: if parentCode = "00001", childCode = "00042" then returns "00001.00042".
            /// </summary>
            /// <param name="parentCode">Parent code. Can be null or empty if parent is a root.</param>
            /// <param name="childCode">Child code.</param>
            public static string AppendCode(string parentCode, string childCode)
            {
                if (childCode.IsNullOrEmpty())
                {
                    throw new ArgumentNullException(nameof(childCode), "childCode can not be null or empty.");
                }
    
                if (parentCode.IsNullOrEmpty())
                {
                    return childCode;
                }
    
                return parentCode + "." + childCode;
            }
    
            /// <summary>
            /// Gets relative code to the parent.
            /// Example: if code = "00019.00055.00001" and parentCode = "00019" then returns "00055.00001".
            /// </summary>
            /// <param name="code">The code.</param>
            /// <param name="parentCode">The parent code.</param>
            public static string GetRelativeCode(string code, string parentCode)
            {
                if (code.IsNullOrEmpty())
                {
                    throw new ArgumentNullException(nameof(code), "code can not be null or empty.");
                }
    
                if (parentCode.IsNullOrEmpty())
                {
                    return code;
                }
    
                if (code.Length == parentCode.Length)
                {
                    return null;
                }
    
                return code.Substring(parentCode.Length + 1);
            }
    
            /// <summary>
            /// Calculates next code for given code.
            /// Example: if code = "00019.00055.00001" returns "00019.00055.00002".
            /// </summary>
            /// <param name="code">The code.</param>
            public static string CalculateNextCode(string code)
            {
                if (code.IsNullOrEmpty())
                {
                    throw new ArgumentNullException(nameof(code), "code can not be null or empty.");
                }
    
                var parentCode = GetParentCode(code);
                var lastUnitCode = GetLastUnitCode(code);
    
                return AppendCode(parentCode, CreateCode(Convert.ToInt32(lastUnitCode) + 1));
            }
    
            /// <summary>
            /// Gets the last unit code.
            /// Example: if code = "00019.00055.00001" returns "00001".
            /// </summary>
            /// <param name="code">The code.</param>
            public static string GetLastUnitCode(string code)
            {
                if (code.IsNullOrEmpty())
                {
                    throw new ArgumentNullException(nameof(code), "code can not be null or empty.");
                }
    
                var splittedCode = code.Split('.');
                return splittedCode[splittedCode.Length - 1];
            }
    
            /// <summary>
            /// Gets parent code.
            /// Example: if code = "00019.00055.00001" returns "00019.00055".
            /// </summary>
            /// <param name="code">The code.</param>
            public static string GetParentCode(string code)
            {
                if (code.IsNullOrEmpty())
                {
                    throw new ArgumentNullException(nameof(code), "code can not be null or empty.");
                }
    
                var splittedCode = code.Split('.');
                if (splittedCode.Length == 1)
                {
                    return null;
                }
    
                return splittedCode.Take(splittedCode.Length - 1).JoinAsString(".");
            }
        }
    

    I notice that whenever the CreateNewSample api is called multiple times with the same parent id, the Line doesn't set correctly..

  • User Avatar
    0
    aaron created
    Support Team

    Yes, so it is currently possible for the next code to be calculated before an insert is completed.

    A quick fix is to lock with a private static _syncObj:

    private static readonly object _syncObj = new object();
    
    public async Task<Sample> CreateNewSample(Sample sample)
    {
        // ...
    
        lock (_syncObj)
        {
            AsyncHelper.RunSync(async () =>
            {
                sample.Line = await GetNextChildCodeAsync(sample.ParentSampleId);
    
                // ...
    
                var insertedSample = await _sampleRepository.InsertAsync(sample);
                await CurrentUnitOfWork.SaveChangesAsync();
            }
        }
    }
    

    Note that:

    • You must run synchronous code in lock.
    • Requests cannot no longer run in parallel.

    A better alternative is to retrieve the next code from a singleton cache that has a lock, like in UserFriendsCache. Then, you can store the next code as a variable in the cache instead of reading from the database every time. You can try to implement that yourself.

  • User Avatar
    0
    soonjoo created

    Thank you for your help, I will try the first method, the second method seems hard, I'm not sure how to store the next code in cache, because the next code is not always in a sequence as it might be under a different parent