Using .Net 4.52 and SQL Server 2014 with FILESTREAM
We have a webservice that is failing on concurrent reads with "System.InvalidOperationException: The process cannot access the file specified because it has been opened in another transaction."
I have isolated the code to reproduce the failure in a test program that spawns 10 concurrent tasks that read the same data with IsolationLevel.ReadCommitted and IO.FileAccess.Read. My understanding is thatthere will be a shared lock both in the database and the file system and there should be no "blocking".
With a single task, the code works consistently. Sometimes it works with 2-3 tasks. With 10 tasks the code fails nearly consistently - every once in a while it works. I have considered that other programmers might be accessing the data since the database is on one of our development servers, but that wouldn't explain the nearly consistent failure with 10 tasks.
Any suggestions as to what could be causing the failure would be greatly appreciated!
The code driving the test:
Dim profileKey As Guid = New Guid("DC3F1949-37DB-4D47-B204-0170FA4A40CD")
Dim taskList As List(Of Task) = New List(Of Task)
For x = 1 To 10
Dim tsa As New TestSqlFileStream
taskList.Add(Task.Run(Function() tsa.GetProfiles(profileKey, True)))
Next
Task.WaitAll(taskList.ToArray)
The class under test:
Public Class TestSqlFileStream
Public Function GetProfiles(profileKey As Guid, getSmallVersionOfImage As Boolean) As List(Of Profile)
Dim retProfiles = New List(Of Profile)
Using conn As New SqlConnection("server=blah,1435; initial catalog=blah;Trusted_Connection=Yes;")
conn.Open()
Dim cmd = conn.CreateCommand()
Dim iso As IsolationLevel = IsolationLevel.ReadCommitted
cmd.Transaction = conn.BeginTransaction(iso)
Try
cmd.CommandText = "GetProfiles"
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.Add(New SqlParameter("#profileKey", SqlDbType.UniqueIdentifier)).Value = profileKey
Using reader As SqlDataReader = cmd.ExecuteReader
retProfiles = MapGetProfiles(reader, getSmallVersionOfImage)
End Using
cmd.Transaction.Commit()
Catch ex As Exception
cmd.Transaction.Rollback()
Throw
Finally
conn.Close()
End Try
End Using
Return retProfiles
End Function
Public Function MapGetProfiles(reader As SqlDataReader, getSmallVersionOfImage As Boolean) _
As List(Of Profile)
Dim profiles As New List(Of Profile)
Dim transactionToken As Byte()
Try
While reader.Read()
Dim profile As New ServiceTypes.SubTypes.Profile
profile.ParentKey = reader("ParentKey")
profile.ProfileKey = reader("ProfileKey")
profile.ProfileType = ConvertToProfileType(reader("ProfileType"))
If reader("Active") Is Nothing Then profile.Active = False Else profile.Active = reader("Active")
If IsDBNull(reader("Data")) Then profile.Data = Nothing Else profile.Data = reader("Data")
Dim imagePath
If getSmallVersionOfImage Then imagePath = reader("ImageThumbnailPath") Else imagePath = reader("ImagePath")
transactionToken = DirectCast(reader("transactionContext"), Byte())
If Not IsDBNull(imagePath) Then
If Not transactionToken.Equals(DBNull.Value) Then
LoadImage(profile, imagePath, transactionToken)
End If
End If
profiles.Add(profile)
End While
Catch ex As Exception
Throw
Finally
reader.Close()
End Try
Return profiles
End Function
Public Sub LoadImage(ByRef profile As Profile, image As String, transactionContext As Byte())
Using sqlFileStream = New SqlFileStream(image, transactionContext, IO.FileAccess.Read, FileOptions.SequentialScan, 0)
Dim retrievedImage = New Byte(sqlFileStream.Length - 1) {}
sqlFileStream.Read(retrievedImage, 0, sqlFileStream.Length)
profile.Image = retrievedImage
sqlFileStream.Close()
End Using
End Sub
Private Function ConvertToProfileType(profileType As String) As ProfileType
Dim type = ServiceTypes.SubTypes.ProfileType.None
Select Case profileType
Case Nothing
type = ServiceTypes.SubTypes.ProfileType.None
End Select
Return type
End Function
End Class
Update: I have looked at this question but the issue is different because they were splitting to parallel within a transaction: Threading and SqlFileStream. The process cannot access the file specified because it has been opened in another transaction
In my example each task starts its own transaction.
Update2 When I stop at a breakpoint within the transaction and run DBCC OPENTRAN in a query window the result is "No active open transactions" It seems like SqlConnection.BeginTransaction is not actually opening a transaction in the database.
Update3 Also read from transaction log (after naming the transaction):
Use myDB
GO
select top 1000 [Current LSN],
[Operation],
[Transaction Name],
[Transaction ID],
[Transaction SID],
[SPID],
[Begin Time]
FROM fn_dblog(null,null)
order by [Begin Time] desc
No transaction of the name I provided is showing in the log.
Note: This is only ok for solutions where the atomicity of the retrieved set is not critical. I suggest a TransactionScope for better atomicity.
It seems like the transaction queue/locking mechanism gets confused when many files are retrieved under the transaction (the While Reader.Read loop). I broke out the file retrieval to use a new transaction or each file retrieval and can run 100 parallel tasks against the same hierarchical set of profiles for a single parent.
Related
Here's my .mdf database file that has 5 columns
I want to add each of those values from my Id column in a list
Private Sub Read_Click(sender As Object, e As EventArgs) Handles Read.Click
Try
If con.State = ConnectionState.Open Then
con.Close()
End If
con.Open()
cmd = con.CreateCommand()
cmd.CommandType = CommandType.Text
cmd.CommandText = "SELECT Id FROM tablekongbago"
cmd.ExecuteNonQuery()
Dim dr As SqlClient.SqlDataReader
dr = cmd.ExecuteReader(CommandBehavior.CloseConnection)
While dr.Read
element = dr.GetInt32(0).ToString()
End While
Catch ex As Exception
End Try
MessageBox.Show(element)
End Sub
The problem is that I can only retrieve the last row of my Id column and not all of the values from my Id column using
element = dr.GetInt32(0).ToString()
If I try to iterate and turn it into
dr.GetInt32(1).ToString()
it displays nothing.
I want to create a collection of Id's to a List(Of Integer) I know how to create a list and a for loop but I don't know how can I retrieve all of my Id's from my Id column, what kind of code should I use if "dr.GetInt32(0)" is only for the last row of the Id column?, is there a way I can loop starting from the very first top row up to the last row of my Id column? I want something like "list[0] - referring to the first row and list[2] - referring to the last row, so that I can add it my List(Of Integer).
I cringe whenever I see If con.State = ConnectionState.Open Then. Connections should be declared in the method where they are used. You should never have to question the ConnectionState.
You have executed your command twice. A Select in not a NonQuery. NoQuery is Insert, Update and Delete.
Your While loop keeps overwriting the element varaiable on each iteration so you only get the value in the last record.
Never write an empty Catch block. It will just swallow errors and you may get unexpected results with no clue why.
It is a good idea to separate you database code from you user interface code.
Create your connection and command with a Using...End Using block so you know they are properly disposed. Likewise with the reader. I like to do as little as possible with a reader because it requires and open connection and connections should be open for as short a time as possible.
Private ConStr As String = "Your connection string"
Private Sub Read_Click(sender As Object, e As EventArgs) Handles Read.Click
Dim dt As DataTable
Try
dt = GetIds()
Catch ex As Exception
MessageBox.Show(ex.Message)
Return
End Try
Dim ListOfIDs = (From row As DataRow In dt.AsEnumerable
Select CInt(row(0))).ToList
ListBox1.DataSource = ListOfIDs
End Sub
Private Function GetIds() As DataTable
Dim dt As New DataTable
Using con As New SqlConnection(ConStr),
cmd As New SqlCommand("SELECT Id FROM tablekongbago;", con)
con.Open()
Using reader = cmd.ExecuteReader
dt.Load(reader)
End Using
End Using
Return dt
End Function
You can simply create a List of Integer and add the ids to your collection during each call to dr.Read()
Dim ids = New List(Of Integer)()
While dr.Read()
ids.Add(dr.GetInt32(0))
End While
You code looks a bit messed up. This should work:
Note that a sql command object is VERY nice.
It has a reader built in - you don't need to define one
It has the command text - you don't need to define one
it has a connection object - again no need to create one (but you look to have one)
And using a dataTable is nice, since you can use for/each, or use the MyTable.Rows(row num) to get a row.
And a datatable is nice, since you don't need a loop to READ the data - use the built in datareader in sqlcommand object.
Using cmdSQL As New SqlCommand("Select Id FROM tblekingbago", con)
cmdSQL.Connection.Open()
Dim MyTable As New DataTable
MyTable.Load(cmdSQL.ExecuteReader)
' table is now loaded with all "ID"
' you can see/use/display/play/have fun with ID like this:
For Each OneRow As DataRow In MyTable.Rows
Debug.Print(OneRow("Id"))
Next
' display the 5th row (it is zero based)
Debug.Print(MyTable.Rows(4).Item("Id"))
End Using
I have been dying in solving the issue in my code my goal is to simply look for the account relative to the user's input. I have been encountering closed states of my record set and or having no response at all from my program i need some clarifications on my code and I also would like to know the best practices in implementing a ADODB connection as SQL query
Dim WithEvents ErrorMessageTimer As New DispatcherTimer
Private Sub BTNLogin_Click(sender As Object, e As RoutedEventArgs) Handles BTNLogin.Click
If (Trim(FLDUsername.Text) = "") Then
LBLErrorMessage.Text = "Username is Empty"
LBLErrorMessage.Visibility = Visibility.Visible
ErrorMessageTimer.Interval = TimeSpan.FromSeconds(2.7)
ErrorMessageTimer.Start()
Else
Dim dbCon As New ADODB.Connection
Dim dbRecSet As New ADODB.Recordset
dbCon.Open("PROVIDER=Microsoft.jet.oledb.4.0;Data Source=inventory.mdb")
dbRecSet.Open("SELECT * FROM [User] WHERE Username='" & FLDUsername.Text & Chr(39), dbCon, ADODB.CursorTypeEnum.adOpenKeyset, ADODB.LockTypeEnum.adLockOptimistic)
Try
If (dbRecSet.Fields("Username").Value = FLDUsername.Text) And (dbRecSet.Fields("Password").Value = FLDPassword.Password) Then
Dim mainMenu As New MainMenuWindow
Me.Hide()
mainMenu.Show()
Else
ErrorMessageTimer.Start()
LBLErrorMessage.Text = "! Invalid Credentials"
End If
Catch ex As Exception
ErrorMessageTimer.Start()
LBLErrorMessage.Text = "! Account not Found"
End Try
dbCon.Close()
dbRecSet.Close()
End If
End Sub
I will repeat my comments here so you don't miss them.
Dump the ancient ADODB and switch to ADO.net. No Com interop.
Do not concatenate strings with user input to build Sql commands. You are risking Sql injection.
NEVER store passwords as plain text.
Using Parameters avoids the risk of Sql injections because the values will not be considered to be executable code. For OleDb (Access) the names of the parameters do not matter. It is the order that they are added to the ParametersCollection must match the order that they appear in the Sql command. Not only will parameters protect your database, they make it easier to write the Sql. No single quotes, double quotes, ampersands.
The Using...End Using blocks will ensure that your database objects are closed and disposed even if there is an error.
I don't think your Data Source = inventory.mdb will be sufficient for the file to be found. A complete path would be better.
I will leave it to you to research how to salt and hash passwords for storage and then retrieve and compare to user input.
Private Sub BTNLogin_Click(sender As Object, e As RoutedEventArgs) Handles BTNLogin.Click
If FLDUsername.Text = "" OrElse FLDPassword.Text = "" Then
'Make this visible at Design time
LBLErrorMessage.Text = "Please fill in both fields."
'I have no idea what you are trying to do with your timer.
Return
End If
Dim RetVal As Integer
Try
Using dbCon As New OleDbConnection("PROVIDER=Microsoft.jet.oledb.4.0;Data Source=inventory.mdb")
Using cmd As New OleDbCommand("SELECT Count(*) FROM [User] WHERE [Username] = #UserName And [Password] = #Password;", dbCon)
cmd.Parameters.Add("#UserName", OleDbType.VarChar, 50).Value = FLDUsername.Text
cmd.Parameters.Add("#Password", OleDbType.VarChar, 50).Value = FLDPassword.Text
dbCon.Open()
RetVal = CInt(cmd.ExecuteScalar())
End Using
End Using
Catch ex As Exception
'An error here is probably due to invalid connection of network error
'Show the error message not "! Account not Found"
MessageBox.Show(ex.Message)
Return
End Try
'Now your connection is closed and your objects disposed.
'Only after the last End Using do we evaluate the results of our query
'and take action.
If RetVal = 1 Then
Dim mainMenu As New MainMenuWindow
Me.Hide()
mainMenu.Show()
Else
LBLErrorMessage.Text = "! Invalid Credentials"
End If
End Sub
Preparing an application which will be used by around 40 users in office with local SQL Server at local network. Application developed in VB.NET. I already read some documentation but would like to get some knowledge directly from your side about access to data.
This is a Winforms app and I wonder whether transactions I am using will be just enough to protect data e.g when one user uses some data and other one will change it in same time, does transaction would protect it? Can someone explain me briefly how it is?
Example of SQL transaction I use in my application
Dim result As Boolean = True
Dim strcon = New AppSettingsReader().GetValue("ConnectionString", GetType(String)).ToString()
Using connection As New SqlConnection(strcon)
'-- Open generall connection for all the queries
connection.Open()
'-- Make the transaction.
Dim transaction As SqlTransaction
transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted)
Try
For Each sentId In pSentsId
'-- Insert aricle to Article's table (T_Artikel) and get inserted row id to use it in other queries
Using cmd As New SqlCommand("INSERT INTO T_Sentence_SubSec_SecKatSubKat_SubSubKat (FK_Sentence_ID, FK_SubSec_SecKatSubKat_SubSubKat) VALUES (#FK_Sentence_ID, #FK_SubSec_SecKatSubKat_SubSubKat)", connection)
cmd.CommandType = CommandType.Text
cmd.Connection = connection
cmd.Transaction = transaction
cmd.Parameters.AddWithValue("#FK_Sentence_ID", sentId)
cmd.Parameters.AddWithValue("#FK_SubSec_SecKatSubKat_SubSubKat", SubSec_SecKatSubKat_SubSubKat)
cmd.ExecuteScalar()
End Using
Next
transaction.Commit()
Catch ex As Exception
result = False
'-- Roll the transaction back.
Try
transaction.Rollback()
Catch ex2 As Exception
' This catch block will handle any errors that may have occurred
' on the server that would cause the rollback to fail, such as
' a closed connection.
'Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType())
'Console.WriteLine(" Message: {0}", ex2.Message)
End Try
End Try
End Using
Return result
There are two versions according to your business rules.
A) All inserts succeed or all fail.
Dim result As Boolean = True
Dim strcon = New AppSettingsReader().GetValue("ConnectionString", GetType(String))'.ToString()'no need. It is already **String**
Using connection As New SqlConnection(strcon)
''//--Following 2 lines are OK
''//--Using cmd As New SqlCommand("INSERT INTO T_Sentence_SubSec_SecKatSubKat_SubSubKat (FK_Sentence_ID, FK_SubSec_SecKatSubKat_SubSubKat) VALUES (#FK_Sentence_ID, #FK_SubSec_SecKatSubKat_SubSubKat)", connection)
''//--cmd.CommandType = CommandType.Text
''//--but these two are better
Using cmd As New SqlCommand("dbo.T_Sentence_SubSec_SecKatSubKat_SubSubKat_ins ", connection)
''//-- Insert aricle to Articles table (T_Artikel) and get inserted
''//--row id to use it in other queries
cmd.CommandType = CommandType.StoredProcedure
''//-- Open generall connection for all the queries
''//--open connection in try block
''//--connection.Open()
''//-- Make the transaction.
''//--Dim transaction As SqlTransaction
''//--transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted)
Try
connection.Open()
cmd.Transaction = connection.BeginTransaction()
cmd.Parameters.Add("#FK_Sentence_ID", SqlDbType.Int) ''//--or whatever
cmd.Parameters.Add("#FK_SubSec_SecKatSubKat_SubSubKat", SqlDbType.Int)
For Each sentId In pSentsId
cmd.Parameters("#FK_Sentence_ID").Value = sentId
cmd.Parameters("#FK_SubSec_SecKatSubKat_SubSubKat").Value = SubSec_SecKatSubKat_SubSubKat
cmd.ExecuteNonQuery() ''//--returns rows affected. We do not use result
Next
''//--everything is OK
cmd.Transaction.Commit()
Catch ex as SqlException
result = False
''//--SqlException is more informative for this case
If cmd.Transaction IsNot Nothing
cmd.Transaction.Rollback
''//--extra try...catch if you wish
End If
Catch ex As Exception
result = False
''//-- Roll the transaction back.
Try
cmd.Transaction.Rollback()
Catch ex2 As Exception
''// This catch block will handle any errors that may have occurred
''// on the server that would cause the rollback to fail, such as
''// a closed connection.
''//Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType())
''//Console.WriteLine(" Message: {0}", ex2.Message)
End Try
Finally
If connection.State <> Closed
connection.Close()
End If
End Try
End Using''//cmd
End Using''//connection
Return result
B) Each insert is independent. Don't use overall transaction. Add try...catch inside for loop.
I checked whether a value exists
Dim connectionString = [connection string ]
Using exist As New SqlConnection(connectionString)
Dim cmd As SqlCommand = New SqlCommand("SELECT * FROM Employees WHERE WorkEmail = #WorkEmail", exist)
cmd.Parameters.AddWithValue("#WorkEmail", DataObjects.Contacts.ElectronicAddress.Email)
If cmd.ExecuteScalar > 0 Then
//return row so that I can grab values from it, given column names
How would I go about doing that commented section on the last line?
You should execute your command and then check if it returned any rows.
See Retrieving Data Using a Data Reader
An excerpt slightly tweaked for your use case:
Private Sub HasRows(ByVal connection As SqlConnection)
Using connection
Using cmd As SqlCommand = New SqlCommand("SELECT * FROM Employees WHERE WorkEmail = #WorkEmail", connection)
cmd.Parameters.AddWithValue("#WorkEmail", DataObjects.Contacts.ElectronicAddress.Email)
connection.Open()
Using reader As SqlDataReader = cmd.ExecuteReader()
While reader.Read()
REM Your code to pull the data you want from the returned data goes here
End While
End Using
End Using
End Using
End Sub
In the future try looking through the Microsoft Documents. You will usually find what you need there.
i need to call a function that do some query while another connection is opened and its doing a transaction.
Ok i get this is weird, here some code:
Main part:
Using connection As New SqlConnection(connectionString)
connection.Open()
Dim command As SqlCommand = connection.CreateCommand()
Dim transaction As SqlTransaction
transaction = connection.BeginTransaction("myTransaction")
command.Connection = connection
command.Transaction = transaction
command.CommandText = sSQL
Try
command.ExecuteNonQuery()
Dim functionResult As String = myFunction(param1, param2)
If functionResult <> "" Then
'error! i need to rollback the first query done here!
transaction.Rollback()
else
transaction.Commit()
End If
Catch ex As Exception
transaction.Rollback()
End Try
End If
End Using
myFunction do lot of stuff, and a lot of querys. Every query needs to reopen connection (without transaction this time) but everytime i try to execute the first query inside my function i got timeout error from database (after 30 seconds).
I know i can do this work "copy-pasting" all the myFunction code inside that already opened connection and using the already opened connection, but i use that function more than once and i don't want to mess up my code.
How can i solve this?
edit for more information:
that was an already reduced version of the code i'm using, but here a reduced version on what "myFunction" do:
Dim connectionString As String = "my connection string"
Dim queryString As String = "SELECT id FROM foo WHERE param1 = #myValue"
Dim ds As DataSet = New DataSet()
Try
Using connection As New SqlConnection(connectionString)
Dim command As New SqlCommand(queryString, connection)
connection.Open()
command.CommandText = queryString
command.Parameters.Add("#myValue", SqlDbType.Int).Value = 10
Dim adapter As New SqlDataAdapter()
adapter.SelectCommand = command
adapter.Fill(ds, "randomName")
If ds.Tables("randomName").Rows.Count < 0 Then
'error!
connection.Close()
Return "error"
End If
End Using
Catch ex As Exception
Return "Database error - " & ex.Message
End Try
The code execution (even in debug) freeze on the adapter.Fill(ds, "randomName") command for 30 seconds, after that i get a timout error
You can use as many connections as you want, just make sure they don't interfere with each other. SQL server is very diligent about preserving data integrity, so if one uncommitted transaction conflicts with another uncommitted transaction, you get a deadlock.
You may want to play with transaction isolation level, default is READ COMMITTED for SQL server, try to set it to READ UNCOMMITTED. Please read the docs to be aware of the consequences.
From the above link:
In SQL Server, you can also minimize locking contention while protecting transactions from dirty reads of uncommitted data modifications using either:
The READ COMMITTED isolation level with the READ_COMMITTED_SNAPSHOT database option set to ON.
The SNAPSHOT isolation level.