Решение задачи. Задача таймера, почти конец.

Посты в этой серии:

  1. Список задач для проверки навыков.
  2. Создание задачи таймера.
  3. Использование подходящих классов.
  4. Передача команд задаче таймера (этот пост).
  5. Взаимодействие веб-фронтэнда с задачами таймера.

Задачи таймера сами по себе очень важны для многих решений, но не менее важно взаимодействие задач таймера с другими частями решения.

Например, задача таймера, которая выполняет очистку папок, требует большого количества ресурсов для работы, фактически просмотр всех баз контента для того чтобы удалить папки. Даже если надо удалить ровно одну папку, то задача таймера все равно будет бегать по всей контентной базе данных. Такое поведение крайне расточительно.

Надо как-то ограничить задачу таймера чтобы она бегала только по тем сайтам где разрешит администратор.

Проще всего этого добиться создав фичу уровня Web, которая выставляет свойства узла при активации и модифицировать задачу таймера, чтобы она проверяла свойство.

Код Feature Receiver

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    SetCleanupFlag(properties, true);
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    SetCleanupFlag(properties, false);
}

private void SetCleanupFlag(SPFeatureReceiverProperties properties, bool flag)
{
    var web = properties.Feature.Parent as SPWeb;
    web.Properties[Constants.FlagPropertyName] = flag.ToString();
    web.Properties.Update();
}

Изменения в Timer Job:

private void ProcessWeb(SPWeb web)
{
    if (Convert.ToBoolean(web.Properties[Constants.FlagPropertyName]))
    {
        tju.ProcessLists(web.Lists, ProcessList, null);
    }
}

Вот теперь задача таймера будет обрабатывать только то что укажет администратор. Для своих задач передачи данных из контекста веб-приложения в  timer job вы можете также использовать списки, лучше скрытые.

Но если задача таймера запускается нечасто, а результат пользователю нужен в короткое время, то эти способы не подойдут.

WorkItems

Про механизм “очередей” в SharePoint я писал ранее, но тогда не было подходящей задачи для иллюстрации.

Используем простую идею: некоторое действие пользователя ставит в очередь задачу (SPWorkItem), а задача таймера (worker) анализирует очередь на наличие новых задач и обрабатывает их.

Для создания worker необходимо создать класс наследник SPWorkItemJobDefinition, в котором переопределить два метода WorkItemType и ProcessWorkItem. Документация на MSDN утверждает что вам необходимо переопределить один из методов ProcessWorkItems (с буквой s в конце) – не верьте.

При обработке work item есть много тонкостей, но я не буду их тут описывать, а просто использую уже знакомый класс TimerJobUtility, в котором уже сделана вся “грязная” работа.

public class CleaupWorker : SPWorkItemJobDefinition
{
    TimerJobUtility tju;
    WorkItemTimerJobState wiJobState = new WorkItemTimerJobState(true);

    public CleaupWorker() : base() { }

    public CleaupWorker(SPWebApplication webApp)
        : base(Constants.WorkerJobName, webApp)
    {
        this.Title = "Folder cleanup worker";
    }

    public override Guid WorkItemType()
    {
        return Constants.WorkItemType;
    }

    protected override bool ProcessWorkItem(SPContentDatabase contentDatabase, SPWorkItemCollection workItems, SPWorkItem workItem, SPJobState jobState)
    {
        tju = new TimerJobUtility(Constants.TimerJobName, jobState);
        return tju.ProcessWorkItem(workItems, workItem, wiJobState, ProcessWorkItemCore);
    }

    private void ProcessWorkItemCore(SPWorkItem wi, WorkItemTimerJobState timerJobstate)
    {
        var list = timerJobstate.Web.Lists[wi.ParentId];
        CleanupUtility.CleanupList(list);
    }
}

Тем, кто занимается разработкой для SharePoint Foundation повезло гораздо меньше, им придется все подводные камни обходить самостоятельно. Крайне рекомендую для этого посмотреть реализацию TimerJobUtility.

Чтобы поместить WorkItem в очередь необходимо вызывать метод SPSite.AddWorkItem. Остается самый интересный вопрос: где вызывать этот метод. Об этом, и многом другом в следующей статье.



Убрать левую панель в SharePoint

Именно с этим вопросом ко мне обратились трое за последнюю неделю.

Сделать это довольно просто c помощью CSS

#s4-leftpanel
{
    display:none;
}

.s4-ca
{
    margin-left:auto;
}

Но вот где написать этот стиль…

  1. Если вам нужно получить такой эффект на одной странице, то необходимо добавить в контент AdditionalPageHead.
  2. Если нужно для всего сайта, то поправить MasterPage или CSS, который используется на мастер-странице.
  3. Если нужно такое на странице веб-частей, то ничего не нужно делать. Уже существует шаблон страницы веб-частей где убрана левая панель.
  4. Если нужно убрать левую панель во всей коллекции сайтов, то можно создать в корневом сайте файл и подключить его с помощью SPWeb.AlternateCssUrl. При включении фич публикации этой настройкой можно управлять из админки сайта.


Решение задачи. Задача таймера, продолжение.

  1. Список задач для проверки навыков.
  2. Создание задачи таймера.
  3. Использование подходящих классов (этот пост).
  4. Передача команд задаче таймера.
  5. Взаимодействие веб-фронтэнда с задачами таймера.

В прошлый раз я писал о том как создать задачу таймера в SharePoint. Такой код писать не нужно. Надеюсь никто не успел скопипастить код к себе в проект.

В SharePoint 2010 основной класс для создания задач таймера – SPPausableJobDefinition. В отличии от обычного SPJobDefinition, как вы можете догадаться из названия, SPPausableJobDefinition можно останавливать.

Чтобы создать приостанавливаемую задачу таймера надо переопределить метод Execute с параметром SPJobState. Объект класса SPJobState содержат как свойства, говорящие о том что задача должна остановиться, так и методы сохранения текущего состояния задачи таймера для продолжения работы.

С одной стороны возможность приостанавливать выполнение задачи таймера ведет только к усложнению кода, но нету необходимости писать код, он уже есть в сборке Microsoft.SharePoint.

Класс, который больше всего подходит к нашей задаче – SPAllSitesJobDefinition. Задачи таймера, наследующиеся от этого класса должны переопределить только один метод ProcessSite.

public class TimerJob : SPAllSitesJobDefinition
{
    public TimerJob()
        : base()
    {

    }

    public TimerJob(SPWebApplication webApp)
        : base(Constants.TimerJobName, webApp)
    {
        this.Title = "Folder cleanup job";
    }

    public override void ProcessSite(SPSite site, SPJobState jobState)
    {            
        foreach (SPWeb web in site.AllWebs)
        {
            try
            {
                ProcessWeb(web);
            }
            finally
            {
                web.Dispose();
            }
        }
    }

    private void ProcessWeb(SPWeb web)
    {
        //omited for clarity
    }
}

Кода получилось даже меньше чем в первом варианте, при этом он поддерживает приостановку и запуск с места остановки, а также обновляет значения прогресса выполнения (это новая фича SharePoint 2010).

В прошлом посте писал о выборе значения SPJobLockType. В SharePoint 2010 нет такой необходимости. Разные варианты запуска задач таймера реализованы разными классами в сборке Microsoft.SharePoint.
Например: SPContentDatabaseJobDefinition, SPFirstAvailableServiceJobDefinition, SPServerJobDefinition, SPServiceJobDefinition.

Далее можно модифицировать код ProcessSite и ProcessWeb чтобы поддерживать перезапуск задачи с конкретной библиотеки документов. Но если вы разрабатываете для SharePoint Server 2010 (платной версии), то вам и это не надо делать. Код уже написан.

TimerJobUtility

TimerJobUtility – класс из сборки Microsoft.Office.Server. Он позволяет обходить содержимое SharePoint учитывая возможность остановки и перезапуска задачи таймера.

С использованием TimerJobUtility методы будут выглядеть так:

TimerJobUtility tju;
public override void ProcessSite(SPSite site, SPJobState jobState)
{
    tju = new TimerJobUtility(Constants.TimerJobName, jobState);
    tju.DisableEventFiring = false;
    tju.CancellationGranularity = IterationGranularity.List;
    tju.ResumeGranularity = IterationGranularity.List;
    tju.ProcessSite(site, s => tju.ProcessSite(s, ProcessWeb, null));
}

private void ProcessWeb(SPWeb web)
{
    tju.ProcessLists(web.Lists, ProcessList, null);
}

private void ProcessList(SPList list)
{
    if (!list.Hidden && list is SPDocumentLibrary)
    {
        DeleteEmptyFolders(list.RootFolder.SubFolders);
    }
}

private void DeleteEmptyFolders(SPFolderCollection folders)
{
    //omited for clarity
}

С таким кодом в случае остановки и перезапуска задачи она продолжит выполнять начиная с того списка, который не был обработан. Кроме того есть возможность блокировать срабатывание обработчиков событий установкой флага DisableEventFiring.

SPFolderHierarchy

5000… Для всех разработчиков SharePoint 2010 это магическая цифра. Если у вас запрос должен обработать более 5000 строк, то выпадает SPQueryThrottledException. Причем даже если реально будет возвращено мало строк, но для их вычисления придется просмотреть более 5000 элементов, то будет ошибка. В таких случаях помогает индекс или постраничное разбиение.

Нетрудно догадаться что стандартная реализация свойства SubFolders считывает все элементы из папки, которых может оказаться более 5000., что вызовет ошибку. Чтобы обрабатывать такие случаи в SharePoint Server 2010 (платной версии) есть класс SPFolderHierarchy, который помогает избежать проблем и содержит множество эвристик для максимального быстродействия навигации по папкам.

В итоге, используя все вышеперечисленные возможности, код будет такой:

public class TimerJob : SPAllSitesJobDefinition
{
    TimerJobUtility tju;

    public TimerJob(): base() { }

    public TimerJob(SPWebApplication webApp)
        : base(Constants.TimerJobName, webApp)
    {
        this.Title = "Folder cleanup job";
    }

    public override void ProcessSite(SPSite site, SPJobState jobState)
    {
        tju = new TimerJobUtility(Constants.TimerJobName, jobState);
        tju.DisableEventFiring = false;
        tju.CancellationGranularity = IterationGranularity.List;
        tju.ResumeGranularity = IterationGranularity.List;
        tju.ProcessSite(site, s => tju.ProcessSite(s, ProcessWeb, null));
    }

    private void ProcessWeb(SPWeb web)
    {
        tju.ProcessLists(web.Lists, ProcessList, null);
    }

    private void ProcessList(SPList list)
    {
        if (!list.Hidden && list is SPDocumentLibrary)
        {
            DeleteEmptyFolders(new SPFolderHierarchy(list));
        }
    }

    private void DeleteEmptyFolders(SPFolderHierarchy h)
    {
        foreach (SPFolder folder in (h as IEnumerable<SPFolder>))
        {
            if (folder.Item != null)
            {
                DeleteEmptyFolders(h.GetSubFolders(folder.ServerRelativeUrl));
                if (folder.ItemCount == 0)
                {
                    folder.Delete();
                }
            }
        }
    }
}

Кроме повышенной надежности такого кода есть еще одно преимущество: классы TimerJobUtility и SPFolderHierarchy очень активно пишут диагностические сообщения в ULS. Таким образом вам гораздо будет легче отлаживать такой код на удаленной машине.



Решение задачи. Задача таймера.

  1. Список задач для проверки навыков.
  2. Создание задачи таймера (этот пост).
  3. Использование подходящих классов.
  4. Передача команд задаче таймера.
  5. Взаимодействие веб-фронтэнда с задачами таймера.

Ранее я приводил список задач для проверки навыков программирования для SharePoint. Сегодня напишу о решении четвертой задачи про задачу таймера для очистки библиотек документов от пустых папок.

Задача

Создать задачу таймера (Timer Job), которая буде находить в библиотеках документов пустые папки, в которых нет файлов и которые содержат пустые папки, и удалять их.

Класс задачи таймера

Чтобы создать задачу таймера необходимо создать класс, унаследованный от Microsoft.SharePoint.Administration.SPJobDefinition. Этот класс недоступен в sandbox, поэтому вам нужен farm solution.

В этом классе необходимо переопределить метод Execute и конструктор с параметрами.

public TimerJob(SPWebApplication webApp)
    : base(Constants.TimerJobName, webApp, null,
           SPJobLockType.ContentDatabase)
{
    this.Title = "Folder cleanup job";
}

Первым параметром конструктора передается строка, которая будет отличать задачу таймера от других. Title задает отображаемое в админке имя задачи таймера.

Так как ферма SharePoint может состоять из нескольких серверов, то появляется интересный вопрос – где и сколько раз будет запускаться задача таймера. Особое внимание надо уделить параметру SPJobLockType. Детальное описание по ссылке. Возможные варианты:

  • SPJobLockType.ContentDatabase  - задача таймера запускается для каждой контентной базы данных родительского приложения.
  • SPJobLockType.Job – задача таймера выполняется один раз на всю ферму. В данном режиме учитывается параметр SPServer конструктора, позволяющий указать конкретный сервер для запуска Timer Job.
  • SPJobLockType.None – запускается на каждом сервере в ферме, где развернут родительский сервис. Очень полезно если вам надо запустить некоторый некоторый процесс на каждом сервере в ферме.
Добавление и удаление задачи таймера

Для добавления задачи таймера удобно использовать фичу уровня фермы или веб-приложения с флагом Activate On Default равным true. При разветрывании решения с такой фичей она автоматически активируется в указанной области действия.

Код feature receiver:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    var webApp = properties.Feature.Parent as SPWebApplication;
    var job = new TimerJob(webApp);
    job.Schedule = new SPHourlySchedule()
    {
        BeginMinute = 0,
        EndMinute = 59
    };

    job.Update();
}


public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    var webApp = properties.Feature.Parent as SPWebApplication;
    var job = webApp.JobDefinitions.GetValue<TimerJob>(Constants.TimerJobName);
    job.Delete();
}

Класс SPHourlySchedule является наследником SPSchedule и позволяет задавать расписание запуска задачи таймера.

Важно. Если вы попытаетесь добавить задачу таймера в фиче уровня Site или Web, то при деплое из Visual Studio оно сработает, а при попытке активировать фичу из веб-интерфейса упадет. Это новое ограничение SharePoint 2010, не позволяющее делать Update для классов наследников SPPersistedObject из контекста веб-приложения.

Если же у вас есть унаследованный код, который вы не можете поправить, то вам скорее всего пригодится эта статья.

Обработка элементов списков

Теперь перейдем к основной функции задачи таймера – очистка папок.

public override void Execute(Guid targetInstanceId)
{
    SPWebApplication webApplication = this.Parent as SPWebApplication;
    SPContentDatabase contentDb = webApplication.ContentDatabases[targetInstanceId];

    ProcessDatabase(contentDb);
}

Так как при создании Timer Job был указан SPJobLockType.ContentDatabase, то в качестве параметра targetInstanceId будет ID базы данных контента.

Далее циклы по SPSite и SPWeb:

private void ProcessDatabase(SPContentDatabase contentDb)
{
    foreach (SPSite site in contentDb.Sites)
    {
        try
        {
            ProcessSite(site);
        }
        finally
        {
            site.Dispose();
        }
    }
}

private void ProcessSite(SPSite site)
{
    foreach (SPWeb web in site.AllWebs)
    {
        try
        {
            ProcessWeb(web);
        }
        finally
        {
            web.Dispose();
        }
    }
}

Вот такие конструкции необходимо использовать чтобы пройтись по всем SPSite и SPWeb в базе данных. Если не напишите Dispose в циклах, то на сервере очень быстро закончится память.

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

Для того чтобы обезопасить себя и ваших пользователей необходимо использовать утилиту SPDisposeCheck. Она вам подскажет где надо совободить объекты.

Ну и наконец удаление папок:

private void ProcessWeb(SPWeb web)
{
    foreach (var lib in web.Lists.OfType<SPDocumentLibrary>())
    {
        if (!lib.Hidden)
        {
            DeleteEmptyFolders(lib.RootFolder.SubFolders);
        }
    }
}

private void DeleteEmptyFolders(SPFolderCollection folders)
{
    foreach (var folder in folders.OfType<SPFolder>().ToList())
    {
        DeleteEmptyFolder(folder);
    }

}

private void DeleteEmptyFolder(SPFolder folder)
{
    if (folder.Item != null)
    {
        DeleteEmptyFolders(folder.SubFolders);

        if (folder.ItemCount == 0)
        {
            folder.Delete();
        }
    }
}

Не копипастите код из статьи до того как прочитаете следующую часть.



MVP

Получил статус Microsoft MVP.

MVP_FullColor_ForScreen

Теперь признанный кем-то эксперт и все такое.