views:

80

answers:

3

Hallo,

I have web service that has multiple methods that can be called. Each time one of these methods is called I am logging the call to a statistics database so we know how many times each method is called each month and the average process time.

Each time I log statistic data I first check the database to see if that method for the current month already exists, if not the row is created and added. If it already exists I update the needed columns to the database.

My problem is that sometimes when I update a row I get the "Row not found or changed" exception and yes I know it is because the row has been modified since I read it.

To solve this I have tried using the following without success:

  • Use using around my datacontext.
  • Use using around a TransactionScope.
  • Use a mutex, this doesn’t work because the web service is (not sure I am calling it the right think) replicated out on different PC for performance but still using the same database.
  • Resolve concurrency conflict in the exception, this doesn’t work because I need to get the new database value and add a value to it.

Below I have added the code used to log the statistics data. Any help would be appreciated very much.

public class StatisticsGateway : IStatisticsGateway
{
    #region member variables
    private StatisticsDataContext db;
    #endregion

    #region Singleton
    [ThreadStatic]
    private static IStatisticsGateway instance;
    [ThreadStatic]
    private static DateTime lastEntryTime = DateTime.MinValue;

    public static IStatisticsGateway Instance
    {
        get
        {
            if (!lastEntryTime.Equals(OperationState.EntryTime) || instance == null)
            {
                instance = new StatisticsGateway();
                lastEntryTime = OperationState.EntryTime;
            }

            return instance;
        }
    }
    #endregion

    #region constructor / initialize
    private StatisticsGateway()
    {
        var configurationAppSettings = new System.Configuration.AppSettingsReader();
        var connectionString = ((string)(configurationAppSettings.GetValue("sqlConnection1.ConnectionString", typeof(string))));

        db = new StatisticsDataContext(connectionString);
    }
    #endregion

    #region IStatisticsGateway members
    public void AddStatisticRecord(StatisticRecord record)
    {
        using (db)
        {
            var existing = db.Statistics.SingleOrDefault(p => p.MethodName == record.MethodName &&
                                                              p.CountryID == record.CountryID &&
                                                              p.TokenType == record.TokenType &&
                                                              p.Year == record.Year &&
                                                              p.Month == record.Month);

            if (existing == null)
            {
                //Add new row
                this.AddNewRecord(record);
                return;
            }

            //Update
            existing.Count += record.Count;
            existing.TotalTimeValue += record.TotalTimeValue;

            db.SubmitChanges();
        }
    }
A: 

I would suggest letting SQL Server deal with the concurrency.

Here's how:

  1. Create a stored procedure that accepts your log values (method name, month/date, and execution statistics) as arguments.

  2. In the stored procedure, before anything else, get an application lock as described here, and here. Now you can be sure only one instance of the stored procedure will be running at once. (Disclaimer! I have not tried sp_getapplock myself. Just saying. But it seems fairly straightforward, given all the examples out there on the interwebs.)

  3. Next, in the stored procedure, query the log table for a current-month's entry for the method to determine whether to insert or update, and then do the insert or update.

  4. As you may know, in VS you can drag stored procedures from the Server Explorer into the DBML designer for easy access with LINQ to SQL.

If you're trying to avoid stored procedures then this solution obviously won't be for you, but it's how I'd solve it easily and quickly. Hope it helps!

shaunmartin
Thanks for the replies. I solved the problem by creating a stored procedure! Works like a charm
Michael
Excellent! Just curious, did you bother with sp_getapplock in your stored procedure? (Since just moving the logic to a stored procedure would greatly reduce the chance of a concurrency problem.)
shaunmartin
I used HOLDLOCK and ROWLOCK, check my answer.
Michael
A: 

If you don't want to use the stored procedure approach, a crude way of dealing with it would simply be retrying on that specific exception. E.g:

int maxRetryCount = 5;
for (int i = 0; i < maxRetryCount; i++)
{
   try
   {
     QueryAndUpdateDB();
     break;
   }
   catch(RowUpdateException ex)
   {
     if (i == maxRetryCount) throw;
   }
}
rasmusvhansen
A: 

I have not used the sp_getapplock, instead I have used HOLDLOCK and ROWLOCK as seen below:

CREATE PROCEDURE [dbo].[UpdateStatistics] 
@MethodName as varchar(50) = null,
@CountryID as varchar(2) = null,
@TokenType as varchar(5) = null,
@Year as int,
@Month as int,
@Count bigint,
@TotalTimeValue bigint 

AS BEGIN SET NOCOUNT ON;

BEGIN TRAN

    UPDATE dbo.[Statistics]
    WITH (HOLDLOCK, ROWLOCK)
    SET Count = Count + @Count
    WHERE MethodName=@MethodName and CountryID=@CountryID and TokenType=@TokenType and Year=@Year and Month=@Month
    IF @@ROWCOUNT=0
        INSERT INTO dbo.[Statistics] (MethodName, CountryID, TokenType, TotalTimeValue, Year, Month, Count) values (@MethodName, @CountryID, @TokenType, @TotalTimeValue, @Year, @Month, @Count)

COMMIT TRAN

END GO

I have tested it by calling my web service methods by multiple threads simultaneous and each call is logged without any problems.

Michael