Детали реализации процесса активации Document ID в Sharepoint – часть 1

При активации фичи Document ID Service на уровне сайт коллекции Sharepoint добавляется поле Document ID в стандартный тип содержимого Документ. Основная проблема это то, что активация происходит асинхронно с помощью заданий таймера один раз в день (см. ниже), что вызывает проблемы в разработчиков и администраторов. На Sharepoint Online ускорить процесс проблематично, но в собственном окружении это возможно и в этой статье я покажу как это сделать и какие механизмы при этом используются. Добавим также, что на Sharepoint Online указанная фича доступна только при использовании Enterprise плана.

Прежде чем продолжать читать эту статью я настоятельно рекомендую ознакомится со следующим моим постом (пост на английском): Internal details of SPWorkItemJobDefinition or several reasons of why SPWorkItemJobDefinition doesn’t work. В нем описаны внутренние детали реализации рабочих элементов (work items) в Sharepoint, которые активно используются при активации Document ID Service. Так что мы должны хорошо понимать, как они работают прежде чем продолжить изучение фичи Document ID.

Рабочие элементы, которые непосредственно используются при активации, обрабатываются следующими заданиями таймера:

Название: Document ID enable/disable
Тип: Microsoft.Office.DocumentManagement.Internal.DocIdEnableWorkItemJobDefinition
Тип рабочего элемента: 749FED41-4F86-4277-8ECE-289FBF18884F
Описание: Рабочий элемент, который устанавливает изменения в тип содержимого во всех сайтах при изменении конфигурации Document ID
Расписание по умолчанию: Каждый день 21:30-21:45

Название: Document ID assignment
Тип: Microsoft.Office.DocumentManagement.Internal.DocIdWorkItemJobDefinition
Тип рабочего элемента: A83644F5-78DB-4F95-9A9D-25238862048C
Описание: Рабочий элемент, который присваивает Document ID ко всем элементам в сайт коллекции
Расписание по умолчанию: Каждый день 22:00-22:30

Оба задания устанавливаются при активации фичи DocumentManagement (Id = «3A4CE811-6FE0-4e97-A6AE-675470282CF2») со скопом WebApplication. Первое задание добавляет поля в типы содержимого, второе – устанавливает значение для указанных документов. Если вы не видите эти задания в Central administration > Monitoring > Job definitions, активируйте DocumentManagement через PowerShell с указанием флага force. Также обратите внимание на типы рабочих элементов (749FED41-4F86-4277-8ECE-289FBF18884F и A83644F5-78DB-4F95-9A9D-25238862048C) – они скоро нам понадобятся.

Прежде всего нам нужно активизировать фичу “Document ID Service” в Настройках сайта > Компоненты сайт коллекции (Site settings > Site collection features). Давайте посмотрим, что происходит в обработчике фичи Microsoft.Office.DocumentManagement.DocIdFeatureReceiver во время активации и деактивации:

public override void FeatureActivated(SPFeatureReceiverProperties receiverProperties)
{
    FeatureActivateDeactivate(receiverProperties, true);
}
 
public override void FeatureDeactivating(SPFeatureReceiverProperties receiverProperties)
{
    FeatureActivateDeactivate(receiverProperties, false);
}

Внутренний метод FeatureActivateDeactivate() показан ниже:

private static void FeatureActivateDeactivate(SPFeatureReceiverProperties
receiverProperties, bool fActivate)
{
    SPSite parent = receiverProperties.Feature.Parent as SPSite;
    ULS.SendTraceTag(0x61326d37, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.High,
        "Document ID/FeatureActivateDeactivate: Entering  with fActivate = {0}",
        new object[] { fActivate });
    DocumentId.EnableAssignment(parent, null, fActivate, fActivate, false, false);
    ULS.SendTraceTag(0x61326d38, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.High,
        "Document ID/FeatureActivateDeactivate: Leaving");
}

Давайте теперь посмотрим на метод DocumentId.EnableAssignment():

public static void EnableAssignment(SPSite site, string prefix, bool fEnable,
bool fScheduleAssignment, bool  fOverwriteExistingIds, bool fDocsetCtUpdateOnly)
{
    if (site == null)
    {
        throw new ArgumentNullException("site");
    }
    if ((site.RootWeb == null) || (site.RootWeb.ContentTypes == null))
    {
        throw new ArgumentException("site.RootWeb.ContentTypes");
    }
    if (fEnable && !IsFeatureEnabled(site))
    {
        throw new InvalidOperationException(
"Assignment cannot be enabled with the feature deactivated");
    }
    if (DocIdHelpers.IsSiteTooBig(site))
    {
        DocIdEnableWorkitem.ScheduleWorkitem(site, prefix, fEnable,
            fScheduleAssignment, fOverwriteExistingIds, fDocsetCtUpdateOnly);
    }
    else
    {
        DocIdEnableWorkItemJobDefinition.EnableAssignment(site, prefix, fEnable,
            fScheduleAssignment, fOverwriteExistingIds,  fDocsetCtUpdateOnly);
    }
    new DocIdUiSettings(fEnable, prefix).Save(site);
}

Итак сначала проверяется, является ли текущая сайт коллекция “слишком большой”, при помощи метода DocIdHelpers.IsSiteTooBig(). Если да, сначала создается рабочий элемент для добавления полей в типы содержимого и затем второй рабочий элемент для установки значения в добавленные поля для существующих документов (см. ниже). Если сайт не является “слишком большим”, изменения типов содержимого будут произведены синхронно и затем также будет создан рабочий элемент для заполнения полей существующих документов. Т.е. второй рабочий элемент будет создан в любом случае, но мы вернемся к нему позже.

Теперь давайте посмотрим что значит “сайт слишком большой”. Вот как реализована функция DocIdHelpers.IsSiteTooBig():

public static bool IsSiteTooBig(SPSite site)
{
    return IsSiteTooBig(site, 1, 40, 20);
}
 
public static bool IsSiteTooBig(SPSite site, int maxWebs, int maxListsPerWeb, int maxDoclibsPerWeb)
{
    ...
}

Т.е. сайт является “слишком большим”, когда в нем содержится более 1 под сайта, более 40 списков или более 20 библиотек документов. Если одно из этих условий истинно для вашего сайте, типы содержимого будут изменятся асинхронно с помощью рабочего элемента. Как было показано выше, он создается внутри метода DocIdEnableWorkitem.ScheduleWorkitem():

public static void ScheduleWorkitem(SPSite site, string Prefix, bool Enable, bool ScheduleAssignment, bool OverwriteExisting, bool DocsetCtUpdateOnly)
{
    if (site == null)
    {
        throw new ArgumentNullException("site");
    }
    string url = site.Url;
    ULS.SendTraceTag(0x636e7037, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.Medium,
"Document ID Enable:ScheduleWorkitem for '{0}':enable={1},schedule={2},overwrite={3}",
        new object[] { url, Enable, ScheduleAssignment, OverwriteExisting });
    ULS.SendTraceTag(0x636e7038, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.Medium,
"Document ID Enable: ScheduleWorkitem called for site '{0}' by this stack: {1}",
        new object[] { url, Environment.StackTrace });
    XmlSerializer serializer = new XmlSerializer(typeof(DocIdEnableWorkitem));
    StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
    DocIdEnableWorkitem o = new DocIdEnableWorkitem();
    o.fEnable = Enable;
    o.fScheduleAssignment = ScheduleAssignment;
    o.fOverwriteExisting = OverwriteExisting;
    o.fDocsetCtUpdateOnly = DocsetCtUpdateOnly;
    o.prefix = Prefix;
    serializer.Serialize((TextWriter) writer, o);
    string strTextPayload = writer.ToString();
    writer.Dispose();
    site.AddWorkItem(Guid.Empty, DateTime.Now.AddMinutes(30.0).ToUniversalTime(), 
DocIdEnableWorkItemJobDefinition.DocIdEnableWIType, site.RootWeb.ID, site.ID, 1,
false, Guid.Empty, Guid.Empty, site.RootWeb.CurrentUser.ID, null, strTextPayload,
Guid.Empty);
    ULS.SendTraceTag(0x636e7039, ULSCat.msoulscat_DLC_DM, ULSTraceLevel.Medium,
"Document ID Enable: ScheduleWorkitem for '{0}' leaving", new object[] { url });
}

Рабочий элемент добавляется в строках 25-28 с помощью вызова метода SPSite.AddWorkItem(). При этом добавляется рабочий элемент с типом 749FED41-4F86-4277-8ECE-289FBF18884F, который обрабатывается заданием “Document ID enable/disable” (см. выше). Этот элемент добавляет поле Document ID в типы содержимого. Более точно, он добавляет 2 поля: Document ID (SPFieldUrlValue) и Document ID Value (string). Эти поля не отображаются ни на странице просмотра деталей стандартного типа содержимого Документ (Настройки сайта > Типы содержимого) ни в списке столбцов в библиотеках Документы. Вы увидите это поле только после того, как добавите какой-либо документ в библиотеку Документы (или любую другую библиотеку, в которой используется стандартный тип содержимого Документ или кастомный тип содержимого, наследующий стандартный Документ) и затем перейдете на форму просмотра свойств документа. Поле Document ID будет содержать значение и будет работать как ссылка, ведущая на http://example.com/_layouts/15/DocIdRedir.aspx?ID={docId}. Эта ссылка является глобальной и уникальной для документа и будет работать даже после перемещения документа, что является большим преимуществом по сравнению с обычным адресом документа.

Если вы прочитали статью, которую я рекомендовал вначале, вы уже знаете, что рабочие элементы хранятся в таблице ScheduledWorkItems контентной базы данных. Для того, чтобы посмотреть их, запустите следующий Sql запрос:

declare @enable uniqueidentifier
set @enable = cast('749FED41-4F86-4277-8ECE-289FBF18884F' as uniqueidentifier)
select * from [dbo].[ScheduledWorkItems] where [Type] = @enable

Запрос должен вернуть 1 строку:

Этот элемент был создан со следующими параметрами, которые указаны в столбце TextPayload:

<?xml version="1.0" encoding="utf-16"?>
<DocIdEnableWorkitem xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <fEnable>true</fEnable>
  <fScheduleAssignment>true</fScheduleAssignment>
  <fOverwriteExisting>false</fOverwriteExisting>
  <fDocsetCtUpdateOnly>false</fDocsetCtUpdateOnly>
</DocIdEnableWorkitem>

Обратите внимание на столбец DeliveryDate. Он содержит самое раннее время в формате UTC (добавьте 3 часа для того, чтобы добавить локальное время), когда задание может быть обработано. Но если вы попробуете запустить задание вручную в Central administration раньше указанного времени, ничего не произойдет. Этот момент является наиболее частой причиной непонимания: администратор пытается запустить задания сразу после активации фичи. Но как мы видели в листинге метода DocIdEnableWorkitem.ScheduleWorkitem() в столбец DeliverDate записывается значение DateTime.Now.AddMinutes(30.0).ToUniversalTime(). Это означает, что мы должны подождать хотя бы 30 минут (либо изменить системное время), перезапустить службу таймера, запустить задание и затем изменить системное время назад и перезапустить таймер снова (более точно, перед тем как вернуть системное время назад, нужно также запустить задание “Document ID assignment”, как раз после того, как задание “Document ID enable/disable” будет завершено – см. вторую часть для подробностей).

Причина такого поведения в хранимой процедуре [dbo].[proc_GetRunnableWorkItems]:

CREATE PROCEDURE [dbo].[proc_GetRunnableWorkItems] (
        @ProcessingId          uniqueidentifier,
        @SiteId                uniqueidentifier,
        @WorkItemType          uniqueidentifier,
        @BatchId               uniqueidentifier,
        @MaxFetchSize          int = 1000,
        @ThrottleThreshold     int = 0,
        @RequestGuid           uniqueidentifier = NULL OUTPUT     
        )
AS
    SET NOCOUNT ON
    IF (dbo.fn_IsOverQuotaOrWriteLocked(@SiteId) >= 1)
    BEGIN
        RETURN 0
    END
    DECLARE @iRet int
    SET @iRet = 0
    DECLARE @oldTranCount int
    SET @oldTranCount = @@TRANCOUNT
    DECLARE @Now datetime
    SET @Now = dbo.fn_RoundDateToNearestSecond(GETUTCDATE())
    DECLARE @InProgressCount int
    DECLARE @ThrottledFetch int
    DECLARE @ReturnWorkItems bit
    SET @ReturnWorkItems = 0
    BEGIN TRAN
    SET @InProgressCount = 0
    SET @ThrottledFetch = 0
    SET @ThrottleThreshold = @ThrottleThreshold + 1
    IF @ThrottleThreshold > 1
    BEGIN
        SET ROWCOUNT @ThrottleThreshold
        SELECT 
            @InProgressCount = COUNT(DISTINCT BatchId)
        FROM
            dbo.ScheduledWorkItems WITH (NOLOCK)
        WHERE
            Type = @WorkItemType AND
            DeliveryDate <= @Now AND
            (InternalState & (1 | 16)) = (1 | 16)
    END
    IF @BatchId IS NOT NULL
    BEGIN
        SET @ThrottledFetch = 16
    END
    IF @InProgressCount < @ThrottleThreshold
    BEGIN
        UPDATE
            dbo.ScheduledWorkItems
        SET
            InternalState = InternalState | 1 | @ThrottledFetch,
            ProcessingId = @ProcessingId
        FROM
            (
            SELECT TOP(@MaxFetchSize)
                Id
            FROM
                dbo.ScheduledWorkItems
            WHERE
                Type = @WorkItemType AND
                DeliveryDate <= @Now AND
                (@SiteId IS NULL OR 
                    SiteId = @SiteId) AND
                (@BatchId IS NULL OR
                    BatchId = @BatchId) AND
                (InternalState & ((1 | 2))) = 0
            ORDER BY
              DeliveryDate
            ) AS work
        WHERE
            ScheduledWorkItems.Id = work.Id
        SET @InProgressCount = @@ROWCOUNT
        SET ROWCOUNT 0            
        IF @InProgressCount <> 0
        BEGIN
          EXEC @iRet = proc_AddFailOver @ProcessingId, NULL, NULL, 20, 0
        END
        SET @ReturnWorkItems = 1
    END
CLEANUP:
        SET ROWCOUNT 0
        IF @iRet <> 0
        BEGIN
            IF @@TRANCOUNT = @oldTranCount + 1
            BEGIN
                ROLLBACK TRAN
            END
        END
        ELSE
        BEGIN
            COMMIT TRAN
            IF @InProgressCount <> 0
               AND @InProgressCount <> @MaxFetchSize 
               AND @WorkItemType = 'BDEADF09-C265-11d0-BCED-00A0C90AB50F'
               AND @BatchId IS NOT NULL AND @SiteId IS NOT NULL
            BEGIN
                UPDATE
                    dbo.Workflow
                SET
                    InternalState = InternalState & ~(1024)
                WHERE
                    SiteId = @SiteId AND
                    Id = @BatchId    
            END            
            IF @ReturnWorkItems = 1
            BEGIN
                SELECT ALL
DeliveryDate, Type, ProcessMachineId as SubType, Id,
SiteId, ParentId, ItemId, BatchId, ItemGuid, WebId, UserId, Created,
BinaryPayload, TextPayload,
                    InternalState
                FROM
                    dbo.ScheduledWorkItems
                WHERE
                    Type = @WorkItemType AND
                    DeliveryDate <= @Now AND
                    ProcessingId = @ProcessingId
                ORDER BY
                    DeliveryDate
                IF @@ROWCOUNT <> 0
                BEGIN
                    EXEC @iRet = proc_UpdateFailOver @ProcessingId, NULL, 20
                END
            END
        END
        RETURN @iRet
 
GO

Как видите процедура возвращает элементы, для которых выполняется условие DeliveryDate <= @Now. Вот почему нужно изменять системное время для того, чтобы рабочие элементы были обработаны.

На этом мы завершаем первую часть. Можете остановится здесь, если вам нужно быстрое решение проблемы, почему поле Document ID не добавляется в ваши типы содержимого. Если же вы хотите узнать больше подробностей реализации, вам также следует ознакомится со второй частью.

Реклама

Об авторе sadomovalex

Старший инженер, team lead, консультант. Работаю в стеке .Net. Последние несколько лет занимаюсь разработкой enterprise приложений под Sharepoint, чему и будет в основном посвящена тематика этого блога. Также активно использую и интересуюсь ASP.Net MVC, DDD, TDD, Agile. Активно участвую в жизни многих профессиональных сообществ, SPb .Net UG, SPb ALT.Net, rsdn, Finland SP UG и др.
Запись опубликована в рубрике Document management, Sharepoint. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s