Is this a valid parallelism pattern with a System User Connection in Creatio?
Is the below a valid multithreading pattern in Creatio when requiring to use a System User Connection, given that the script task (in my case) has access to a UserConnection?
var appConnection = (AppConnection)UserConnection.AppConnection;
Terrasoft.Core.Tasks.Parallel.ForEach(batchedCustomersToUpdate, new Terrasoft.Core.Tasks.ParallelOptions { MaxDegreeOfParallelism = maxParallelism }, (batch, state, currentBatch) => {
var uc = appConnection.SystemUserConnection;
foreach(var customer in batch) {
var contact = uc.EntitySchemaManager
.GetInstanceByName("Contact")
.CreateEntity(uc);
if (contact.FetchFromDB(customer.customerId)) {
contact.SetColumnValue("UsrColumnName", customer.columnNameValue);
contact.SetColumnValue("UsrDateColumnName", uc.CurrentUser.GetCurrentDateTime());
contact.Save(false);
}
}
});From what I can tell, I think appConnection.SystemUserConnection gets a new SystemUserConnection instance each time it is called, but that's only from having a brief look at the decompiled code based on the method names, so I might be wrong.
I cannot find any examples online of correct multi-threaded usage of User Connections in a similar way, just warnings that a single instance should not be used in multiple threads due to them not being threadsafe. The only documentation I could find was for a fire-and-forget method of running parallel background operations, but in my case I needed the launching code to know when all child processes had finished so it wasn't a great solution for me: https://academy.creatio.com/guides/dev/development-on-creatio-platform/back-end-development/data-operations-back-end/execute-operations-in-the-background/overview
The above code in a script task does appear to run fine & perform the expected updates, but I wanted to check to see if it might cause unexpected intermittent issues?
Like
Short answer: yes, the pattern you posted is valid, and it is essentially the same approach the platform core uses internally. It should not cause intermittent issues on MSSQL or PostgreSQL. A few clarifications and recommendations below.
1. SystemUserConnection returns a shared instance, not a new one per call.
Your reading of the decompiled code is slightly off here. With default settings, AppConnection.SystemUserConnection lazily creates a single SystemUserConnection and returns that same object on every access. So all your parallel branches are working with one connection object.
2. That is fine, because SystemUserConnection is designed for concurrent use.
The class overrides how it obtains a DBExecutor. When the database engine does not use MARS (which is the default for both MSSQL and PostgreSQL in Creatio), the executor is cached per managed thread. Each Parallel.ForEach worker therefore gets its own DBExecutor and its own database connection, and Entity.Save starts and commits its transaction on that thread's executor only. The connection's internal caches are thread-safe collections. The core itself runs Parallel.ForEach loops that create entities on the shared SystemUserConnection, fetch them, and save them, exactly as your script task does.
3. Where the "not thread-safe" warning comes from.
It applies to a regular UserConnection, such as the one your script task is executing under. For a regular connection, the executor lookup will reuse any executor that currently has an open transaction, regardless of which thread owns it. Two threads calling Entity.Save on the same regular UserConnection can end up on the same executor mid-transaction. So the rule is: inside the parallel body, use only the system user connection, and never touch the process's own UserConnection from the worker threads. Your code already follows this.
4. Recommendations.
- Keep using
Terrasoft.Core.Tasks.Parallelrather thanSystem.Threading.Tasks.Parallel. The Creatio wrapper initialises and resets the platform's logical call context for each branch, which you already have. - Keep creating
Entity,Select, andEntitySchemaQueryobjects inside the loop body, one per iteration. Do not share them between branches. Again, your code is already correct here. - Each worker holds a database connection while its batch runs. Keep
MaxDegreeOfParallelismmodest and well below the connection pool size, since regular user requests share the same pool. - Exceptions thrown inside branches are returned as an
AggregateExceptionfromForEach. Wrap the call if you want to log individual batch failures instead of failing the whole process. - Optionally, resolve
appConnection.SystemUserConnectiononce before the loop and capture it. It makes no functional difference since the same instance is returned, but it reads more clearly.
5. On the background operations documentation.
You are right that Task.StartNewWithUserConnection is fire-and-forget and does not let the caller wait for completion. For a case where the launching code needs to know when all work is finished, Terrasoft.Core.Tasks.Parallel.ForEach with the system user connection, as you have it, is an appropriate choice.
Short answer: yes, the pattern you posted is valid, and it is essentially the same approach the platform core uses internally. It should not cause intermittent issues on MSSQL or PostgreSQL. A few clarifications and recommendations below.
1. SystemUserConnection returns a shared instance, not a new one per call.
Your reading of the decompiled code is slightly off here. With default settings, AppConnection.SystemUserConnection lazily creates a single SystemUserConnection and returns that same object on every access. So all your parallel branches are working with one connection object.
2. That is fine, because SystemUserConnection is designed for concurrent use.
The class overrides how it obtains a DBExecutor. When the database engine does not use MARS (which is the default for both MSSQL and PostgreSQL in Creatio), the executor is cached per managed thread. Each Parallel.ForEach worker therefore gets its own DBExecutor and its own database connection, and Entity.Save starts and commits its transaction on that thread's executor only. The connection's internal caches are thread-safe collections. The core itself runs Parallel.ForEach loops that create entities on the shared SystemUserConnection, fetch them, and save them, exactly as your script task does.
3. Where the "not thread-safe" warning comes from.
It applies to a regular UserConnection, such as the one your script task is executing under. For a regular connection, the executor lookup will reuse any executor that currently has an open transaction, regardless of which thread owns it. Two threads calling Entity.Save on the same regular UserConnection can end up on the same executor mid-transaction. So the rule is: inside the parallel body, use only the system user connection, and never touch the process's own UserConnection from the worker threads. Your code already follows this.
4. Recommendations.
- Keep using
Terrasoft.Core.Tasks.Parallelrather thanSystem.Threading.Tasks.Parallel. The Creatio wrapper initialises and resets the platform's logical call context for each branch, which you already have. - Keep creating
Entity,Select, andEntitySchemaQueryobjects inside the loop body, one per iteration. Do not share them between branches. Again, your code is already correct here. - Each worker holds a database connection while its batch runs. Keep
MaxDegreeOfParallelismmodest and well below the connection pool size, since regular user requests share the same pool. - Exceptions thrown inside branches are returned as an
AggregateExceptionfromForEach. Wrap the call if you want to log individual batch failures instead of failing the whole process. - Optionally, resolve
appConnection.SystemUserConnectiononce before the loop and capture it. It makes no functional difference since the same instance is returned, but it reads more clearly.
5. On the background operations documentation.
You are right that Task.StartNewWithUserConnection is fire-and-forget and does not let the caller wait for completion. For a case where the launching code needs to know when all work is finished, Terrasoft.Core.Tasks.Parallel.ForEach with the system user connection, as you have it, is an appropriate choice.
Thanks Oleg, very useful to have this information. Is there any way of safely using a non-System User Connection inside patterns such as the Parallel.ForEach so that any data changes appear as being performed by the instigating user, or is that not possible? I don't need it for this use case, but it's definitely something I've thought about in the past.
Harvey Adcock,
Good question. Yes, it is possible, and there are two ways to do it depending on how far "appears as being performed by the instigating user" needs to go.
Option 1: keep the system connection, set the audit columns explicitly.
If all you need is for ModifiedBy (and CreatedBy for inserts) to show the real user, you do not need a user connection at all. Entity.Save only fills the history columns when they have not been explicitly changed to a non-null value. So this works with your existing pattern:
var initiatorContactId = UserConnection.CurrentUser.ContactId; // captured before the loop
// inside the parallel body, on the system connection
contact.SetColumnValue("UsrColumnName", customer.columnNameValue);
contact.SetColumnValue("ModifiedById", initiatorContactId);
contact.Save(false);This is the simplest and cheapest approach. Its limitation is that everything else still runs as the system user: access-rights checks, default record rights on inserts, and anything downstream that reads UserConnection.CurrentUser (business processes, event listeners, change log) will see Supervisor.
Option 2: one dedicated UserConnection per worker, logged in as the instigating user.
If you need the work to genuinely run in that user's context, create a separate connection per parallel branch. This is public API and is what the platform's own background-task infrastructure does internally:
string userName = UserConnection.CurrentUser.Name;
TimeZoneInfo timeZone = UserConnection.CurrentUser.TimeZone;
var appConnection = UserConnection.AppConnection;
Terrasoft.Core.Tasks.Parallel.ForEach(batches, options, batch => {
var uc = new UserConnection(appConnection) {
SessionId = Guid.NewGuid().ToString()
};
try {
uc.Initialize();
uc.Login(userName, timeZone, needRegisterSessionStart: false);
foreach (var customer in batch) {
var contact = uc.EntitySchemaManager.GetInstanceByName("Contact").CreateEntity(uc);
if (contact.FetchFromDB(customer.customerId)) {
contact.SetColumnValue("UsrColumnName", customer.columnNameValue);
contact.Save(false);
}
}
} finally {
uc.Close(SessionEndMethod.Logout, needRegisterSessionEnd: false);
}
});Notes on this approach:
- One connection per branch, not per record.
Loginreads the user's profile, culture and settings from the database and runs a licence check, so it is not free. Your batching already gives you one branch per batch, which is the right granularity. - The user must be active and licensed, otherwise
Loginthrows. That is the same rule as for an interactive login. - Access rights are enforced for that user, which is usually what you want in this scenario, but it means saves can fail where the system connection would have succeeded.
needRegisterSessionStart: falseandneedRegisterSessionEnd: falsestop the connection from appearing as a separate login in the user session log. Drop them if you want those sessions recorded.- Always close the connection in a
finallyblock. Each one owns its own database executor and connection, andClosealso clears its session-level caches. - Keep parallelism modest. Each branch holds a database connection from the shared pool for the duration of its batch.
- The single-argument
Loginoverload performs no password check. It is intended for trusted server-side code that has already authenticated the user, so never feed it a user name that comes from client input.
What not to do. Do not pass the process's own UserConnection into the worker threads. A regular UserConnection reuses any database executor that currently has an open transaction, regardless of which thread started it, so two parallel Entity.Save calls can end up interleaved on one executor. That is the real source of the "not thread-safe" warnings you found. A connection that is created and used by a single worker thread does not have this problem.
In short: for audit columns alone, set ModifiedById on the system connection. For true user context, create a fresh UserConnection per branch and close it when done.