Files
sqltoolsservice/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs
2023-02-16 16:15:30 -08:00

421 lines
15 KiB
C#

//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.XEvent;
using Microsoft.SqlServer.Management.XEventDbScoped;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
/// <summary>
/// Main class for Profiler Service functionality
/// </summary>
public sealed class ProfilerService : IDisposable, IXEventSessionFactory, IProfilerSessionListener
{
private bool disposed;
private ConnectionService connectionService = null;
private ProfilerSessionMonitor monitor = new ProfilerSessionMonitor();
private static readonly Lazy<ProfilerService> instance = new Lazy<ProfilerService>(() => new ProfilerService());
/// <summary>
/// Construct a new ProfilerService instance with default parameters
/// </summary>
public ProfilerService()
{
this.XEventSessionFactory = this;
}
/// <summary>
/// Gets the singleton instance object
/// </summary>
public static ProfilerService Instance
{
get { return instance.Value; }
}
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal ConnectionService ConnectionServiceInstance
{
get
{
connectionService ??= ConnectionService.Instance;
return connectionService;
}
set
{
connectionService = value;
}
}
/// <summary>
/// XEvent session factory. Internal to allow mocking in unit tests.
/// </summary>
internal IXEventSessionFactory XEventSessionFactory { get; set; }
/// <summary>
/// Session monitor instance
/// </summary>
internal ProfilerSessionMonitor SessionMonitor
{
get
{
return this.monitor;
}
}
/// <summary>
/// Service host object for sending/receiving requests/events.
/// Internal for testing purposes.
/// </summary>
internal IProtocolEndpoint ServiceHost
{
get;
set;
}
/// <summary>
/// Initializes the Profiler Service instance
/// </summary>
public void InitializeService(ServiceHost serviceHost)
{
this.ServiceHost = serviceHost;
this.ServiceHost.SetRequestHandler(CreateXEventSessionRequest.Type, HandleCreateXEventSessionRequest, true);
this.ServiceHost.SetRequestHandler(StartProfilingRequest.Type, HandleStartProfilingRequest, true);
this.ServiceHost.SetRequestHandler(StopProfilingRequest.Type, HandleStopProfilingRequest, true);
this.ServiceHost.SetRequestHandler(PauseProfilingRequest.Type, HandlePauseProfilingRequest, true);
this.ServiceHost.SetRequestHandler(GetXEventSessionsRequest.Type, HandleGetXEventSessionsRequest, true);
this.ServiceHost.SetRequestHandler(DisconnectSessionRequest.Type, HandleDisconnectSessionRequest, true);
this.SessionMonitor.AddSessionListener(this);
}
/// <summary>
/// Handle request to start a profiling session
/// </summary>
internal async Task HandleCreateXEventSessionRequest(CreateXEventSessionParams parameters, RequestContext<CreateXEventSessionResult> requestContext)
{
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(
parameters.OwnerUri,
out connInfo);
if (connInfo == null)
{
throw new Exception(SR.ProfilerConnectionNotFound);
}
else if (parameters.SessionName == null)
{
throw new ArgumentNullException("SessionName");
}
else if (parameters.Template == null)
{
throw new ArgumentNullException("Template");
}
else
{
IXEventSession xeSession = null;
// first check whether the session with the given name already exists.
// if so skip the creation part. An exception will be thrown if no session with given name can be found,
// and it can be ignored.
try
{
xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo);
}
catch { }
// create a new XEvent session and Profiler session
xeSession ??= this.XEventSessionFactory.CreateXEventSession(parameters.Template.CreateStatement, parameters.SessionName, connInfo);
// start monitoring the profiler session
monitor.StartMonitoringSession(parameters.OwnerUri, xeSession);
var result = new CreateXEventSessionResult();
await requestContext.SendResult(result);
SessionCreatedNotification(parameters.OwnerUri, parameters.SessionName, parameters.Template.Name);
}
}
/// <summary>
/// Handle request to start a profiling session
/// </summary>
internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext<StartProfilingResult> requestContext)
{
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(
parameters.OwnerUri,
out connInfo);
if (connInfo != null)
{
// create a new XEvent session and Profiler session
var xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo);
// start monitoring the profiler session
monitor.StartMonitoringSession(parameters.OwnerUri, xeSession);
var result = new StartProfilingResult();
await requestContext.SendResult(result);
}
else
{
throw new Exception(SR.ProfilerConnectionNotFound);
}
}
/// <summary>
/// Handle request to stop a profiling session
/// </summary>
internal async Task HandleStopProfilingRequest(StopProfilingParams parameters, RequestContext<StopProfilingResult> requestContext)
{
ProfilerSession session;
monitor.StopMonitoringSession(parameters.OwnerUri, out session);
if (session != null)
{
// Occasionally we might see the InvalidOperationException due to a read is
// in progress, add the following retry logic will solve the problem.
int remainingAttempts = 3;
while (true)
{
try
{
session.XEventSession.Stop();
await requestContext.SendResult(new StopProfilingResult { });
break;
}
catch (InvalidOperationException)
{
remainingAttempts--;
if (remainingAttempts == 0)
{
throw;
}
Thread.Sleep(500);
}
}
}
else
{
throw new Exception(SR.SessionNotFound);
}
}
/// <summary>
/// Handle request to pause a profiling session
/// </summary>
internal async Task HandlePauseProfilingRequest(PauseProfilingParams parameters, RequestContext<PauseProfilingResult> requestContext)
{
monitor.PauseViewer(parameters.OwnerUri);
await requestContext.SendResult(new PauseProfilingResult { });
}
/// <summary>
/// Handle request to pause a profiling session
/// </summary>
internal async Task HandleGetXEventSessionsRequest(GetXEventSessionsParams parameters, RequestContext<GetXEventSessionsResult> requestContext)
{
var result = new GetXEventSessionsResult();
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(
parameters.OwnerUri,
out connInfo);
if (connInfo == null)
{
await requestContext.SendError(new Exception(SR.ProfilerConnectionNotFound));
}
else
{
List<string> sessions = GetXEventSessionList(parameters.OwnerUri, connInfo);
result.Sessions = sessions;
await requestContext.SendResult(result);
}
}
/// <summary>
/// Handle request to disconnect a session
/// </summary>
internal async Task HandleDisconnectSessionRequest(DisconnectSessionParams parameters, RequestContext<DisconnectSessionResult> requestContext)
{
monitor.StopMonitoringSession(parameters.OwnerUri, out _);
}
/// <summary>
/// Gets a list of all running XEvent Sessions
/// </summary>
/// <returns>
/// A list of the names of all running XEvent sessions
/// </returns>
internal List<string> GetXEventSessionList(string ownerUri, ConnectionInfo connInfo)
{
var sqlConnection = ConnectionService.OpenSqlConnection(connInfo);
SqlStoreConnection connection = new SqlStoreConnection(sqlConnection);
BaseXEStore store = CreateXEventStore(connInfo, connection);
// get session names from the session list
List<string> results = store.Sessions.Aggregate(new List<string>(), (result, next) =>
{
result.Add(next.Name);
return result;
});
return results;
}
private static BaseXEStore CreateXEventStore(ConnectionInfo connInfo, SqlStoreConnection connection)
{
BaseXEStore store = null;
if (connInfo.IsCloud)
{
if (DatabaseUtils.IsSystemDatabaseConnection(connInfo.ConnectionDetails.DatabaseName))
{
throw new NotSupportedException(SR.AzureSystemDbProfilingError);
}
store = new DatabaseXEStore(connection, connInfo.ConnectionDetails.DatabaseName);
}
else
{
store = new XEStore(connection);
}
return store;
}
/// <summary>
/// Gets an XEvent session with the given name per the IXEventSessionFactory contract
/// Also starts the session if it isn't currently running
/// </summary>
public IXEventSession GetXEventSession(string sessionName, ConnectionInfo connInfo)
{
var sqlConnection = ConnectionService.OpenSqlConnection(connInfo);
SqlStoreConnection connection = new SqlStoreConnection(sqlConnection);
BaseXEStore store = CreateXEventStore(connInfo, connection);
Session session = store.Sessions[sessionName] ?? throw new Exception(SR.SessionNotFound);
// start the session if it isn't already running
if (session != null && !session.IsRunning)
{
session.Start();
}
// create xevent session wrapper
return new XEventSession()
{
Session = session
};
}
/// <summary>
/// Creates and starts an XEvent session with the given name and create statement per the IXEventSessionFactory contract
/// </summary>
public IXEventSession CreateXEventSession(string createStatement, string sessionName, ConnectionInfo connInfo)
{
var sqlConnection = ConnectionService.OpenSqlConnection(connInfo);
SqlStoreConnection connection = new SqlStoreConnection(sqlConnection);
BaseXEStore store = CreateXEventStore(connInfo, connection);
Session session = store.Sessions[sessionName];
// session shouldn't already exist
if (session != null)
{
throw new Exception(SR.SessionAlreadyExists(sessionName));
}
var statement = createStatement.Replace("{sessionName}", sessionName);
connection.ServerConnection.ExecuteNonQuery(statement);
store.Refresh();
session = store.Sessions[sessionName];
if (session == null)
{
throw new Exception(SR.SessionNotFound);
}
if (!session.IsRunning)
{
session.Start();
}
// create xevent session wrapper
return new XEventSession()
{
Session = store.Sessions[sessionName]
};
}
/// <summary>
/// Callback when profiler events are available
/// </summary>
public void EventsAvailable(string sessionId, List<ProfilerEvent> events, bool eventsLost)
{
// pass the profiler events on to the client
this.ServiceHost.SendEvent(
ProfilerEventsAvailableNotification.Type,
new ProfilerEventsAvailableParams()
{
OwnerUri = sessionId,
Events = events,
EventsLost = eventsLost
});
}
/// <summary>
/// Callback when the XEvent session is closed unexpectedly
/// </summary>
public void SessionStopped(string viewerId, int sessionId)
{
// notify the client that their session closed
this.ServiceHost.SendEvent(
ProfilerSessionStoppedNotification.Type,
new ProfilerSessionStoppedParams()
{
OwnerUri = viewerId,
SessionId = sessionId
});
}
/// <summary>
/// Callback when a new session is created
/// </summary>
public void SessionCreatedNotification(string viewerId, string sessionName, string templateName)
{
// pass the profiler events on to the client
this.ServiceHost.SendEvent(
ProfilerSessionCreatedNotification.Type,
new ProfilerSessionCreatedParams()
{
OwnerUri = viewerId,
SessionName = sessionName,
TemplateName = templateName
});
}
/// <summary>
/// Disposes the Profiler Service
/// </summary>
public void Dispose()
{
if (!disposed)
{
disposed = true;
}
}
}
}