$null строки в Powershell и переиспользование сессий в Analysis Services

В Analysis Services различаются понятия соединения (connection) и сессии (session, в русских BOL - сеанс). Между ними существует отношение многие-ко-многим, в частности, несколько соединений могут шарить одну сессию. Сессия поддерживает контекст состояния: текущий каталог, вычисления и т.д. По умолчанию, каждое новое соединение открывает новую сессию, и по его закрытию она закрывается. Можно закрыть соединение без закрытия сессии, если вызвать перегруженый метод Close соединения AdomdConnection с параметром false. Осиротевшую сессию, т.е.не имеющую связанных с ней открытых соединений, SSAS прибирает после IdleOrphanSessionTimeout. Время жизни неактивной сессии определяется MaxIdleSessionTimeout. Эти и другие конфигурационные настройки сервера Analysis Services можно видеть, если кликнуть по нему правой кнопкой в Object Explorere SSMS, выбрать в контекстном меню пункт Properties и отметить галку Show Advanced (All) Properties.

Рис.1

Указанные настройки хранятся в файле C:\Program Files\Microsoft SQL Server\MSAS10_50.MSSQLSERVER\OLAP\Config\msmdsrv.ini. Еще во времена 2005-го Энтони Манн и Эдвард Меломед написали статью «SQL Server 2005 Analysis Services (SSAS) Server Properties», в которой толково объясняется, что означает каждая настройка и на что она влияет. Замечательный документ. Я рекомендую всем его распечатать и пришпилить куда-нибудь на видное место. Более подробно прочитать про различия сессий и соединений можно в BOL – см. Установление соединений в ADOMD.NET и Работа с соединениями и сеансами в ADOMD.NET.

С точки зрения производительности бывает выгодно повторно использовать сессии, экономя на пересоздании контекста. В этом случае мы сохраняем в приложении идентификатор сессии (свойство соединения SessionID), a закрывая AdomdConnection, говорим серверу оставить сессию - сnn.Close(false). Перед открытием нового соединения ему присваивается сохраненный идентификатор прежней сессии и, если та еще не проэкспайрилась, оно будет использовать ее контекст. Все просто. Я попытался воспроизвести пример, описанный в книжке Тео Лачева «Applied Microsoft Analysis Services 2005» (п.17.4.3 Managing Connections and Sessions, стр.574), на PowerShell.

[reflection.assembly]::LoadWithPartialName("Microsoft.AnalysisServices.AdomdClient") | Out-Null 
[string] $global:sessionId 
[Microsoft.AnalysisServices.AdomdClient.AdomdConnection] $cnn = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdConnection("Data Source=localhost;Initial Catalog=Adventure Works DW 2008R2") 
<# Предполагается, что в переменной $global:sessionId хранится идентификатор сессии AS, оставшийся от прошлого раза. #> 
$cnn.SessionID = $global:sessionId 
<# Если такого sessionId не значится, например, дело происходит в первый раз и он еще пуст или прошло больше IdleOrphanSessionTimeout 
и старый проэкспайрился, при открытии соединения с таким sessionId выкинется ошибка. #> 
while ($cnn.State -eq "Closed") 
{ 
 try   
 { 
  $cnn.Open() 
 } 
 catch [Microsoft.AnalysisServices.AdomdClient.AdomdConnectionException] 
 { 
  <# Ожидаем исключения, что такого SessionID нет. Если возникает другое - проталкиваем дальше. #> 
  if ($_.Exception.InnerException.Message.IndexOf("Either the session does not exist or it has already expired") -le 0) 
  { 
   throw 
  } 
  else 
  { 
   <# Если выяснилось, что этот $cnn.SessionID истек, надо сбросить его обратно в Null и заново попытаться открыть соединение. 
      Новому соединению будет назначена новая сессия с новым SessionID. #> 
   $cnn.SessionID = $null 
  } 
 } 
} 
<# Сохраняем SessionID (на случай, если он сменился) для дальнейшего переиспользования. #> 
$global:sessionId = $cnn.SessionID 
<# Выполняем какие-то полезные действия на соединении #> 
[Microsoft.AnalysisServices.AdomdClient.AdomdCommand] $cmd = $cnn.CreateCommand() 
#... 
$cnn.Close($false) #закрыть соединение, оставив сессию открытой

Скрипт 1

Смысл скрипта понятен: сначала пытаемся открыть соединение с прежним SessionID, оставшимся от каких-то предыдущих действий. Если таковой истек, сбрасываем $cnn.SessionID обратно в Null и снова пытаемся открыть соединение. Тогда при открытии для него будет автоматически создана новая сессия, и в $cnn.SessionID появится ее идентификатор.

К сожалению, этот скрипт не работает из-за одной неприятной особенности PowerShell. При присвоении $null в $cnn.SessionID он конвертируется в пустую строку, т.к. св-во SessionID имеет тип строки. ADOMD.NET обучено заводить новую сессию только в случае, когда свойство SessionID у соединения равно строго Null, т.е. нулевой указатель на область памяти, где хранится содержание строки. Если же там лежит какая-то строка, пусть даже пустая, т.е. указатель непустой, но указывает на пустую область памти, ADOMD.NET воспринимает это как сигнал того, что мы хотим открыть соединение в контексте некоторой сессии. Сессии с SessionID в виде пустой строки она не находит, поэтому соединение не открывает, и цикл будет длиться бесконечно .

Когда в переменную сохраняется некоторое значение, хотелось бы быть уверенным, что оно там в целости и неизменности сохранится, и в своем первозданном виде его можно оттуда достать по мере надобности. PowerShell является уникальным языком в этом плане. Можно положить одно, а достать совсем другое:

[string] $s = $null 
Write-Host ($s -eq $null) 
-------------------------------------------- 
False

Скрипт 2

Вопрос: не знает ли кто, как запретить Powershell’у творить такое непотребство? Мне не нужно, чтобы $null конвертировался в пустую строку. Нужно, чтобы $null оставался $null, как это принято во всех нормальных человеческих языцех.

В прошлом посте я полагал, что самый занятный язык – это диалект SQL, поддерживаемый Analysis Services. На самом деле, то были цветочки. Самый занятный язык – это PowerShell. Нарекания на такое поведение существуют с незапамятных времен - https://connect.microsoft.com/feedback/ViewFeedback.aspx?FeedbackID=307821&SiteID=99, http://www.vistax64.com/powershell/160947-string-null-eq-null-eq.html, http://www.techtalkz.com/microsoft-windows-powershell/150495-gotcha-null-string-not-null.html . Возможно, его с тех пор уже поправили. Если вы в курсе таких исправлений, подскажите, и вам воздастся. Пока что воркэраунды, которые удалось найти, достаточно монструозны. Проще переписать Скрипт 1 как

cls 
[reflection.assembly]::LoadWithPartialName("Microsoft.AnalysisServices.AdomdClient") | Out-Null 
[string] $global:sessionId 
[string] $connectionString = "Data Source=localhost;Initial Catalog=Adventure Works DW 2008R2" 
[Microsoft.AnalysisServices.AdomdClient.AdomdConnection] $cnn = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdConnection($connectionString) 
try   
{ 
 $cnn.Open() 
} 
catch [Microsoft.AnalysisServices.AdomdClient.AdomdConnectionException] 
{ 
 if ($_.Exception.InnerException.Message.IndexOf("Either the session does not exist or it has already expired") -le 0) 
 { 
  throw 
 } 
 else 
 { 
  $cnn = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdConnection($connectionString) 
  $cnn.Open() 
  $global:sessionId = $cnn.SessionID 
 } 
} 
<# Выполняем какие-то полезные действия на соединении #> 
[Microsoft.AnalysisServices.AdomdClient.AdomdCommand] $cmd = $cnn.CreateCommand() 
#... 
$cnn.Close($false) #закрыть соединение, оставив сессию открытой

Скрипт 3

Автор: Алексей Шуленин