При активации фичи 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 не добавляется в ваши типы содержимого. Если же вы хотите узнать больше подробностей реализации, вам также следует ознакомится со второй частью.