Translated MSDN article - Creating Custom Timer Jobs in Windows SharePoint Services 3.0

I have finished translation of the article written by Andrew Connel into Russian. The original article is available in MSDN Library. The article explains how to build, deploy, and debug custom timer jobs in Windows SharePoint Services 3.0.


Создание Пользовательских Заданий Таймера в Windows SharePoint Services 3.0

Резюме: В документе рассматривается процесс создания, развертывания и отладки пользовательских заданий таймера в Windows SharePoint Services 3.0. Также рассматриваются доступные разработчикам различные варианты конфигурирования этих заданий.

Эндрю Коннэл, Andrew Connell Inc. [ http://www.andrewconnell.com/default.aspx ] (Microsoft MVP)

Апрель 2008

Перевод: Руслан Грабар [ / ]

Применимо к продуктам: Windows SharePoint Services 3.0, Microsoft Office SharePoint Server 2007

Содержание

· Введение в задания таймера Windows SharePoint Services

· Создание пользовательских заданий таймера

· Варианты хранения настроек задания таймера

· Развертывание пользовательских заданий таймера

· Отладка пользовательских заданий таймера

· Заключение

· Дополнительные ресурсы

Введение в задания таймера Windows SharePoint Services

Большому количеству приложений часто требуется запускать по расписанию различные процессы. Эти процессы используются для сложных вычислений, уведомлений, поверки валидности и множества других задач. Подобные задачи приходится решать и при использовании Windows SharePoint Services. Например, чтобы быстро вернуть релевантные результаты в ответ на поисковый запрос пользователя, содержимое фермы серверов должно быть проиндексировано заранее. Более того, необходимо регулярно, через заданные промежутки времени, производить переиндексацию содержимого. Это всего лишь один пример. Другим примером может служить отправка каждую ночь, или раз в неделю, сообщений электронной почты пользователям, которые изъявили желание быть оповещенными об изменениях в списках SharePoint. Подобные задания, выполняемые по расписанию, запускаются службой Заданий Таймера SharePoint(the SharePoint Timer service). Эта служба является одной из многих служб Windows, которая устанавливается при развертывании пакета WSS.

Служба заданий таймера очень похожа на стандартное приложение любой версии Windowsизвестное под названием Планировщик Задач. Главным преимуществом службы заданий таймера SharePoint над Планировщиком Задач является осведомленность службы о топологии фермы серверов, что позволяет распределять нагрузку выполнения заданий между серверами, либо привязывать задания к конкретным серверам, предоставляющим услуги, необходимые для выполнения конкретного задания.

Несмотря на то, что служба заданий таймера не является чем-то новым в инфраструктуре SharePoint, ранее разработчики либо не могли воспользоваться возможностями этой службы, либо разработка и регистрация собственных заданий таймера требовала значительных затрат времени и усилий. С выходом Windows SharePoint Services 3.0 ситуация значительно улучшилась и программирование собственных заданий упростилось. Так, например, администраторы фермы серверов могут просматривать списки заданий установленных на ферме при помощи инструмента Центра Администрирования. Для этого необходимо перейти на страницу Operations и выбрать Timer Jobs Definitions в разделе Global Configuration. В дополнение к списку всех заданий, страница отображает статус и результат их последнего выполнения.

Вы можете определить задание таймера, используя всего один класс, который необходимо унаследовать от класса Microsoft.SharePoint.Administration.SPJobDefinition [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.administration.spjobdefinition(en-us).aspx ]. Вам также необходимо поместить сборку, содержащую ваш класс, в глобальный кэш сборок (GAC). Далее вам необходимо установить ваше задание таймера на ферму серверов. В этой статье описывается процесс создания и развертывания пользовательского задания таймера на ферме серверов, работающих под управлением Windows SharePoint Services 3.0. Кроме того, как и многие другие приложения, задания таймера могут потребовать определенных конфигурационных параметров для своего функционирования. У вас есть несколько различных вариантов хранения параметров конфигурации, которые мы рассмотрим в данной статье.

Пример задания таймера, рассмотренного в этой статье, не является очень сложным решением. Цель создания задания – заменить собой скрипты «прогрева» Windows SharePoint Services. Поскольку Windows SharePoint Services является всего лишь ASP.NET 2.0 приложением, страницы, при первом к ним обращении, подвергаются процессу компиляции, известной как just-in-time (JIT) компиляция. Использование скриптов «прогрева» позволяет вызвать JITкомпиляцию принудительно, что положительно сказывается на времени загрузки страниц при демонстрации и разработке. Разрабатываемое в статье задание таймера призвано заменить собой эти «прогревочные» скрипты.

Создание пользовательских заданий таймера

Первый шаг на пути создания пользовательского задания таймера – это создание класса, унаследованного от класса SPJobDefinition. Класс SPJobDefinition содержит три конструктора. Согласно Windows SharePoint Services 3.0 SDK, первый конструктор без параметров зарезервирован для внутреннего использования. Два других конструктора определяют имя задания таймера, родительское веб-приложение или службу (например, поисковый индексатор), затем сервер и тип блокировки задания. Указание типа блокировки позволяет избежать одновременной работы различных заданий с одним объектом.

Имеется три различных типа блокировки, определенных перечислением SPJobLockType:

· SPJobLockType.ContentDatabase Блокирует базу содержимого, к которой привязано задание.

· SPJobLockType.Job Не позволяет нескольким экземплярам задания выполнятся на одном сервере

· SPJobLockType.None Определяет отсутствие блокировки

Реализуя наше задание «прогрева» мы не хотим, чтобы несколько экземпляров задания одновременно исполнялись на одном сервере. Кроме того, мы укажем определенное название для задания, что позволит легко получать экземпляр задания по его названию.

C#

using System;
using System.Net;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace MSDN.SharePoint.Samples {
  public class SharePointWarmupJob : SPJobDefinition {

    public SharePointWarmupJob () : base() { }

    public SharePointWarmupJob (SPWebApplication webApp)
      : base(Globals.JobName, webApp, null, SPJobLockType.ContentDatabase) {
      this.Title = Globals.JobName;
    }
  }
}

Кроме необходимости унаследовать наш класс от класса SPJobDefinition, нам также необходимо переопределить виртуальный метод Execute, определенный в нашем базовом классе. Это тот метод, который Windows SharePoint Services вызывает в момент, когда задание должно быть выполнено. Метод Execute получает единственный параметр, идентификатор базы данных содержимого, с которой будет работать экземпляр задания. В нашем примере значение этого параметра будет равно Guid.Empty, поскольку база содержимого не является целевым типом нашего задания. Метод Execute в задании «прогрева» инициирует запрос к домашней станице каждой коллекции сайтов на заданном веб-приложении. Ниже представлен пример, иллюстрирующий этот метод.

C#

public override void Execute (GuidtargetInstanceId) {  
  foreach (SPSite siteCollection in this.WebApplication.Sites) {
    WarmUpSiteCollection(siteCollection);

    siteCollection.RootWeb.Dispose();
    siteCollection.Dispose();
  }
}

private void WarmUpSiteCollection (SPSite siteCollection) {  
  WebRequest request = WebRequest.Create(siteCollection.Url);
  request.Credentials = CredentialCache.DefaultCredentials;
  request.Method = "GET";

  WebResponse response = request.GetResponse();
  response.Close();
}

В момент, когда задание тамера регистрируется или разворачивается на определенном веб-приложении, устанавливается взаимосвязь между заданием и веб-приложением. Именно поэтому метод Execute полагает, что свойство WebApplication [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.administration.spjobdefinition.webapplication(en-us).aspx ] базового класса не содержит null. Следующий шаг заключается в получении конфигурационных параметров из внешнего источника. Следующая часть статьи рассматривает различные варианты хранения конфигурационных данных.

Варианты хранения настроек задания таймера

Многим приложениям для работы часто требуется информация о параметрах конфигураций, хранящихся во вне этих приложений. Конфигурационные параметры могут включать, например, адреса точек подключения к веб-сервисам или параметры подключения к базам данных. Приложения, написанные с использованием технологии Windows Forms, могут использовать особые XML-файлы конфигурации, такие как application.config. Приложения ASP.NET 2.0 используют файл Web.config. Задания таймера, как было сказано ранее, выполняются службой таймера SharePoint, представленной исполняемым файлом Owstimer.exe. Таким образом, одним из вариантов конфигурирования может служить написание файл конфигурации для исполняемого файла службы таймера SharePoint, например Owstimer.exe.config. Этот способ, однако, не рекомендуется использовать по ряду причин. Во-первых, вам необходимо внести изменения в каждый файл конфигурации на каждом сервере, входящим в ферму. Во-вторых, эти манипуляции придется проделывать вручную из консоли вместо использования инструментов администрирования или инфраструктуры файлов решений Windows SharePoint Services (*.WSPs).

В вашем распоряжении имеется несколько других вариантов получения конфигурационной информации, если она потребуется вашим приложениям. Среди вариантов можно упомянуть механизм «Propertybag», чтение и разбор внешнихXML-файлов, хранение конфигурационных данных в списках SharePoint или размещение информации в иерархическом хранилище.

Внешние файлы

Использование внешних файлов для хранения конфигурационных данных похоже на процедуру создания файлов Web.config или *.exe.config основанных на XML и используемых в обычных ASP.NET 2.0 приложениях или приложениях, построенных на платформе Microsoft .NET Framework. Основное отличие лишь в том, что вам необходимо самостоятельно написать код, извлекающий конфигурационные данные из этих файлов. Один из вариантов реализации этого способа – это создание сериализуемого класса.

Например, рассмотрим вариант, когда заданию таймера для работы требуется почтовый адрес. Во-первых, создайте новый класс Address с полями, характерными для стандартного североамериканского почтового адреса, как показано в примере ниже.

C#

public class Address {
  public string Address1;
  public string Address2;
  public string City;
  public string State;
  public string Zip;
}

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

Xml

<?xml version="1.0" encoding="utf-8"?> <Address> <Address1>One Microsoft Way</Address1> <City>Redmond</City> <State>WA</State> <Zip>98052</Zip> </Address>

Для того чтобы извлечь данные, используйте класс XmlSerializer, который может восстановить объект типа Address из XML файла.

C#

Address _address = new Address();
XmlSerializer serializer = new XmlSerializer(typeof(Address));
FileStream file = new FileStream(@"c:\address.xml", FileMode.Open);
_address = serializer.Deserialize(file) as Address;

Чтобы сохранить объект типа Address обратно в XML файл, выполняйте операцию обратную чтению из файла.

C#

Address _address = new Address();
XmlSerializer serializer = new XmlSerializer(typeof(Address));
TextWriter writer = new StreamWriter(@"c:\address.xml");
serializer.Serialize(writer, _address);
writer.Close();

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

Хранилище свойств объекта SharePoint (Property Bag)

В Windows SharePoint Services 3.0, компания Microsoft добавила коллекцию обобщений (generic collection) свойств часто используемых объектов Windows SharePoint Services:

· SPWeb [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.spweb(en-us).aspx ]

· SPFolder [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.spfolder(en-us).aspx ]

· SPFile [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.spfile(en-us).aspx ]

· SPListItem [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.splistitem(en-us).aspx ]

Свойства SPWeb.Properties и SPListItem.Properties возвращают объект типа Microsoft.SharePoint.Utilities. [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.utilities.sppropertybag(en-us).aspx ] , который унаследован от класса System.Collections.Specialized.StringDictionary. Единственное отличие между этими классами заключается в наличии у класса SPPropertyBag метода Update, который сохраняет в соответствующую базу данных контента Windows SharePoint Services все изменения внесенные в содержимое хранилища свойств объекта (PropertyBag).

Использовать хранилища свойств объекта (PropertyBag) достаточно просто. Код, представленный ниже, демонстрирует добавление нового элемента в хранилище свойств узла SharePoint и последующее извлечение значения из хранилища. В последних строках показан процесс удаления свойств объекта из коллекции элементов хранилища.

C#

using (SPSite siteCollection = new SPSite("http://litware"){
  using (SPWeb site = siteCollection.RootWeb){

    // Добавляемновуюпаруключ/значение в хранилище свойств.
    site.Properties.Add("SomeKey", "SomeValue");
    site.Properties.Update();

    // Читаем значение по заданному ключу
    string keyValue = site.Properties["SomeKey"];

    // Удаляемпаруключ/значение
    site.Properties.Remove["SomeKey"];
    site.Properties.Update();
  }
}

Используя представленную технику, вы можете создавать веб-парты, обычные страницы, страницы приложений SharePoint, а также любой другой код, который способен обращаться к хранилищу свойств текущего узла SharePoint.

Списки SharePoint

Использование списков SharePoint является третьим по счету вариантом сохранения конфигурационных параметров. Списки рекомендуется использовать с том случае, если задание таймера связано с конкретной коллекцией узлов (site collection) SharePoint. Вы можете создать особый список (или несколько списков) на корневом узле коллекции узлов SharePoint, на узле, с которым будет взаимодействовать задание таймера. Работа со списком в коде задания таймера ни чем не отличается от работы со списками, например, при разработке веб-частей.

C#

using (SPSite siteCollection = new SPSite("http://litwareinc.com"){
  using (SPWeb site = siteCollection.RootWeb){
    SPList taskList = site.Lists["TimerJobData"];
    foreach(SPListItem item = taskList.Items){
      // Используем данные полученные из списка.
    }
  }
}

Хранилище Иерархических Объектов (Hierarchical Object Storage)

В документации Windows SharePoint Services 3.0 Software Development  Kit (SDK) [ http://msdn.microsoft.com/ru-ru/library/ms441339(en-us).aspx ] упоминается новое дополнение в службам Windows SharePoint, которое, при помощи общего фреймворка, позволяет создавать и взаимодействовать с административными данными располагающимися в хранилище иерархических объектов.

Чтобы добавить элементы в хранилище объектов, необходимо, во-первых, создать класс описывающий данные для хранения. Этот класс должен быть унаследован от Microsoft.SharePoint.Administration.SPPersistedObject [ http://msdn.microsoft.com/ru-ru/library/microsoft.sharepoint.administration.sppersistedobject(en-us).aspx ] и поддерживать механизм сериализации. Чтобы поддерживать механизм сериализации, класс должен иметь конструктор по умолчанию без параметров. Все свойства объекта, подлежащие сериализации, должны быть объявлены публичными и сопровождены атрибутом Microsoft.SharePoint.Administration.PersistedAttribute. Ниже приведен пример класса данных, используемых в задании таймера SharePointWarmupJob.

C#

using System;
using System.Collections.Generic;
using Microsoft.SharePoint.Administration;

namespace MSDN.SharePoint.Samples {
  public class WarmupJobSettings : SPPersistedObject {

    [Persisted]
    public List SiteCollectionsEnabled = new List();

    public WarmupJobSettings () { }

    public WarmupJobSettings (string name, SPPersistedObject parent, Guid id)
      : base(name, parent, id) {
    }
  }
}

Объект содержит лишь одно публичное поле, в котором хранится список идентификаторов (IDs) коллекций узлов которые должно«разогреть» наше задание таймера SharePointWarmupJob.

Обратите внимание на второй конструктор. В этом конструкторе указывается имя, ссылка на родительский объект и уникальный идентификатор сохраняемого экземпляра данных. Ссылка на родительский объект позволяет связать экземпляр сохраняемых данных с конкретным объектом в ферме SharePoint.

Хранилище иерархических объектов в нашем примере используется для хранения списка коллекций узлов, доступных в каждом веб-приложении с которым будет работать задание таймера SharePointWarmupJob. Достаточно лишь получить ссылку на родительский объект и, используя метод GetChild, можно прочитать настройки из хранилища. Давайте изменим метод SharePointWarmupJob.Execute таким образом, чтобы он мог получать данные из хранилища объектов.

C#

namespace MSDN.SharePoint.Samples {
  internal static class Globals {

    internal static string SharePointWarmupJobSettingsId {
      get { return"SharePointWarmupJobs"; }
    }

    internal static string JobName {
      get { return"SharePoint Warmup Job"; }
    }
  }
}
public class SharePointWarmupJob : SPJobDefinition {

  // Опущено...

  public override void Execute (Guid targetInstanceId) {

    // Получаем настройки для задания «прогрев».
    WarmupJobSettings settings = this.WebApplication.GetChild(Globals.SharePointWarmupJobSettingsId);
    foreach (SPSite siteCollection in this.WebApplication.Sites) {
      if (settings.SiteCollectionsEnabled.Contains(siteCollection.ID))
        WarmUpSiteCollection(siteCollection);

      siteCollection.RootWeb.Dispose();
      siteCollection.Dispose();
    }
  }
}

В этом виде, реализация задания тамера закончена в объеме, достаточном для обращения к коллекциям узлов по протоколу HTTP. Задание таймера может определить к каким именно коллекциям узлов обращаться, почерпнув необходимую информацию из хранилища иерархических объектов. Следующим шагом, который необходимо осуществить, является инсталляция задания таймера на ферме серверов WSS.

Развертывание пользовательских заданий таймера

Класс SharePointWarmupJob должен быть скомпилирован в подписанную сборку, что позволит получить строгое имя. Полученную сборку необходимо поместить в GAC. Размещение сборки в глобальном кэше позволяет инсталлировать задание таймера на ферму серверов WSS. К сожалению, административный интерфейс WSS не предоставляет средства установки заданий, поэтому вам необходимо написать свой инсталлятор. Несмотря на то, что потребность в самостоятельном написании установщика похоже на искусственное ограничение, задумайтесь о том, какой широкий спектр способов установки задания находится в вашем распоряжении. Далее вы узнаете о трех различных способах разворачивания заданий таймера. Последний способ, использующий пользовательское приложение, является тем способом, который будет использован для демонстрации установки задания таймера SharePointWarmupJob, поскольку является одним из самых сложных вариантов.

Приемник сообщений Возможностей (Feature) SharePoint

В Windows SharePoint Services 3.0 появилась инфраструктура Возможностей SharePoint, которая позволяет разработчикам и администраторам внедрять дополнительный функционал в уже существующие сайты и разворачивать пользовательские программные решения, такие как, например, шаблоны рабочих процессов, которые обычно разрабатываются в Microsoft Visual Studio. Возможности SharePoint также могут быть использованы для установки заданий таймера. Хотя механизм Возможностей SharePoint и не предусматривает возможность установки заданий таймера напрямую, существует возможность использовать события активации и деактивации Возможности. Мы можем перехватывать события Activated и Deactivating и в обработчике этих событий производить соответственно установку и удаление задания таймера.

Возможность SharePoint, осуществляющая установку и удаление задания таймера должна быть спрятанной, другими словами, описана с атрибутом Hidden="FALSE", для того чтобы активация этой возможности была доступна только через консольное административное приложение Stsadm.exe. Поскольку возможности SharePoint активируются через веб-интерфейс, с привилегиями текущего пула веб-приложения. Эта учетная запись обычно не обладает необходимым набором привилегий для успешной установки задания таймера, поэтому необходимо использовать консольное приложение Stsadm.exe, запущенное с правами администратора фермы серверов SharePoint. Для включения учетной записи в группу администраторов, необходимо воспользоваться инструментом Центра Администрирования, кликните на вкладку Operations. В группе SecurityConfiguration, кликните Update farm administrator's group. Обратите внимание, что при запуске Stsadm.exe из консоли, это приложение работает с правами текущего пользователя.

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

C#

public override void FeatureActivated (SPFeatureReceiverProperties properties) {
  SPSite site = properties.Feature.Parent as SPSite;

  // Убедимся, что задание уже не было активировано ранее.
  foreach (SPJobDefinition job in site.WebApplication.JobDefinitions) {
    if (job.Name == Globals.SharePointWarmupJobSettingsId)
      job.Delete();
  }

  // Устанавливаем задание.
  SharePointWarmupJob SharePointWarmupJob = new SharePointWarmupJob(site.WebApplication);

  SPMinuteSchedule schedule = new SPMinuteSchedule();
  schedule.BeginSecond = 0;
  schedule.EndSecond = 59;
  schedule.Interval = 2;
  SharePointWarmupJob.Schedule = schedule;

  SharePointWarmupJob.Update();
}

Удаление задания таймера осуществляется путем обработки события в методе FeatureDeactivated.

C#

public override void FeatureDeactivating (SPFeatureReceiverProperties properties) {
  SPSite site = properties.Feature.Parent as SPSite;

  // Удаляем задание.
  foreach (SPJobDefinition job in site.WebApplication.JobDefinitions) {
    if (job.Name == Globals.SharePointWarmupJobSettingsId)
      job.Delete();
  }
}

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

Пользовательские команды для Stsadm.exe

Административное консольное приложение Stsadm.exe не предоставляет возможность устанавливать, удалять и управлять заданиями таймера. Однако, функционал Stsadm.exe может быть расширен. Вы можете написать свои команды и зарегистрировать их для использования в Stsadm.exe. Это дает администраторам фермы серверов легкий способ управлять заданиями и даже исполнять сценарии команд, используя стандартные BAT файлы.

Создание пользовательской команды для Stsadm.exe состоит из двух шагов.

  1. Создайте класс, реализующий методы интерфейса Microsoft.SharePoint.StsAdmin.ISPStadmCommand и установите получившуюся подписанную сборку в глобальный кэш (GAC).

  2. Создайте XML файл с именем stsadmcommands.*.xml и перечислите в нем список команд, имя класса, содержащего команду и строгое имя сборки, содержащей класс. Получившийся файл копируется в папку [..]\12\CONFIG на каждый сервер в ферме серверов.

При использовании пользовательских команд для установки и удаления заданий таймера, вам необходимо решить непростую задачу – либо написать команду для конкретного задания таймера, либо написать команду, которая может быть использована для управления любым заданием таймера. Второй вариант гораздо сложнее, поскольку задание таймера может иметь произвольное число аргументов, передаваемых в его конструкторах. Для получения дополнительной информации о создании пользовательских команд для STSADM.EXE обратитесь к разделу «Дополнительные ресурсы».

Пользовательские приложения

Еще один вариант установки и удаления пользовательского задания таймера заключается в использовании Пользовательских Приложений. Этот вариант будет использован для установки задания таймера SharePointWarmupJob. Мы используем этот подход еще и потому, что заданию требуются некоторые параметры конфигурации для его корректной работы. Наше пользовательское приложение для задания таймера будет состоять только из одной административной страницы, которая будет добавлена на сайт Центра Администрирования фермы серверов. Новая ссылка, озаглавленная “Управление заданием прогрева”, будет добавлена на страницу управления Управление Приложением. Этого можно достичь путем использования инфраструктуры Возможностей SharePoint. Ниже представлен пример определения Возможности.

Xml

<?xml version="1.0" encoding="utf-8"?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
         Id="5B16233D-4396-4241-B320-276EF3FA42FE"
         Title="Задание Прогрева Коллекции Сайтов MSDN"
         Scope="Farm"
         Hidden="FALSE"
         ActivateOnDefault="TRUE"
         Version="1.0.0.0">
  <ElementManifests>
    <ElementManifest Location="centralAdmin.xml"/>
  </ElementManifests>
</Feature>

Файл декларации элемента centralAdmin.xml выполняет работу по добавлению нашей ссылки на страницу Центра Администрирования.

Xml

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="B2EA3A10-701C-4B7A-A1D5-68EFD00D9957"
                GroupId="SiteManagement"
                Location="Microsoft.SharePoint.Administration.ApplicationManagement"
                Sequence="100"
                Title="Manage SharePoint Warmup Jobs"
                Description="Manage the warmup jobs for existing SharePoint Web applications.">
    <UrlAction Url="_admin/MSDN/WarmupJobManager.aspx" />
  </CustomAction>
</Elements>

Следующим шагом будет создания страницы администрирования. Эта страница позволяет:

  • Выбрать веб-приложение на котором необходимо производить прогрев.
  • Отображать список коллекций узлов внутри выбранного веб-приложения. Пользователь должен иметь возможность выбрать конкретную коллекцию из списка для последующего прогрева.
  • · Указать как часто должно выполняться задание таймера по прогреву.

Страница WarmupJobManager.aspx использует несколько элементов управления Windows SharePoint Services разработанных Microsoft для интерфейса Центра Администрирования. Страница использует элементы управления InputFormSelector, ButtonSection и WebApplicationSelector для того чтобы внешний вид интерфейса совпадал с интерфейсом, используемым в Центре Администрирования. Исходный код страницы представлен ниже.

aspx

<%@ Page Debug="true" Language="C#" MasterPageFile="~/_admin/admin.master"
  Inherits="MSDN.SharePoint.Samples.WarmupJobManager, MSDN.SharePoint.Samples.SharePointWarmupJob,
  Version=1.0.0.0, Culture=neutral, PublicKeyToken=7fd1d26c5854f031" %>

<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
  Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>

<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>

<asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderPageTitleInTitlearea" runat="server">
  Web Application Warmup Job Manager
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageDescription" runat="server">

  This page allows you to manage a warmup job that will simply issue requests to trigger
  a Just In Time (JIT) compilation. Forcing a JIT compilation can speed up the initial request
  for a page which is helpful on development and sites that do not incur a high volume of traffic.

</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderMain" runat="server">
  <table width="100%" class="propertysheet" cellpadding="0" cellspacing="0" border="0">
    <tr><td class="ms-error"><asp:Literal ID="ErrorMessageLiteral" runat="server" EnableViewState="false" /> </td></tr>
  </table>

  <table border="0" cellspacing="0" cellpadding="0" width="100%">
    <tr>
      <td>
        <!-- web application selector -->
        <wssuc:InputFormSection runat="server"
                    Title="<%$Resources:spadmin, multipages_webapplication_title%>"
                    Description="<%$Resources:spadmin, multipages_webapplication_desc%>" >
          <Template_InputFormControls>
            <tr>
              <td>
                <SharePoint:WebApplicationSelector id="WebAppSelector" runat="server"
                  OnContextChange="WebAppSelector_OnContextChange" />
              </td>
            </tr>
          </Template_InputFormControls>
        </wssuc:InputFormSection>

        <!-- site collection selector -->
        <wssuc:InputFormSection runat="server"
                                  Title="Site Collections"
                                  Description="Select the site collections to enable or disable
                                  the warmup job. Only the top level site's homepage within the site collection is hit.">
          <Template_InputFormControls>
            <tr valign="top">
              <td>
                <asp:Repeater ID="SiteCollectionRepeater" runat="server"
                OnItemDataBound="SiteCollectionRepeater_OnItemDataBound">
                  <ItemTemplate>
                    <asp:TextBox runat="server" Visible="false" ID="SiteCollectionIdTextBox" />
                    <asp:Literal runat="server" ID="SiteCollectionLiteral" /><br />
                    <asp:RadioButtonList ID="JobStatusList" runat="server" RepeatDirection="Horizontal">
                      <asp:ListItem class="ms-descriptionText">Enabled</asp:ListItem>
                      <asp:ListItem class="ms-descriptionText">Disabled</asp:ListItem>
                    </asp:RadioButtonList>
                  </ItemTemplate>
                  <SeparatorTemplate><br /></SeparatorTemplate>
                </asp:Repeater>
              </td>
            </tr>
          </Template_InputFormControls>
        </wssuc:InputFormSection>

        <!-- job runtime schedule -->
        <wssuc:InputFormSection runat="server"
                                title="Scheduled Run Interval">
          <Template_Description>Select the interval for the warmup jobs. All site collections
          for a particular web application will have the same schedule.</Template_Description>
          <Template_InputFormControls>
            Run warmup job every <asp:DropDownList id="IntervalMinutesDropDownList" runat="server" >
                                    <asp:ListItem>1</asp:ListItem>
                                    <asp:ListItem>2</asp:ListItem>
                                    <!-- omitted for readability -->
                                    <asp:ListItem>58</asp:ListItem>
                                    <asp:ListItem>59</asp:ListItem>
                                  </asp:DropDownList> minutes.
          </Template_InputFormControls>
        </wssuc:InputFormSection>

        <wssuc:ButtonSection runat="server">
          <template_buttons>
            <asp:Button id="SetTimerJobsButton" runat="server" class="ms-ButtonHeightWidth" Text="OK"
            OnClick="SetTimerJobsButton_OnClick" />
          </template_buttons>
        </wssuc:ButtonSection>
      </td>
    </tr>
  </table>
</asp:Content>

Если загрузить в браузер страницу SiteWarmUpJobManager.aspx, то вы должны увидеть страницу подобную той, что изображена на Рисунке 1.

Рисунок 1. Страница управления заданием разогрева веб-приложения

clip-image001.gif

Обратите внимание на несколько моментов в исходном коде страницы. Во-первых, атрибут Inherits в тэге Page указывает на полное имя класса code-behind, находящегося в подписанной сборке, расположенной в глобальном кэше сборок. Это не что иное, как использование разделения кода бизнес-логики и кода представления, предоставляемого ASP.NET 2.0. Во-вторых, тэг <SharePoint:WebApplicationSelector> содержит атрибут OnContextChange, значение которого ссылается на метод WebAppSelector_OnContextChange. Это позволяет вам перехватить событие, возникающее в момент, когда пользователь изменяет выбранное веб-приложение. Кроме того, перехватываются еще два других события:

· Событие OnItemDataBound контрола-повторителя SiteCollectionRepeater регистрируется для вызова метода SiteCollectionRepeater_OnItemDataBound для связывания данных с элементами пользовательского интерфейса

· Для установки и удаления, а также установки необходимых параметров, событие OnClick кнопки SetTimerJobsButton регистрируется для вызова метода SetTimerJobsButton_OnClick.

После того, как создана страница, нам необходимо создать файл code-behind, содержащий класс нашей страницы. Однако, прежде чем приступить к созданию файла code-behind, необходимо определить пару утилитарных классов. Первый класс будет статическим классом, содержащим имя задания таймера и уникальный идентификатор настроек, хранящихся хранилище иерархических объектов (Global.cs).

C#

using System;
namespace MSDN.SharePoint.Samples {
  internal staticclass Globals {

    internal staticstring SharePointWarmupJobSettingsId {
      get { return"SharePointWarmupJobs"; }
    }

    internal staticstring JobName {
      get { return"SharePoint Warmup Job"; }
    }
  }
}

Второй утилитарный класс содержит описание данных настроек, которые записываются в хранилище объектов.

C#

using System;
using System.Collections.Generic;
using Microsoft.SharePoint.Administration;

namespace MSDN.SharePoint.Samples {
  public class WarmupJobSettings : SPPersistedObject {

    [Persisted]
    public List SiteCollectionsEnabled = new List();

    public WarmupJobSettings () { }

    public WarmupJobSettings (string name, SPPersistedObject parent, Guid id)
      : base(name, parent, id) {
    }
  }
}

Теперь настало время создать файл code-behind. Первая часть процесса создания файла заключается в создании класса, который наследуется от базового класса Microsoft.SharePoint.ApplicationPages.GlobalAdminPage. Этот класс определен в сборке [..]\12\CONFIG\ADMINBIN\Microsoft.SharePoint.ApplicationPages.Administration.dll и содержит методы, облегчающие управление настройками безопасности и перенаправления запросов в рамках узла Центра Администрирования. Кроме того, вам необходимо переопределить метод что бы иметь возможность обновлять узел данными, полученными по протоколу HTTP методом GET.

C#

using System;
using System.Collections.Generic;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.ApplicationPages;
using Microsoft.SharePoint.WebControls;

namespace MSDN.SharePoint.Samples {
  public class WarmupJobManager : GlobalAdminPageBase {

    protected Literal ErrorMessageLiteral;
    protected WebApplicationSelector WebAppSelector;
    protected Repeater SiteCollectionRepeater;
    protected DropDownList IntervalMinutesDropDownList;

    protected override void OnLoad (EventArgs e) {
      SPContext.Current.Web.AllowUnsafeUpdates = true;
    }
}
Следующим шагом, вам необходимо обработать событие OnContextChange, которое усатнавливает состояние для страницы путем загрузки коллекций узлов в текущем веб-приложении.
C#
protected void WebAppSelector_OnContextChange (object sender, EventArgs e) {
  InitSiteCollectionSelector();

  // Пытаемся получить настройки инициализировать выпадающий список
  foreach (SPJobDefinition job in this.WebAppSelector.CurrentItem.JobDefinitions) {
    if (job.Title == Globals.JobName)
      IntervalMinutesDropDownList.Items.FindByValue(((SPMinuteSchedule)job.Schedule).Interval.ToString()).Selected = true;
  }
}

private void InitSiteCollectionSelector () {
  if (this.WebAppSelector.CurrentItem.Sites.Count > 0) {
    this.SiteCollectionRepeater.DataSource = this.WebAppSelector.CurrentItem.Sites;
    this.SiteCollectionRepeater.DataBind();
  }
}

protected void SiteCollectionRepeater_OnItemDataBound (object sender, RepeaterItemEventArgs e) {
  if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem) {
    SPSite siteCollection = e.Item.DataItem as SPSite;

    TextBox siteCollectionIdTextBox = e.Item.FindControl("SiteCollectionIdTextBox") as TextBox;
    siteCollectionIdTextBox.Text = siteCollection.ID.ToString();

    Literal siteCollectionName = e.Item.FindControl("SiteCollectionLiteral") as Literal;
    siteCollectionName.Text = siteCollection.Url;

    RadioButtonList jobStatusList = e.Item.FindControl("JobStatusList") as RadioButtonList;
    if (SiteCollectionHasWarmupJobConfigured(siteCollection))
      jobStatusList.SelectedValue = "Enabled";
    else
      jobStatusList.SelectedValue = "Disabled";
  }
}

private bool SiteCollectionHasWarmupJobConfigured (SPSite siteCollection) {
  try {
    WarmupJobSettings settings = this.WebAppSelector.CurrentItem.GetChild(Globals.SharePointWarmupJobSettingsId);

    // Если настройки еще не были созданы, то создадим их
    if (settings == null) {
      SPPersistedObject parent = this.WebAppSelector.CurrentItem;
      settings = new WarmupJobSettings(Globals.SiteWarmupJobSettingsId, parent, Guid.NewGuid());
      settings.Update();
    }

    return settings.SiteCollectionsEnabled.Contains(siteCollection.ID);
  } catch (Exception ex) {
    ErrorMessageLiteral.Text = "A new storage location had to be created. Please go back to the Application Management
      page and come back in before doing any work.";
    return false;
  }
}

Теперь страница должна загружать коллекции узлов, входящих в состав веб-приложения. Последним шагом реализуем обработчик события OnClick кнопки Submit, чтобы можно было инсталлировать и удалять задание таймера и устанавливать необходимые конфигурационные параметры.

C#

protected void SetTimerJobsButton_OnClick (object sender, EventArgs e) {
  WarmupJobSettings settings = this.WebAppSelector.CurrentItem.GetChild(Globals.SiteWarmupJobSettingsId);

  // Delete the job for the current Web application.
  foreach (SPJobDefinition oldJob in this.WebAppSelector.CurrentItem.JobDefinitions) {
    if (oldJob.Title == Globals.JobName)
      oldJob.Delete();
  }
  // Purge the configuration data for the current Web application.
  settings.SiteCollectionsEnabled.Clear();
  settings.Update();

  // Get a list of all the site collections that were requested to be enabled.
  List selectedSiteCollections = GetSelectedSiteCollections();
  if (selectedSiteCollections.Count > 0) {

    // Create a new instance of the job and schedule it.
    SPMinuteSchedule schedule = new SPMinuteSchedule();
    schedule.BeginSecond = 0;
    schedule.EndSecond = 59;
    schedule.Interval = Convert.ToInt32(this.IntervalMinutesDropDownList.SelectedValue);

    SharePointWarmupJob warmupJob = new SharePointWarmupJob(this.WebAppSelector.CurrentItem);
    warmupJob.Schedule = schedule;
    warmupJob.Update();

    // Add the settings.
    foreach (Guid siteCollectionID in selectedSiteCollections) {
      settings.SiteCollectionsEnabled.Add(siteCollectionID);
    }
    settings.Update();
  }
}

private List GetSelectedSiteCollections () {
  List selectedSiteCollections = new List();

  TextBox siteCollectionIdTextBox;
  RadioButtonList siteCollectionList;
  foreach (RepeaterItem item in SiteCollectionRepeater.Items) {
    siteCollectionIdTextBox = item.FindControl("SiteCollectionIdTextBox") as TextBox;
    siteCollectionList = item.FindControl("JobStatusList") as RadioButtonList;
    if (siteCollectionList.SelectedValue == "Enabled")
      selectedSiteCollections.Add(new Guid(siteCollectionIdTextBox.Text));
  }

  return selectedSiteCollections;
}

Обратите внимание на то, как задается расписание для задания таймера. Поля SPMinuteSchedule.BeginSecond и SPMinuteSchedule.EndSecond задают временное окно для старта задания. Служба таймера SharePoint запускает задание таймера в произвольное время в промежуток между значениями поля BeginSecond и поля EndSecond. Этот нюанс работы службы таймера объясняется тем, что служба спроектирована с учетом особенностей работы ресурсоемких заданий таймера на всех северах, входящих в ферму. Если бы все задания таймера стартовали в один момент, это бы привело к увеличению нагрузки сразу на всех серверах фермы. Следовательно, произвольный выбор времени запуска задания облегчает распределение нагрузки по серверам фермы.

Последним шагом нам необходимо подписать сборку, чтобы получить строгое имя, поместить полученную сборку в глобальный кэш, и развернуть ASPX страницу в новую папку нашего приложения в следующей директории [..]\12\ADMIN\MSDN. Когда вы разместите все необходимые файлы и активируете Возможность SharePoint, в Центре Администрирования появится ссылка на управление заданием разогрева (см. Рисунок 2).

Рисунок 2. Ссылка на задание в Центре Администрирования

clip-image002.gif

clip-image0021.gif

Теперь администратор может перейти на страницу управления заданием таймера из Центра Администрирования и активировать прогрев определенной коллекции сайта на заданном веб-приложении. Если наше задние таймера активировано хотя бы для одной коллекции узлов, мы сможем увидеть его в списке определений заданий таймера Центра Администрирования. Кроме того, после первого запуска нашего задания, информацию об исходе выполнения задания можно найти на странице статуса заданий таймера.

Отладка пользовательских заданий таймера

Безусловно, на определенном шаге процесса разработки, вам придется производить отладку вашего кода, либо для поиска дефектного участка кода, либо для отслеживания состояния и хода выполнения приложения. К счастью, вы можете отлаживать задания таймера в Visual Studio так же, как вы бы отлаживали любое другое приложение. Однако, это не так тривиально, как простое нажатие F5 или подключение к процессу W3WP.exe, которое является хостом для пулов веб-приложений. Задания таймера выполняются специальной службой Windows, которая устанавливается на сервер в тот момент, когда вы устанавливаете WSS. Эта служба называется Windows SharePoint Services Timer. Эта служба запускает исполняемый файл Owstimer.exe, следовательно, именно к этому процессу необходимо подключаться для отладки ваших заданий таймера.

Иногда бывает непросто определить, подключились ли мы к процессу или нет, потому что задания таймера могут не запускаться по несколько минут. Вы можете подолгу сидеть перед экраном, не понимая, что на самом деле происходит – либо задание еще не выполняется, либо ваши брейк-пойнты не сработали из-за проблем с загрузкой файлов с отладочными символами (*.pdb). Простой способ определить, что задание таймера запустилось. Он заключается в добавлении в метод SPJobDefinition.Execute одной строки, отображающей модальное окно, которое блокирует поток исполнения задания таймера до тех пор, пока вы не закроете это окно с сообщением.

Добавив следующий код в метод Execute, вы не только увидите что код вашего задания таймера исполняется (как показано на рисунке 3), но вы также получите время на подключение к процессу Owstimer.exe. После подключения отладчика Visual Studio к процессу, нажмите на кнопку Ignore в окне сообщения для того чтобы продолжить исполнение потока задания таймера.

C#

public override void Execute (Guid targetInstanceId) {

  // Если в компилируем отладочный код, принудительно вызываем прерывание в потоке
  // используя Assert(false), что бы подключиться отладчиком к процессу OWSTIMER.EXE
#if (DEBUG)
      System.Diagnostics.Trace.Assert(false);
#endif

  // Поток продолжает исполнение кода в методе Execute()
}

Рисунок 3. Результат использования Assert(false)

clip-image0031.gif

Размещение оператора Assert(false)внутри директивы условной компиляции #if (DEBUG) приводит к тому, что окно с сообщением появляется только в отладочной версии программы.

Замечание:
Возможно, что процесс Owstimer.exe не будет виден в списке процессов диалогового окна Attach To Process в Visual Studio. В этом случае поставьте “галку” в поле Show process from all users.

Заключение

Компания Microsoft представила в Windows SharePoint Services 3.0 концепцию процессов, работающих по расписанию, известных как задания таймера. Фактическая реализация заданий таймера – задача разработчиков, решая которую, они могут удовлетворить практическую любую потребность представленную в реализуемом проекте.

Дополнительные ресурсы

Для получения дополнительной информации посетите следующие ссылки:

· Visual How To: Creating, Deploying, and Debugging Custom Timer Jobs in Windows SharePoint Services 3.0 [ http://msdn.microsoft.com/ru-ru/library/cc427068(en-us).aspx ]

· Windows SharePoint Services Developer Center [ http://msdn2.microsoft.com/en-us/sharepoint/default.aspx ]

· Microsoft Office Developer Center [ http://msdn.microsoft.com/office/default.aspx ]

· Joel Oleson's Blog: Warm Up Your SharePoint Servers [ http://blogs.msdn.com/joelo/archive/2006/08/13/697044.aspx ]

· Andrew Connell's Blog: Creating Custom SharePoint Timer Jobs [ http://www.andrewconnell.com/blog/articles/CreatingCustomSharePointTimerJobs.aspx ]

· Spencer Harbar's Blog: Application Pool Recycle Utility for SharePoint Developers [ http://www.harbar.net/articles/APM.aspx ]

· Maurice Prather's Blog: Hierarchical Object Store [ http://www.bluedoglimited.com/SharePointThoughts/ViewPost.aspx?ID=271 ]

· WSS 3.0 SDK: What's New in WSS 3.0 - Object Model Enhancements [ http://msdn2.microsoft.com/en-us/library/ms480101.aspx ]

· WSS 3.0 SDK: Microsoft.Sharepoint.StsAdmin.ISpStsadmCommand [ http://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.stsadmin.ispstsadmcommand.aspx ]

· WSS 3.0 SDK: Microsoft.SharePoint.Administration.SPPersistedObject [ http://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.administration.sppersistedobject.aspx ]

Метки: development job sharepoint timer