we have implemented multi-tenant option in our application. Each tenant have each separate DB. using application filter i can manage or assign the each tenant from the request. same how can we do it in the spring boot scheduler?
#component
public class scheduler{
#Scheduled(fixedRate = 5000)
public void reminderEmail() {
//how can we fetch the exact data from exact tenant DB?
//since there is no request how can we get the tenant name for
fetching exact tenant db?
}
}
Please let me know how can we achieve this?
Something like:
...
public class TenantContext {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CONTEXT.set(tenantId);
}
public static String getTenantId() {
return CONTEXT.get();
}
...
}
then your Filter or Spring MVC interceptor could do this just before chaining the request:
String tenantId = request.getHeader(TENANT_HEADER_NAME);
TenantContext.setTenantId(tenantId);
and reset it on the way back:
TenantContext.setTenantId(null);
To use it in a thread not related to an http request you could just do:
TenantContext.setTenantId("tenant_1");
More could be found in my blog post Multi-tenant applications using Spring Boot, JPA, Hibernate and Postgres
If you are using a multitenant setup similar to the one at this link: https://www.ricston.com/blog/multitenancy-jpa-spring-hibernate-part-1/ and/or you have a default tenant. The easiest way to accomplish this is to add a static method to your CurrentTenantIdentifierResolverImpl class that changes the default tenant for asynchronous tasks that have no session. This is because that the scheduled task will always use the default tenant.
CurrentTenantIdentifierResolverImpl.java
private static String DEFAULT_TENANTID = "tenantId1";
public static void setDefaultTenantForScheduledTasks(String tenant) {
DEFAULT_TENANT = tenant;
}
ScheduledTask.java
#Scheduled(fixedRate=20000)
public void runTasks() {
CurrentTenantIdentifierResolverImpl.setDefaultTenantForScheduledTasks("tenantId2");
//do something
CurrentTenantIdentifierResolverImpl.setDefaultTenantForScheduledTasks("tenantId1");
}
Then after the scheduled task is complete change it back. That is how we accomplished it and it works for our needs.
If you're using a request to determine which tenant is currently active and using tenant to determine database connections, then it's impossible to do anything involving the database from a scheduled task since the scheduled task has no tenant id
Related
I have very simillar problem as in this StackOverflow question
We have multi-tenant environment with significant number of tenants. Each of them has physically separated database (with different connection string). Solution proposed in above SO question is not optimal in our case because for large num of tenants (for example 100) it will require to have 200 additional BackgroundServices.
In our project we use EntityFramework Core 7 and SQL server.
To outline our context a bit: connection string is stored within TenantContext class which is registered in DI.
public class TenantContext
{
public string CurrentTenantId { get; set; } = "no-tenant";
public string? ConnectionStr { get; set; }
}
TenantContext is set while creation of IServiceScope specific for tenant.
internal class TenantServiceScopeFactory : ITenantServiceScopeFactory
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public TenantServiceScopeFactory(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public IServiceScope CreateScope(string tenantId)
{
var scope = _serviceScopeFactory.CreateScope();
var tenantContext = scope.ServiceProvider.GetRequiredService<TenantContext>();
tenantContext.CurrentTenantId = tenantId;
return scope;
}
}
DbContext is registered with factory method based on connection string present in TenantContext.
builder.Services.AddDbContext<AppDbContext>((sp, optionsBuilder) =>
{
var tenantContext = sp.GetRequiredService<TenantContext>();
optionsBuilder.UseSqlServer(tenantContext.ConnectionStr);
});
That means that if we want to do anything for specific tenant, we need to create IServiceScope with TenantContext set to specific tenant, and based on that DbContext is created.
If we add MassTransit outbox feature
configurator.AddEntityFrameworkOutbox<AppDbContext>(outboxConfigurator => { … });
then it will do his job for no-tenant TenantContext which always will throw exception because there is no connection string for no-tenant database.
We would like to have single outbox background job (BackgroundService) for all those tenants with logic like this:
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var tenants = new[]
{
"tenant1",
"tenant2"
};
foreach (var tenant in tenants)
{
var tenantScope = _tenantServiceScopeFactory.CreateScope(tenant);
// do MassTransit outbox delivery based on AppDbContext resolved within tenantScope
}
await Task.Delay(Interval, cancellationToken);
}
}
Is it even achievable in current shape of MassTransit?
Tried solution proposed in: Is it possible to use MassTransit Transactional Outbox in a Multi-Tenant per DB architecture?
If the only issue you're experiencing is the need to have a delivery service for each tenant, you should be able to copy the existing delivery service code and modify it so that it delivers for all of your tenants. This isn't something that would be built into MassTransit. Consulting is available if you need help or would like this built for you in your application.
I am using spring-boot-starter-data-mongodb:2.2.1.RELEASE and trying to add transaction support for Mongo DB operations.
I have the account service below, where documents are inserted into two collections accounts and profiles. In case an error occurs while inserting into profile collection, the insertion into accounts should rollback. I have configured Spring transactions using MongoTransactionManager.
#Service
public class AccountService {
#Transactional
public void register(UserAccount userAccount) {
userAccount = accountRepository.save(userAccount);
UserProfile userProfile = new UserProfile(userAccountDoc.getId());
userProfile = profileRepository.save(userProfile);
}
}
Enabled Spring transaction support for MongoDB.
#Configuration
public abstract class MongoConfig extends AbstractMongoConfiguration {
#Bean
MongoTransactionManager transactionManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}
As per Spring reference doc https://docs.spring.io/spring-data/mongodb/docs/2.2.1.RELEASE/reference/html/#mongo.transactions that is all required to enable transactions for MongoDB. But this is not working. Insertions into accounts collection are not rolled back in case error occurs while inserting into profiles collection. Any suggestions if I am missing anything?
I would use command monitoring or examine query logs on the server side to ensure that:
session ids are sent with queries/write commands
transaction operations are performed (there is no startTransaction command but there is a commitTransaction)
I have a Spring MVC application deployed as Multi-Tenant (Single Instance) on Tomcat. All users login to the same application.
User belongs to a Region, and each Region has a separate Database instance.
We are using Dynamic DataSource Routing using Spring AbstractRoutingDataSource".
This works correctly only for the first time, when User_1 from Region_1 logs into the application, Datasource_1 is correctly assigned.
But subsequently when User_2 from Reqion_2 logs into the application, AbstractRoutingDataSource never gets called and Datasource_1 gets assigned.
It looks like Spring AbstractRoutingDataSource is caching the Datasouce.
Is there a way to change this behaviour of AbstractRoutingDataSource and get it working correctly?
You should provide more details for a better understanding.
I think the problem might be related to changing the tenant identifier. You may have a ThreadLocal storage to store the tenant identifier.
public class ThreadLocalStorage {
private static ThreadLocal<String> tenant = new ThreadLocal<>();
public static void setTenantName(String tenantName) {
tenant.set(tenantName);
}
public static String getTenantName() {
return tenant.get();
}
}
AbstractRoutingDataSource should use this to retrieve the tenantId
public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return ThreadLocalStorage.getTenantName();
}
}
And you should set the tenantId on each request for the current thread that is handling the request so that the following operations will be done on the correct database. For you may add a Spring Security filter to extract the tenant identifier from JWT token or from host subdomain.
I've written a custom Validation Annotation and a ConstraintValidator implementation, which uses a Spring Service (and executes a Database Query):
public class MyValidator implements ConstraintValidator<MyValidationAnnotation, String> {
private final MyService service;
public MyValidator(MyService service) {
this.service = service;
}
#Override
public void initialize(MyValidationAnnotation constraintAnnotation) {}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return service.exists(value);
}
}
It's used like this:
public class MyEntity {
#Valid
List<Foo> list;
}
public class Foo {
#MyValidationAnnotation
String id;
}
This works quite nice, but service.exists(value) is getting called for every item within the list, which is correct, but could/should be optimized.
Question:
When validating an instance of MyEntity, I'd like to cache the results of the service.exists(value) calls.
I don't want to use a static HashMap<String, Boolean>, because this would cache the results for the entire application lifetime.
Is it possible to access some kind of Constraint Validation Context, which only exists while this particular validation is running, so I can put there the cached results?
Or do you have some other solution?
Thanks in advance!
You can use Spring's cache support. There might be other parts in the application which needs caching and this can be reused. And the setup is very simple too. And it will keep your code neat and readable.
You can cache your service calls. You need to put annotation on your service methods and a little bit of configuration.
And for cache provider you can use Ehcache. You have many options like setting ttl and max number of elements that can be cached and eviction policy etc etc if needed.
Or you can implement your own cache provider if your needs are simple. And if it is web request, In this cache you may find ThreadLocal to be useful. you can do all caching for this running thread using threadlocal. When the request is processed you can clear the threadlocal cache.
I use Spring Data Rest with Spring Data Mongo.
I have a rather simple REST API which looks similar to this:
public class User {
String id;
String email;
String password;
List<String> roles;
}
public class UserData {
String data;
User user;
}
#PreAuthorize("hasRole('ROLE_USER')")
public interface QueryTemplateRepository extends
MongoRepository<UserData, String> {
}
What I want now is that users can only access their data and if they create/edit data it will be linked to their account.
Do I have to get rid of the MongoRepository and write everything myself? Is there some kind of interceptor or filter where I can do this?
I will want to create more REST APIs that are restricted to the user's data, so it would be great if there was some generic solution to this problem.
You can use features of AbstractMongoEventListener, it has convinient methods for your needs:
void onAfterConvert(DBObject dbo, E source)
void onAfterSave(E source, DBObject dbo)
void onBeforeSave(E source, DBObject dbo)
void onBeforeConvert(E source)
void onAfterLoad(DBObject dbo)
void onApplicationEvent(MongoMappingEvent event)
I think this is a concern you could deal with in your service layer through aspects as a generic approach. AbstractMongoEventListener is a good example of an applied aspect technique.
Because you are dealing with spring-data-rest, no service layer is available unless you wrap it and expose the repository via a Controller.
There is nothing in REST standard regarding allowing modification/deletion of entities only by the creator of it.