Prepared statement caching issue in Cassandra Csharp driver - c#

I believe I have found a bug with the logic of how a prepared statement is cached in the StatementFactory in the Cassandra csharp driver (version 2.7.3). Here is the use case.
Guid key = Guid.NewGuid(); // your key
ISession session_foo = new Session("foo"); //This is pseudo code
ISession session_bar = new Session("bar");
var foo_mapper = new Mapper(session_foo); //table foo_bar
var bar_mapper = new Mapper(session_bar); //table foo_bar
await Task.WhenAll(
foo_mapper.DeleteAsync<Foo>("WHERE id = ?", key),
bar_mapper.DeleteAsync<Bar>("WHERE id = ?", key));
We have found that after running this deletes, only the first request is succeeding. After diving in the the source code of StatementFactory
public Task<Statement> GetStatementAsync(ISession session, Cql cql)
{
if (cql.QueryOptions.NoPrepare)
{
// Use a SimpleStatement if we're not supposed to prepare
Statement statement = new SimpleStatement(cql.Statement, cql.Arguments);
SetStatementProperties(statement, cql);
return TaskHelper.ToTask(statement);
}
return _statementCache
.GetOrAdd(cql.Statement, session.PrepareAsync)
.Continue(t =>
{
if (_statementCache.Count > MaxPreparedStatementsThreshold)
{
Logger.Warning(String.Format("The prepared statement cache contains {0} queries. Use parameter markers for queries. You can configure this warning threshold using MappingConfiguration.SetMaxStatementPreparedThreshold() method.", _statementCache.Count));
}
Statement boundStatement = t.Result.Bind(cql.Arguments);
SetStatementProperties(boundStatement, cql);
return boundStatement;
});
}
You can see that the cache only uses the cql statement. In our case, we have the same table names in different keyspaces (aka sessions). Our cql statement in both queries look the same. ie DELETE FROM foo_bar WHERE id =?.
If I had to guess, I would say that a simple fix would be to combine the cql statement and keyspace together as the cache key.
Has anyone else run into this issue before?

As a simple workaround, I am skipping the cache by using the DoNotPrepare
await _mapper.DeleteAsync<Foo>(Cql.New("WHERE id = ?", key).WithOptions(opt => opt.DoNotPrepare()));
I also found an open issue with Datastax

There is an open ticket to fix this behaviour.
As a workaround, you can use a different MappingConfiguration instances when creating the Mapper:
ISession session1 = cluster.Connect("ks1");
ISession session2 = cluster.Connect("ks2");
IMapper mapper1 = new Mapper(session1, new MappingConfiguration());
IMapper mapper2 = new Mapper(session2, new MappingConfiguration());
Or, you can reuse a single ISession instance and fully-qualify your queries to include the keyspace.
MappingConfiguration.Global.Define(
new Map<Foo>()
.TableName("foo")
.KeyspaceName("ks1"),
new Map<Bar>()
.TableName("bar")
.KeyspaceName("ks2"));
ISession session = cluster.Connect();
IMapper mapper = new Mapper(session);

Related

MongoDB C# Driver: Query interceptors?

Does the MongoDB C# driver support query interceptors like Entity Framework?
I've checked the documentation but can't find anything.
Basically what I need to do is ensure that certain queries to the database, depending on context, always have certain restrictions applied.
For example, if my documents can be soft deleted then I always need to make sure a filter is added for { "SoftDeleted": false }. Entitity Framework handles this gracefully via query interceptors.
MongoClient allows subscribing to CommandStartedEvent. Here is a sample that dumps to console each command sent to the server:
var mongoClient = new MongoClient(new MongoClientSettings
{
Server = new MongoServerAddress("localhost", 27017),
ClusterConfigurator = cb =>
{
cb.Subscribe<CommandStartedEvent>(e =>
{
Console.WriteLine($"{e.CommandName} - {e.Command.ToJson(new JsonWriterSettings { Indent = true })}");
Console.WriteLine(new String('-', 32));
});
}
});
CommandStartedEvent contains CommandName and Command properties that you could use for your specific logic.

How to delete table on Cassandra using C# Datastax driver?

var keyspace = "mydb";
var datacentersReplicationFactors = new Dictionary<string, int>(0);
var replication = ReplicationStrategies.CreateNetworkTopologyStrategyReplicationProperty(datacentersReplicationFactors);
using (var cluster = Cluster.Builder().AddContactPoints("my_ip").Build())
using (var session = cluster.Connect())
{
session.CreateKeyspaceIfNotExists(keyspace, replication, true);
session.ChangeKeyspace(keyspace);
var entityTable = new Table<Models.Entity>(session);
var attributeTable = new Table<Models.Attribute>(session);
entityTable.CreateIfNotExists(); // Worked
attributeTable.CreateIfNotExists(); // Worked
entityTable.Delete(); // Does nothing
attributeTable.Delete(); // Does nothing
}
EDIT: Without using raw queries session.Execute("DROP TABLE entities;"); working fine.
Delete() method is not intended for drop of the tables. It returns representation of a DELETE cql statement. If you call it, you just get {DELETE FROM entities}.
If you need to drop a table, the easiest way is just to execute DROP statement:
session.Execute("DROP TABLE entities;");
Unless there is already a method for dropping tables that I not aware of, you can use this extensions.
public static class DastaxTableExtensions
{
public static void Drop<T>(this Table<T> table)
{
table.GetSession().Execute($"DROP TABLE {table.Name};");
}
public static void DropIfExists<T>(this Table<T> table)
{
table.GetSession().Execute($"DROP TABLE IF EXISTS {table.Name};");
}
}
Then you can use it like this
entityTable.Drop();
attributeTable.DropIfExists();

MongoDb Connect to Replica Set Primary Issue C#

How do I get back the name of the primary database? Lets say database3 was primary
Thanks
var connString = "mongodb://database1,database2,database3/?replicaSet=repl";
var client = new MongoClient(connString);
var server = client.GetServer().Instances.FirstOrDefault(server => server.IsPrimary);
var address = server.Address;
Having looked at the source code of the MongoDB driver, there is no straightforward way to get the name of the primary server from the driver itself. However, you can query server name in MOngoDB by executing {isMaster:1} using RunCommand. You can then parse the primary server from the returned JSON document. This approach works regardless if you are connected to the primary or secondary server.
var mongoClient = new MongoClient(clientSettings);
var testDB = mongoClient.GetDatabase("test");
var cmd = new BsonDocument("isMaster", "1");
var result = testDB.RunCommand<BsonDocument>(cmd);
var primaryServer = result.Where(x => x.Name == "primary").FirstOrDefault().Value.ToString();
Thanks to Jaco for putting me on the right track I solved my problem by doing the following
public static string GetPrimaryDatabase()
{
var mongoClient = new MongoClient(clientSettings);
var server = mongoClient.GetServer();
var database = server.GetDatabase("test");
var cmd = new CommandDocument("isMaster", "1");
var result = database.RunCommand(cmd);
return result.Response.FirstOrDefault(
response => response.ToString().Contains("primary")).Value.ToString();
}
You really should not handle connecting to the right server yourself. The MongoDB driver handles that for you.
Just specify all of your servers in the connections string and the driver will connect to one of them and get replica set's current state on its own. The driver will then direct write operations to the current primary. Read operations may be directed to the primary, or any other server, depending on the read preference you specify.
You can read about forming replica set connection strings here: https://docs.mongodb.org/v3.0/reference/connection-string/

Nhibernate transaction locks a tabel

I have developed a WCF api which is using nHibernate. I am new to this. I have used session.update to take care of transaction. I have a for loop in which based on select condition I am updating a record ie. If A is present in tabel1 then I am updating the table else inserting a new entry.
I am getting "could not execute query." when trying to execute a select query on a table which was previously being updated by adding a new entry in the table.
What I think is, because I am using session.save(table1) and then trying select entries from that table I am getting an error. Since session.save temporarily locks the table I am not able to execute a select query on that table.
What can be the solution on this?
Update:
This the for loop I am using to check in the database for some field:
using (ITransaction tranx = session.BeginTransaction())
{
savefunction();
tranx.Commit();
}
Save function:
public void savefunction()
{
for (int i = 0; i < dictionary.Count; i++)
{
ICandidateAttachmentManager candidateAttach = new ManagerFactory().GetCandidateAttachmentManager();
CandidateAttachment attach = new CandidateAttachment();
attach = checkCV();
if(attach == null)
{
//insert new entry into table attach
session.save(attach);
}
}
}
checkCV function:
public void checkCV()
{
using (ICandidateAttachmentManager CandidateAttachmentManager = new ManagerFactory().GetCandidateAttachmentManager())
{
IList<CandidateAttachment> lstCandidateAttachment = CandidateAttachmentManager.GetByfkCandidateId(CandidateId);
if (lstCandidateAttachment.Count > 0)
{
CandidateAttachment attach = lstCandidateAttachment.Where(x => x.CandidateAttachementType.Id.Equals(FileType)).FirstOrDefault();
if (attach != null)
{
return null;
}
else
{
return "some string";
}
}
}
}
What happening here is in the for loop if say for i=2 the attach value comes to null that I am entering new entry into attach table. Then for i=3 when it enters checkCV function I get an error at this line:
IList lstCandidateAttachment =
CandidateAttachmentManager.GetByfkCandidateId(CandidateId);
I think it is because since I am using session.save and then trying to read the tabel contents I am unable to execute the query and table is locked till I commit my session. Between the beginTransaction and commit, the table associated with the object is locked. How can I achieve this? Any Ideas?
Update:
I read up on some of the post. It looks like I need to set isolation level for the transaction. But even after adding it doesn't seem to work. Here is how I tried to inplement it:
using (ITransaction tranx = session.BeginTransaction(IsolationLevel.ReadUncommitted))
{
saveDocument();
}
something I don't understand in your code is where you get your nHibernate session.
Indeed you use
new ManagerFactory().GetCandidateAttachmentManager();
and
using (ICandidateAttachmentManager CandidateAttachmentManager = new ManagerFactory().GetCandidateAttachmentManager())
so your ManagerFactory class provides you the ISession ?
then you do:
CandidateAttachment attach = new CandidateAttachment();
attach = checkCV();
but
checkCV() returns either a null or a string ?
Finally you should never do
Save()
but instead
SaveOrUpdate()
Hope that helps you resolving your issue.
Feel free to give more details

Entity Framework changing database of existing DbContext

If I have the following code (for example, in the constructor of my repository):
var db = new MyDbContext();
var entity = db.Set<Customer>();
Then later I do:
db.Database.Connection.ConnectionString = mySQLconnectionstring;
Do I need to 're-set' the entity?
Bad idea. You should create new context instance:
var db1 = new MyDbContext("connstr1");
var db2 = new MyDbContext("connstr2");
Otherwise you'll get more difficulties, than benefits you're supposing (if this ever possible). Note, that every context instance keeps local cache of materialized entities and tracks their changes.
Since the model is the same, model building (which is most significant performance hit in EF) will happen just once. I can't imagine, what else could force you to re-use context instances.
if you want to use same context for multiple database, you can do that.
one way is changing connection string of context in the memory.
before changing db that you want to use. Call this piece of code:
var connStr="YOUR_NEW_DB_CONNECTION_STRING_HERE";
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var connectionStringsSection = (ConnectionStringsSection)config.GetSection("connectionStrings");
connectionStringsSection.ConnectionStrings["YOUR_DB_CONNECTION_STRING_NAME_EG_CONN_STR"].ConnectionString = connStr;
connectionStringsSection.ConnectionStrings["YOUR_DB_CONNECTION_STRING_NAME_EG_CONN_STR"].ProviderName = "System.Data.EntityClient";
config.Save();
ConfigurationManager.RefreshSection("connectionStrings");
Write new class file and add this method to your context
public partial class YourEntitities
{
public void SetDatabase(string dataBase)
{
string connString = Database.Connection.ConnectionString;
Regex rgx = new Regex(#"(?<=initial catalog=)\w+");
string newconnString = rgx.Replace(connString, dataBase);
//string = connString.Replace("{database}", dataBase);
Database.Connection.ConnectionString = newconnString;
}
}

Categories

Resources