-1

Ok. I completely rewrote my question.

Database table

CREATE TABLE [dbo].[IntegrationCommandLog](
    [Id] [uniqueidentifier] NOT NULL,
    [Content] [nvarchar](max) NULL,
    [OrderingKey] [bigint] IDENTITY(1,1) NOT NULL,
    [RowVersion] [timestamp] NULL,
    [Topic] [nvarchar](13) NULL,
 CONSTRAINT [PK_IntegrationCommandLog] PRIMARY KEY NONCLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

CREATE CLUSTERED INDEX [IX_IntegrationCommandLog_OrderingKey] ON [dbo].[IntegrationCommandLog]
(
    [OrderingKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

Reproducer. Run release without attached debugger is required

Required packages:

Install-Package Dapper
Install-Package System.Data.SqlClient

Code:

using Dapper;
using System;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        private const string Sql = "SELECT * FROM dbo.IntegrationCommandLog WHERE OrderingKey > @OrderingKey ORDER BY OrderingKey";
        private const string cs = "Data Source=.;Initial Catalog=Test;Integrated Security=True";

        static void Main(string[] args)
        {
            Task.Run(() => Query());

            var tasks = new Task[200];
            for (int i = 0; i < tasks.Length; ++i)
                tasks[i] = Task.Run(() => Insert());

            while (true)
            {
                int j = Task.WaitAny(tasks);
                tasks[j] = Task.Run(() => Insert());
            }
        }

        private async static Task Query()
        {
            long last = -1;
            var connection = new SqlConnection(cs);
            await connection.OpenAsync();
            while (true)
            {
                var entries = await connection.QueryAsync<IntegrationLogEntry>(Sql, new { OrderingKey = last });

                Console.WriteLine(entries.Count());
                if (entries.Any())
                {
                    last = entries.Aggregate((e1, e2) =>
                    {
                        if (e1.OrderingKey + 1 != e2.OrderingKey)
                            Console.WriteLine($"Sequence violation {e1.OrderingKey} {e2.OrderingKey}");

                        return e2;
                    }).OrderingKey;
                }
                await Task.Delay(1000);
            }
        }

        private static async Task Insert()
        {
            string sql = @"SET NOCOUNT ON;
INSERT INTO [dbo].[IntegrationCommandLog] ([Id], [Content], [Topic])
VALUES ( @Id, @Content, @Topic);
SELECT [OrderingKey], [RowVersion]
FROM [dbo].[IntegrationCommandLog]
WHERE @@ROWCOUNT = 1 AND [Id] = @Id";

            var content = new string('a', 1000);
            using (var connection = new SqlConnection(cs))
            {
                await connection.OpenAsync();
                await connection.ExecuteAsync(sql, new { Id = Guid.NewGuid(), Content = content, Topic = "SomeTopic" });
            }
        }
    }

    public class IntegrationLogEntry
    {
        public Guid Id { get; private set; }
        public string Content { get; private set; }
        public string Topic { get; private set; }
        public long OrderingKey { get; private set; }
        public byte[] RowVersion { get; set; }
    }
}

Make sure there are no gaps

SELECT top 100 * FROM (SELECT *, rowid = ROW_NUMBER() OVER (ORDER BY OrderingKey) FROM [dbo].[IntegrationCommandLog]) l1
JOIN (SELECT *, rowid = ROW_NUMBER() OVER (ORDER BY OrderingKey) FROM [dbo].[IntegrationCommandLog]) l2 on l1.rowid + 1 = l2.rowid
WHERE l1.OrderingKey + 1 != l2.OrderingKey

Output

Console output

Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) Oct 28 2016 18:17:30 Copyright (c) Microsoft Corporation Developer Edition (64-bit) on Windows Server 2016 Datacenter 6.3 (Build 14393: ) (Hypervisor)

Questions:

  1. Why the query does not return all entries?
  2. How to get correct list of entries?
Alew
  • 316
  • 4
  • 12
  • ok, what values do you see for @Prev and @After? The query has "AND OrderingKey > @OrderingKey" so we should expect to have various Keys, and the testing signals error when "!= " not equal... – donPablo Sep 18 '18 at 22:57
  • I added .net poco class which represents the query result in my app code. OrderingKey is long. – Alew Sep 18 '18 at 23:13
  • Last error contains two gaps 1:@prev=17198 @after=17213 2:@prev=17214 @after=17218 – Alew Sep 18 '18 at 23:21
  • Can there be later changes to the topic? It is the only other thing in the Where. – donPablo Sep 18 '18 at 23:58
  • Possible duplicate of [Identity increment is jumping in SQL Server database](https://stackoverflow.com/questions/14146148/identity-increment-is-jumping-in-sql-server-database) – Igor Sep 19 '18 at 00:21
  • Checked it twice. No changing code, no update statements in a profiler. Removed topic from where clause but problem still exists. – Alew Sep 19 '18 at 00:32
  • 2
    "My log contains error messages" **What messages?** Show us what the error was. – Joel Coehoorn Sep 19 '18 at 00:40
  • @JoelCoehoorn - I think the OP is referring to the log record bring written `Log.Error("Sequence violation...`, not an actual error being emitted. – Igor Sep 19 '18 at 00:47
  • It could be a race condition, a record not committed but where the is has been assigned from the cach of numbers. If you want more help you will have to provide an [mcve]. – Igor Sep 19 '18 at 01:06
  • Any analysis via SMSS of the rows with OrderingKey 2:@prev=17214 @after=17218 both ends and inbetween rows? What might be special about these? Present in SMSS, absent in separate dedicated thread? Both pointing at same instance and db? Row locks/page locks? – donPablo Sep 19 '18 at 03:27

1 Answers1

0

e1.OrderingKey + 1 != e2.OrderingKey tests that e2 is only 1 higher than the previous key. That isn't guaranteed by the database.

There are many things that will cause non-consecutive IDENTITY: (not forgetting deletes)

https://learn.microsoft.com/en-us/sql/t-sql/statements/create-table-transact-sql-identity-property?view=sql-server-2017

Consecutive values within a transaction – A transaction inserting multiple rows is not guaranteed to get consecutive values for the rows because other concurrent inserts might occur on the table. If values must be consecutive then the transaction should use an exclusive lock on the table or use the SERIALIZABLE isolation level.

Consecutive values after server restart or other failures – SQL Server might cache identity values for performance reasons and some of the assigned values can be lost during a database failure or server restart. This can result in gaps in the identity value upon insert. If gaps are not acceptable then the application should use its own mechanism to generate key values. Using a sequence generator with the NOCACHE option can limit the gaps to transactions that are never committed.

Reuse of values – For a given identity property with specific seed/increment, the identity values are not reused by the engine. If a particular insert statement fails or if the insert statement is rolled back then the consumed identity values are lost and will not be generated again. This can result in gaps when the subsequent identity values are generated.

AaronHolland
  • 1,595
  • 1
  • 16
  • 32
  • Does it mean that sql server can fill gaps in the sequence later after the query was executed? I dont think this is possible. – Alew Sep 18 '18 at 23:35
  • I checked every error message. I'm not able to find any gap – Alew Sep 18 '18 at 23:38
  • I understand that there may be gaps, but that is not the case. This check was added to track down why I lose messages – Alew Sep 18 '18 at 23:47