SQL Server failover cluster - determine active node - sql-server

Is there a way to programmatically determine which node in a SQL Server failover cluster is the active node? Or at least determine whether the current machine is the active node?
I have a Windows program which runs on both physical nodes in a failover cluster, but that should operate differently depending on whether it is running on the active node. Part of the reason is that this program should not run simultaneously on the inactive and the active node.
(I've read a bit about making the program cluster aware, but that seems heavily overkill for this simple scenario.)

From SQL Server:
Select ServerProperty('ComputerNamePhysicalNetBIOS')
You can also access it through the Microsoft.SqlServer.Management.Smo Namespace as shown here.

You can check like that:
1. Check Availability Group Status:
if (select
ars.role_desc
from sys.dm_hadr_availability_replica_states ars
inner join sys.availability_groups ag
on ars.group_id = ag.group_id
where ag.name = 'AvailabilityGroupName'
and ars.is_local = 1) = 'PRIMARY'
begin
-- this server is the primary replica, do something here
end
else
begin
-- this server is not the primary replica, (optional) do something here
end
*Remember to change AvailabilityGroupName
or
2. prevent executing job on secondary:
IF master.dbo.svf_AgReplicaState('AvailabilityGroupName')=0  raiserror ('This is not the primary replica.',2,1)
or
3. check write availability on secondary:
IF (SELECT CONVERT(sysname,DatabasePropertyEx(DB_NAME(),'Updateability'))) != 'READ_ONLY'
BEGIN
-- this server is the primary replica, do something here
END
or
4. for SQL2014 and newer:
IF master.dbo.fn_hadr_database_is_primary_replica('Admin') = 1
    BEGIN 
        -- this server is the primary replica, do something here
    END
ELSE 
    BEGIN 
        -- this server is not the primary replica, (optional) do something here
    END

Related

Allow Windows AD group to own a SQL job

The SQL Agent Jobs sits above the user level and requires a login to be assigned to the owner. But it doesn't take a group login as an accepted parameter. I need to use the Windows AD group as owner because I have different SQL users and some of them should see only the specific jobs. As now Ive created separate jobs for every user using SQLAgentUserRole which is not good for sure and the database is full of 1:1 jobs, each of them with different owner to avoid seeing the other jobs.
The whole picture:
Lets say that I have 10 different jobs in the database. One of those jobs is named UserJob. I want specific users when connecting to the database and expand the jobs section to see ONLY the job named "UserJob" and be able to start it. I dont need it via Stored procedure, etc. I just need to start the job via the SSMS (right click, start job, enter parameters if needed). Thanks.
As per the docs SSMS checks user membership in the following Database Roles to show SQL Server Agent tree node:
SQLAgentUserRole
SQLAgentReaderRole
SQLAgentOperatorRole
I used SQL Server Profiler to find what queries are executed when you first connect to database in Object Browser and expand various nodes.
For SQL Server Agent it uses SELECT * FROM msdb.dbo.sysjobs_view view to list Jobs. This view can be modified.
Changes
Create a new Database Role in msdb database. I called it "CustomJobRole".
I then created a new Job (I assume you already have a Job) called "TestJob"
Create a low privilege user that should be able to see and run only "TestJob".
Add this user to "CustomJobRole" and "SQLAgentReaderRole" and/or "SQLAgentOperatorRole" (see linked above docs for details)
Modify sysjobs_view as follows:
(see comments in code)
ALTER VIEW sysjobs_view
AS
SELECT jobs.job_id,
svr.originating_server,
jobs.name,
jobs.enabled,
jobs.description,
jobs.start_step_id,
jobs.category_id,
jobs.owner_sid,
jobs.notify_level_eventlog,
jobs.notify_level_email,
jobs.notify_level_netsend,
jobs.notify_level_page,
jobs.notify_email_operator_id,
jobs.notify_netsend_operator_id,
jobs.notify_page_operator_id,
jobs.delete_level,
jobs.date_created,
jobs.date_modified,
jobs.version_number,
jobs.originating_server_id,
svr.master_server
FROM msdb.dbo.sysjobs as jobs
JOIN msdb.dbo.sysoriginatingservers_view as svr
ON jobs.originating_server_id = svr.originating_server_id
--LEFT JOIN msdb.dbo.sysjobservers js ON jobs.job_id = js.job_id
WHERE
-- Custom: Add Condition for your Custom Role and Job Name
( (ISNULL(IS_MEMBER(N'CustomJobRole'), 0) = 1) AND jobs.name = 'TestJob' )
OR (owner_sid = SUSER_SID())
OR (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1)
-- Custom: In order for users to be able to see and start Jobs they have to be members of SQLAgentReaderRole/SQLAgentOperatorRole
-- but these roles gives them ability to see all jobs so add an exclusion
OR ( ISNULL(IS_MEMBER(N'SQLAgentReaderRole'), 0) = 1 AND ISNULL( IS_MEMBER(N'CustomJobRole'), 0 ) = 0 )
OR ( (ISNULL(IS_MEMBER(N'TargetServersRole'), 0) = 1) AND
(EXISTS(SELECT * FROM msdb.dbo.sysjobservers js
WHERE js.server_id <> 0 AND js.job_id = jobs.job_id))) -- filter out local jobs
Note: commented out LEFT JOIN is original code and has nothing to do with the solution.
Summary
This method is "hacky" as it only modifies the job list for certain users and does not actually prevent them from running other jobs via code, in other words this does not offer any security, just convenience of clean UI. Implementation is simple but, obviously not scalable: Job name is hard-coded and negative membership presence is used (i.e. AND ISNULL( IS_MEMBER(N'CustomJobRole'), 0 ) = 0). IMO, it is the simplest and most reliable (least side effects) method though.
Tested on
SSMS v18.9.2 + SQL Server 2014 SP3
Editing Job Step Workaround
It is not possible to modify Job Step unless you are a Job Owner or a Sysadmin.
One, even more "hacky", way to work around this problem is to create a table that would hold all input parameters and give users insert/update access to this table. Your SP can then read parameters from this table. It should be easy for Users to Right-Click -> Edit on the table and modify data.
For the table structure I would recommend the following:
Assuming you have relatively few parameters I suggest that you create one column per parameter. This way you have correct data types for each
parameter.
Add an After Insert / Delete trigger to the table to ensure
that the table always has exactly one row of data.

SQL Server linked server error "The partner transaction manager has disabled its support for remote/network transactions."

I have a linked server (SQL Server 14.0.1000.169). The local server (SQL Server 10.0.1600) receives data in short periods of time, around 1 new row per minute, into Table46. I need to pass some of the information of this new row to the linked server, so I created a trigger in the local server for this:
CREATE TRIGGER New_Event
ON dbo.Table46 FOR INSERT AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [LinkedServer].[Database].[dbo].[TableEvents]
SELECT i.[046_ID] AS [id]
, NP.NoPart + ' ' + CONVERT(VARCHAR(3), T41.[041_No]) AS [name]
, DATEADD(MINUTE, -1 * i.[046_ExeTime], i.[046_DateTime]) AS [eventstart]
, i.[046_DateTime] AS [eventend]
, i.[046_IDRes] AS [resource_id]
, i.[046_ExeTime] AS [execution]
, ISNULL(MIN(T48.[048_MachTime]), 0) AS [same]
, ISNULL(MIN(T48_1.[048_MachTime]), 0) AS [all]
, i.[046_Pallet] AS [pallet]
FROM inserted AS i
INNER JOIN Table41 AS T41
ON i.[046_IDOp] = T41.[041_IDOp]
INNER JOIN NoParts AS NP
ON T41.[041_IDNoPart] = NP.Autonumber
INNER JOIN Table48 AS T48
ON i.[046_IDRes] = T48.[048_IDRes]
AND i.[046_IDOp] = T48.[048_IDOp]
INNER JOIN Table48 AS T48_1
ON i.[046_IDOp] = T48_1.[048_IDOp]
GROUP BY i.[046_ID], NP.NoPart, T41.[041_No], i.[046_MachTime],
i.[046_DateTime], i.[046_IDRes], i.[046_ExeTime], i.[046_Pallet];
END;
The original query after the INSERT INTO works, I just changed Table46 for the inserted virtual table because of the trigger.
Edit 1:
If I add a new row manually to Table46 I get the following error (already started MSDTC service):
OLE DB provider "SQLNCLI10" for linked server "[LinkedServer]" returned message "The partner transaction manager has disabled its support for remote/network transactions.".
Msg 7391, Level 16, State 2, Procedure New_Event, Line 5 [Batch Start Line 15]
The operation could not be performed because OLE DB provider "SQLNCLI10" for linked server "[LinkedServer]" was unable to begin a distributed transaction.
Edit 2:
I have followed these instructions and also allowed MSDTC inbound rules in the Firewall of both servers but now if I try to add the row the query takes a lot of time executing, it hasn't finished yet. The same is happening with a SELECT query to Table46.
What are other ways to insert in the remote server whenever Table46 receives a new row, if triggers don't work?
As mentioned in my comment you need to configure MSDTC to enable distributed transactions between the two linked SQL servers.
If you don't want to do that, then you can use a trigger on the source table to save the required data in a 'queue' table. Then you can have a separate application poll the queue table, fetch data and insert them on the linked server on separate connections (and thus separate transactions). This method may seem suboptimal but does have one advantage: if the linked server is unavailable or slow the source server continues to work at full speed and no data is ever lost.
One way to implement the second approach is to use SQL Server broker. In the trigger send the necessary data to a message queue. On the receiving (linked) server process the messages and insert the data in TableEvents. SQL Server Broker ensures transactional integrity all the way without the use of MSDTC between the two servers while decoupling the two servers. Note that the servers no longer need to be linked anymore (unless you need them linked for some other reason)

SSISDB and Always On. How to schedule jobs?

Now that SQL Server 2016 enables SSISDB to be fully High Available, I have a question regarding the job setup.
When I do create a SQL Agent Job that executes a SSIS Package that is deployed in SSISDB, should in the job step the Server be the Listener Name or the physical host name?
I am asking that because if I use the physical host name and create the job in both replicas, the secondary jobs will always fail because the DB is in read only mode. I didn't try placing the Listener name yet, because I wanted to get opinions first.
The server name should be listener name ,if you follow this approach,it is enough to deploy job in one instance
you also can use Physical host names and deploy jobs in all instances,provided you have below piece of code as first step
- fn_hadr_group_is_primary
USE master;
GO
IF OBJECT_ID('dbo.fn_hadr_group_is_primary', 'FN') IS NOT NULL
DROP FUNCTION dbo.fn_hadr_group_is_primary;
GO
CREATE FUNCTION dbo.fn_hadr_group_is_primary (#AGName sysname)
RETURNS bit
AS
BEGIN;
DECLARE #PrimaryReplica sysname;
SELECT
#PrimaryReplica = hags.primary_replica
FROM sys.dm_hadr_availability_group_states hags
INNER JOIN sys.availability_groups ag ON ag.group_id = hags.group_id
WHERE ag.name = #AGName;
IF UPPER(#PrimaryReplica) = UPPER(##SERVERNAME)
RETURN 1; -- primary
RETURN 0; -- not primary
END;
This post also deals with some of the common issues,that needs to be taken care off
https://blogs.msdn.microsoft.com/mattm/2012/09/19/ssis-with-alwayson/

How to detect a SQL Server database's read-only status using T-SQL?

I need to know how to interrogate a Microsoft SQL Server, to see if a given database has been set to Read-Only or not.
Is that possible, using T-SQL?
The information is stored in sys.databases.
SELECT name, is_read_only
FROM sys.databases
WHERE name = 'MyDBNAme'
GO
--returns 1 in is_read_only when database is set to read-only mode.
Querying sys.databases for checking a DB's Read-Only property will only give the right information if the database has been explicitly set to Read-Only mode.
For databases that are in the passive servers (e.g. in AlwaysOn technology Secondary Servers), even though the databases cannot be written into, their Read-Only mode in sys.databases would still be set as False(0).
Hence, it is advisable to check the Read-Only mode of databases using the statement:
SELECT DATABASEPROPERTYEX('MyDBNAme', 'Updateability');
I was trying to use the p.campbell's answer to check if my Azure SQL DB is the primary one or the read only replica - it didn't work. Both the primary DB and the replica returned had 0 on the is_read_only field.
Here's what worked for me:
SELECT DATABASEPROPERTYEX('MyDBNAme', 'Updateability');
the above select statement returns string 'READ_ONLY' or 'READ_WRITE'.
Here is a command to display or set this property.
EXEC sp_dboption "AdventureWorks", "read only"
Sample output
OptionName CurrentSetting
read only OFF
If DB is part of your Always On and the secondary node is designed in Read_Only then
"sys.databases --> Is_Read_Only" column wont show the correct result ! its a bug that Microsoft needs to address it during the next versions.
If you would like to check all DB statuses in your server, use this:
SELECT name, user_access_desc, is_read_only, state_desc, recovery_model_desc
FROM sys.databases;
You can quickly determine your next steps.

Determine Active Node in SQL Failover Cluster

Does anyone know how to determine the active node of a SQL Active-Passive Failover Cluster programmatically from T-SQL?
##SERVERNAME only returns the virtual server name, which is identical from both nodes.
I don't plan to make any decisions based on the data - I trust the failover to do its thing - but I would like to include the information in an event log so I can tell which node in the cluster was active when the event occurred, or help determine if exceptions come up as a result of a failover.
Select ServerProperty('ComputerNamePhysicalNetBIOS')
This works in newer versions:
SELECT * FROM fn_virtualservernodes();
full details at
https://learn.microsoft.com/en-us/sql/relational-databases/system-functions/sys-fn-virtualservernodes-transact-sql?view=sql-server-2017
SELECT * FROM sys.dm_os_cluster_nodes;
full details at https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-cluster-nodes-transact-sql?view=sql-server-2017
Try this, this checks if Server is the Primary server in the Availability Group and then based on this condition do X:
IF EXISTS (SELECT ars.role FROM sys.dm_hadr_availability_replica_states ars JOIN sys.availability_groups ag ON ars.group_id = ag.group_id WHERE ars.role_desc = 'PRIMARY')
BEGIN
SELECT 'PRIMARY' -- DO STUFF IF PRIMARY
END
ELSE
BEGIN
SELECT 'NOT PRIMARY' --- DON'T DO STUFF
END

Resources