$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
Алексей Шуленин
Comments
- Anonymous
July 22, 2010
Согласен, что обходные пути совсем не просты: вот вариант [reflection.assembly]$analysisServicesAssembly = [reflection.assembly]::LoadWithPartialName("Microsoft.AnalysisServices.AdomdClient") [Microsoft.CSharp.CSharpCodeProvider]$provider = New-Object Microsoft.CSharp.CSharpCodeProvider [System.CodeDom.Compiler.CompilerParameters]$params = New-Object System.CodeDom.Compiler.CompilerParameters $params.GenerateInMemory = $True $refs = "System.dll", "System.Data.dll", $analysisServicesAssembly.Location, "mscorlib.dll" $params.ReferencedAssemblies.AddRange($refs) [string]$txtCode = ' public class ConnectionSessionIDWrapper { public void SetSessionID(Microsoft.AnalysisServices.AdomdClient.AdomdConnection connection, object sessionID) { if(connection != null) connection.SessionID = (string)sessionID; } } ' $results = $provider.CompileAssemblyFromSource($params, $txtCode) $mAssembly = $results.CompiledAssembly $wrapper = $mAssembly.CreateInstance('ConnectionSessionIDWrapper') [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=[nullable]$global:sessionId $wrapper.SetSessionID($cnn, $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. #> $wrapper.SetSessionID($cnn, $null) } } } <# Сохраняем SessionID (на случай, если он сменился) для дальнейшего переиспользования. #> $global:sessionId = $cnn.SessionID <# Выполняем какие-то полезные действия на соединении #> [Microsoft.AnalysisServices.AdomdClient.AdomdCommand] $cmd = $cnn.CreateCommand() #... $cnn.Close($false) #закрыть соединение, оставив сессию открытой