I am developing a Springboot application, and my requirements are the following:
I have 8 sql queries, that I have to run in parallel and that return a single result.
My entities using this sql requests are:
#SqlResultSetMapping(name = "bud01Mapping", classes = {
#ConstructorResult(targetClass = Bud01Entity.class, columns = {
#ColumnResult(name = "MNTBUDGETMB", type = Double.class),
}
) })
#Entity
#Getter
#Setter
#AllArgsConstructor
public class Bud01Entity {
#Id
private Double mntBudgetMB;
}
To avoid duplication in my post, please consider: Bud01Entity, Bud02Entity ... until Bud08Entity.
My queries are implemented in an hbm.xml file Bud01Entity.hbm.xml as follow:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<entity
class="budget.Bud01Entity"
name="Bud01Entity">
<named-native-query
name="Bud01Entity.findBud01"
result-set-mapping="bud01Mapping">
<query>
SELECT
SUM(CASE WHEN budget.sens='C' THEN -1 * budget.mnt_budget ELSE budget.mnt_budget END) AS mntBudgetProduction,
ent.code as codService
FROM si.t_sb_budget budget
JOIN si.t_sb_rubrique_budget rub ON rub.pk_id_rubrique_budget = budget.fk_id_rubrique_budget
JOIN si.t_ga_activite act ON act.pk_id_activite = budget.fk_id_activite
JOIN si.t_rd_service serv ON act.fk_id_service = serv.pk_id
JOIN si.t_rd_entite ent ON ent.pk_id = serv.pk_id
JOIN si.t_sb_type_budget typ_bud ON typ_bud.pk_id_type_budget=budget.fk_id_type_budget
WHERE budget.exercice = :codAnnee
AND ent.code = :codService
AND typ_bud.pk_id_type_budget = 6
GROUP BY
ent.code
</query>
</named-native-query>
</entity>
</entity-mappings>
As for the first one, to avoid duplication in my post, please consider: Bud01Entity.hbm.xml, Bud02Entity.hbm.xml ... Bud08Entity.hbm.xml.
My service is implemented as follow:
#Async
#Slf4j
#Service
public class Bud01Service {
#Autowired
private Bud01Repository bud01Repository;
public CompletableFuture<List<Bud01Entity>> findBud(String codService, String codAnnee) {
log.info("Running findBud for service {} and annee {}", codService, codAnnee);
List<Bud01Entity> data = bud01Repository.findBud01(codService, codAnnee);
return CompletableFuture.completedFuture(data);
}
}
Same as the other, same logic for Bud02Service, ...Bud08Service.
My repository interface is as follow:
#Repository
public interface Bud01Repository extends JpaRepository<Bud01Entity, Double> {
List<Bud01Entity> findBud01(#Param("codService") String codService, #Param("codAnnee") String codAnnee);
}
Each of this 8 results represent attributes of BudAllEntity:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class BudAllEntity {
private Double mntBudgetProduction;
private Double mntBudgetMB;
private Double mntBudgetRegul;
private Double mntBudgetFraisGenAct;
private Double mntBudgetFraisGenSer;
private Double mntBudgetFraisGenReg;
private Double mntBudgetFraisGenSie;
private Double mntBudgetFraisFin;
}
And my aggregation endpoint:
#Slf4j
#RestController
#RequestMapping("/AggBud")
public class BudAggEndpoint {
#Autowired
private Bud01Service bud01Service;
#Autowired
private Bud02Service bud02Service;
#Autowired
private Bud03Service bud03Service;
#Autowired
private Bud04Service bud04Service;
#Autowired
private Bud05Service bud05Service;
#Autowired
private Bud06Service bud06Service;
#Autowired
private Bud07Service bud07Service;
#Autowired
private Bud08Service bud08Service;
#GetMapping(value = "details", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Page<BudAllEntity> findBudClient(#RequestParam(value = "codService") String codService,
#RequestParam(value = "codAnnee") String codAnnee,
#RequestParam(required = false) Pageable pageRequest) throws ExecutionException, InterruptedException {
CompletableFuture<List<Bud01Entity>> att1 = bud01Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud02Entity>> att2 = bud02Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud03Entity>> att3 = bud03Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud04Entity>> att4 = bud04Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud05Entity>> att5 = bud05Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud06Entity>> att6 = bud06Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud07Entity>> att7 = bud07Service.findBud(codService, codAnnee);
CompletableFuture<List<Bud08Entity>> att8 = bud08Service.findBud(codService, codAnnee);
CompletableFuture.allOf(att1, att2, att3, att4, att5, att6, att7, att8);
log.info("Retrieve Budgets");
List<BudAllEntity> finalResult = new ArrayList<BudAllEntity>();
BudAllEntity result = new BudAllEntity();
try{
result.setMntBudgetProduction(att1.get().get(0).getMntBudgetProduction());
result.setMntBudgetMB(att2.get().get(0).getMntBudgetMB());
result.setMntBudgetRegul(att3.get().get(0).getMntBudgetRegul());
result.setMntBudgetFraisGenAct(att4.get().get(0).getMntBudgetFraisGenAct());
result.setMntBudgetFraisGenReg(att5.get().get(0).getMntBudgetFraisGenSer());
result.setMntBudgetFraisGenSer(att6.get().get(0).getMntBudgetFraisGenReg());
result.setMntBudgetFraisGenSie(att7.get().get(0).getMntBudgetFraisGenSie());
result.setMntBudgetFraisFin(att8.get().get(0).getMntBudgetFraisFin());
} catch (Exception e){
log.error(e.getMessage(),e);
}
finalResult.add(result);
Pageable page = Optional.ofNullable(pageRequest).orElse(PageRequest.of(0, 20));
return new ResponsePage<>(finalResult, page, finalResult.size());
}
}
I have added #EnableAsync annotation in my main class.
Is there a way to make this code more performant, more simple ?
Thanks in advance for your help
The approach you are following looks pretty good, but there are some minute mistakes you need to take care
#Async Annotation It should be annotated on method not class
it must be applied to public methods only
self-invocation – calling the async method from within the same class – won't work
The reasons are simple – the method needs to be public so that it can be proxied. And self-invocation doesn't work because it bypasses the proxy and calls the underlying method directly.
Override the Executor :
By default, Spring uses a SimpleAsyncTaskExecutor to actually run these methods asynchronously.
Override the default executor
#Configuration
#EnableAsync
public class SpringAsyncConfig {
#Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setMaxPoolSize(10);
return pool;
}
}
Then the executor name should be provided as an attribute in #Async:
#Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
System.out.println("Execute method with configured executor - "
+ Thread.currentThread().getName());
}
Note : Make sure you create executor thread pool with N of threads that matches to cpu resources here
Related
I am using Spring for GraphQL to create a small microservice project which consists of 2 apps, a customer service and an order service.
My order service app is running on port 8081 and it contains an OrderData model:
public record OrderData(#Id Integer id, Integer customerId) {}
It also contains an OrderDataRepository interface:
#Repository
public interface OrderDataRepository extends ReactiveCrudRepository<OrderData, Integer> {
Flux<OrderData> getByCustomerId(Integer customerId);
}
And it exposes a single endpoint
#RestController
#RequestMapping(path = "/api/v1/orders")
public class OrderDataController {
private final OrderDataRepository orderDataRepository;
public OrderDataController(OrderDataRepository orderDataRepository) {
this.orderDataRepository = orderDataRepository;
}
#GetMapping
Flux<OrderData> getByCustomerId(#RequestParam Integer customerId) {
return orderDataRepository.getByCustomerId(customerId);
}
}
My customer service app defines the following graphql schema:
type Query {
customers: [Customer]
customersByName(name: String): [Customer]
customerById(id: ID): Customer
}
type Mutation {
addCustomer(name: String): Customer
}
type Customer {
id: ID
name: String
orders: [Order]
}
type Order {
id: ID
customerId: ID
}
And it exposes a few graphql endpoints for querying and mutating customer data, one of which is used to fetch customer orders by using a WebClient to call the endpoint exposed by my order service app:
#Controller
public class CustomerController {
private final CustomerRepository customerRepository;
private final WebClient webClient;
public CustomerController(CustomerRepository customerRepository, WebClient.Builder webClientBuilder) {
this.customerRepository = customerRepository;
this.webClient = webClientBuilder.baseUrl("http://localhost:8081").build();
}
// ...
#QueryMapping
Mono<Customer> customerById(#Argument Integer id) {
return this.customerRepository.findById(id);
}
#SchemaMapping(typeName = "Customer")
Flux<Order> orders(Customer customer) {
return webClient
.get()
.uri("/api/v1/orders?customerId=" + customer.id())
.retrieve()
.bodyToFlux(Order.class);
}
}
record Order(Integer id, Integer customerId){}
My question is how would I refactor this #SchemaMapping endpoint to use #BatchMapping and keep the app nonblocking.
I tried the following:
#BatchMapping
Map<Customer, Flux<Order>> orders(List<Customer> customers) {
return customers
.stream()
.collect(Collectors.toMap(customer -> customer,
customer -> webClient
.get()
.uri("/api/v1/orders?customerId=" + customer.id())
.retrieve()
.bodyToFlux(Order.class)));
}
But I get this error...
Can't resolve value (/customerById/orders) : type mismatch error, expected type LIST got class reactor.core.publisher.MonoFlatMapMany
... because the type of Customer has a orders LIST field and my orders service is returning a Flux.
How can I resolve this problem so I can return a Map<Customer, List<Order>> from my #BatchMapping endpoint and keep it nonblocking?
I assume it's a pretty simple solution but I don't have a lot of experience with Spring Webflux.
Thanks in advance!
I believe that what's missing from your method signature is an additional Mono wrapping it all. You should perform the necessary logic to transform what you return as well. For example, I have this #BatchMapping:
#BatchMapping(typeName = "Artista")
public Mono<Map<Artista, List<Obra>>> obras(List<Artista> artistas){
var artistasIds = artistas.stream()
.map(Artista::id)
.toList();
var todasLasObras = obtenerObras(artistasIds); // service method
return todasLasObras.collectList()
.map(obras -> {
Map<Long, List<Obra>> obrasDeCadaArtistaId = obras.stream()
.collect(Collectors.groupingBy(Obra::artistaId));
return artistas.stream()
.collect(Collectors.toMap(
unArtista -> unArtista, //K, the Artista
unArtista -> obrasDeCadaArtistaId.get(Long.parseLong(unArtista.id().toString())))); //V, the Obra List
});
}
You should replace my Artista for your Customer, and my Obra for your Order. Therefore you will return a Mono<Map<Customer, List>>
I am sorry for the long post, but I believe It is important to mention everything related to the issue.
I am dealing with a requirement for my webservice that sends out notifications for 20k+ users at a time. Since this is quite a heavy task, I thought that having it Async was probably the best approach as it will take some time to process the data. This feature is available for a vast majority of users on the platform, hence there can be multiple requests at once. The amount of users that will receive a notification can vary from 1k to 20k+. Since the request processing takes quite a long time - I basically create a notification, assign it to the correct talents and then send it out in waves. This feature alone seems to have a massive impact on performance when there is multiple concurrent requets for notifications active at the same time and I end up with an out of memory error. I am not sure if this can be optimized at all, or If I should just perhaps choose a completely different approach to everything. I apologize for the long post but I believe it important that I mention everything.
I designed the system to act as follows:
I receive a notification request, which is created in a separate table
I receive a token that indicates which users should get the notification
I fetch the users via a mapped class that is used as a predicate inside of a findAll method (QueryDSL)
I created a relational table taht contains the notificationId, talentId and an extra 'sent' column. Every talent that should receive the message is added to this table along with the notificationId
I have a #Scheduled method that picks up a portion of the notification/talent relations and sends out the notification periodically
My Async configuration class is as follows:
#Component
#Configuration
#EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
#Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(5);
executor.setCorePoolSize(5);
executor.setThreadNamePrefix("asyncExec-");
executor.initialize();
return executor;
}
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
System.out.println("Exception with message :" + ex.getMessage());
System.out.println("Method :" + method);
System.out.println("Number of parameters :" + params.length);
};
}
}
My user entity has the following mapping:
#OneToMany(mappedBy = "talent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TalentRoleNotificationRelations> roleNotifications;
My notification has the following mapping:
#OneToMany(mappedBy = "roleNotification", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TalentRoleNotificationRelations> roleNotifications;
And my relational table entity is as follows:
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class TalentRoleNotificationRelations implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
private TalentRoleNotificationIdentity identity;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("talentId")
#JoinColumn(name = "talent_id")
private Talent talent;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("roleNotificationId")
#JoinColumn(name = "role_notification_id")
private RoleNotification roleNotification;
private Boolean sent;
}
And the composite key identity:
#Embeddable
#Builder
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
#Data
public class TalentRoleNotificationIdentity implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "talent_id")
private String talentId;
#Column(name = "role_notification_id")
private String roleNotificationId;
}
This mapping was done following the guide here which indicates how a many to many relation with an extra column should be implemented.
And the actual process of creating the notification
Controller method:
#PostMapping(value = "notify/{searchToken}/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE)
public RoleNotificationInfo notifyTalentsOfNewRole(#PathVariable String searchToken, #PathVariable String roleId) {
var roleProfileInfo = (RoleProfileInfo) Optional.ofNullable(searchToken)
.map(token -> searchFactory.fromToken(token, RoleProfileInfo.class))
.orElse(null);
return productionService.notifyTalentsOfMatchingRole(roleProfileInfo, roleId);
}
Service method: (This is where I believe the issue is, as well as a very bad use of a many to many table mapping?)
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Async
public RoleNotificationInfo notifyTalentsOfMatchingRole(RoleProfileInfo roleProfileInfo, String roleId) {
var predicate = Optional.ofNullable(userService.getPredicate(roleProfileInfo)).orElse(new BooleanBuilder());
var role = roleDao.findById(roleId).orElseThrow(NoSuchRole::new);
var isNotificationLimitReached = isNotificationLimitReached(roleId);
if (!isNotificationLimitReached) {
var notificationBody = notificationBodyDao.findById(NotificationBodyIdentifier.MATCHING_ROLE)
.orElseThrow(NoSuchNotificationBody::new);
var newNotification = RoleNotification.builder()
.notificationBody(notificationBody)
.role(role)
.build();
roleNotificationDao.saveAndFlush(newNotification);
talentDao.findAll(predicate)
.forEach(talent -> {
var identity = TalentRoleNotificationIdentity.builder()
.talentId(talent.getId())
.roleNotificationId(newNotification.getId())
.build();
var talentRoleNotification = TalentRoleNotificationRelations.builder()
.identity(identity)
.roleNotification(newNotification)
.talent(talent)
.sent(false)
.build();
talentRoleNotification.setIdentity(identity);
talentRoleNotification.setTalent(talent);
talentRoleNotificationRelationsDao.save(talentRoleNotification);
});
return dtoFactory.toInfo(newNotification);
} else throw new RoleNotificationLimitReached();
}
Scheduled method that sends out the notifications:
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Scheduled(fixedDelay = 5)
public void sendRoleMessage() {
var unsentNotificationIds = talentRoleNotificationRelationsDao.findAll(
QTalentRoleNotificationRelations.talentRoleNotificationRelations.sent.isFalse()
.and(QTalentRoleNotificationRelations.talentRoleNotificationRelations.talent.notificationToken.isNotNull()),
PageRequest.of(0, 50)
);
unsentNotificationIds.forEach(unsentNotification -> {
var talent = unsentNotification.getTalent();
var roleNotification = unsentNotification.getRoleNotification();
markAsSent(unsentNotification);
talentRoleNotificationRelationsDao.saveAndFlush(unsentNotification);
notificationPusher.push(notificationFactory.buildComposite(talent.getNotificationToken()), dtoFactory.toInfo(roleNotification));
});
}
The notificationPusher method itself:
#Override
public void push(PushMessageComposite composite, RoleNotificationInfo roleNotificationInfo) {
String roleId = roleNotificationInfo.getRoleId();
String title = "New matching role!";
var push = Message.builder()
.setToken(composite.getMeta().getDeviceToken())
.setAndroidConfig(AndroidConfig.builder()
.setNotification(AndroidNotification.builder()
.setTitle(title)
.setBody(roleNotificationInfo.getBody())
.setSound(composite.getMeta().getSound())
.build())
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setAlert(ApsAlert.builder()
.setTitle(title)
.setBody(roleNotificationInfo.getBody())
.build())
.setBadge(composite.getMeta().getBadge().intValue())
.setSound(composite.getMeta().getSound())
.build())
.build())
.putData("roleId", roleId)
.build();
Try.run(() -> FirebaseMessaging.getInstance().sendAsync(push).get())
.onFailure(e -> {
log.error("Firebase Cloud Messaging failed during sendNotification", e);
var talent = talentDao.findTalentByNotificationToken(composite.getMeta().getDeviceToken());
talent.setNotificationToken(null);
talentDao.save(talent);
});
}
And the dto factory mapper method:
#Transactional(propagation = Propagation.MANDATORY)
public RoleNotificationInfo toInfo(RoleNotification source) {
return RoleNotificationInfo.builder()
.id(source.getId())
.body(source.getNotificationBody().getBody())
.created(source.getCreated())
.roleId(source.getRole().getId())
.build();
}
I am unsure where the problem lies. I assume it is due to the high amount of users fetched from certain queries (20k+). I did some profiling, and this were the results:
Memory/CPU charts:
My question is, should this even be possible at all? Is there a different approach that would be much more efficient? Should I use an external service for this? Is the problem something very obvious that I do not see? I am not sure where to look. If anything in my code is unclear and needs further clarification, please let me know and I'll try to edit and format it the best way I can.
I need some help to find solution for my problem. I created 1 utility class and inject here some CrudRepositories. But repositoryies doesn't works fine here. They returns NullPointerException (Repositories works fine only in controllers).
Here is error and some code.
Error image
Once again, I note that such errors do not appear in the controller.
#Repository
public interface EventRepository extends CrudRepository<Event, Long> {
#Query(nativeQuery = true, value = "select * from events e order by e.id desc LIMIT 5")
List<Event> getEventsWithLimit();
}
#Service
public class CachedObject extends TimerTask {
#Autowired
EventRepository eventRepository;
#Autowired
MatchRepository matchRepository;
#Autowired
PlayerRepository playerRepository;
#Autowired
ImageRepository imageRepository;
List<Rank> ranking;
List<Image> image;
//Last 10 next Matches
List<Match> nextMatches;
//Last 10 results
List<Match> results;
List<PlayerOfTheWeek> playerOfTheWeek;
//Last 5 event
List<Event> events;
#Override
public void run() {
try{
refreshCache();
}
catch (Exception e){
e.printStackTrace();
}
}
public void refreshCache() throws Exception{
events = eventRepository.getEventsWithLimit();
image = imageRepository.getRandomImage();
results = matchRepository.getLastResult();
nextMatches = matchRepository.getLastMatches();
ranking = makeRanking();
...
}
...
}
Can you give me some tips for find solution guys?((
If you want to use solution TimerTask i think u need to create a constructor with needed autowired beans. But better solution in spring is to use #Scheduler annotation to periodically execute needed method. (more about Schedulers in spring https://spring.io/guides/gs/scheduling-tasks/)
I've followed an open Course on Spring web. Written some code to list all orders from a database and return them through a rest api. This works perfectly. Now I'm writing some code to give the ID of the order in the request, find 0 or 1 orders and return them. However, when there is no Order find with the given ID, a nullpointerexception is given. I can't find out what is causing this. I'm assuming the .orElse(null) statement. Please advise
Controller:
#RequestMapping("api/V1/order")
#RestController
public class OrderController {
private final OrderService orderService;
#Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
#GetMapping(path = "{id}")
public Order getOrderById(#PathVariable("id") int id) {
return orderService.getOrderById(id)
.orElse(null);
}
}
Service:
#Service
public class OrderService {
private final OrderDao orderDao;
#Autowired
public OrderService(#Qualifier("oracle") OrderDao orderDao) {
this.orderDao = orderDao;
}
public Optional<Order> getOrderById(int orderNumber) {
return orderDao.selectOrderById(orderNumber);
}
}
Dao:
#Override
public Optional<Order> selectOrderById(int searchedOrderNumber) {
final String sql = "SELECT \"order\", sender, receiver, patient, orderdate, duedate, paymentref, status, netprice from \"ORDER\" where \"order\" = ?";
Order order = jdbcTemplate.queryForObject(sql, new Object[] {searchedOrderNumber}, (resultSet, i) -> {
int orderNumber = resultSet.getInt( "\"order\"");
String sender = resultSet.getString("sender");
String receiver = resultSet.getString("receiver");
String patient = resultSet.getString("patient");
String orderDate = resultSet.getString("orderdate");
String dueDate = resultSet.getString("duedate");
String paymentRef = resultSet.getString("paymentref");
String status = resultSet.getString("status");
int netPrice = resultSet.getInt("netprice");
return new Order(orderNumber,sender,receiver,patient,orderDate,dueDate,paymentRef,status,netPrice);
});
return Optional.ofNullable(order);
}
For the Jdbcexception, use general query instead of the queryForObject, or use try/catch to convert the Jdbc related exception, else Spring itself will handle these internally using ExceptionTranslater, ExceptionHandler etc.
To handle optional case in controllers, just throw an exception there, for example PostController.java#L63
And handle it in the PostExceptionHandler.
Editing based on comment about stack trace
For your error please check - Jdbctemplate query for string: EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
To solve problem associated with orderService.getOrderById(id) returning null you can return ResponseEntity.ResponseEntity gives you more flexibility in terms of status code and header. If you can change your code to return ResponseEntitythen you can do something like
#GetMapping(path = "{id}")
public ResponseEntity<?> getOrderById(#PathVariable("id") int id) {
return orderService
.getOrderById(id)
.map(order -> new ResponseEntity<>(order.getId(), HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
You can even write generic Exception handler using #ControllerAdvice and throw OrderNotFoundException as .orElse(throw new OrderNotFoundException);. Check more information here.
I am currently using the JHipster generator for really boiler plate code which involves HazelCast as a second level cache. I was able to get Multi-tenancy (schema per tenant) working with a header based tenant context. The problem I have now, is that the #Cacheable annotations all share a context. If the cache is hot, I end up with cross-schema data. For example, tenant1 pulls all records from their table which is cached. Tenant 2 goes to pull the same items from their table, the cache is read, and it never goes to the actual tenant db. An easy fix would be disable caching all together but I would like to not do that. I can not for the life of me figure out how to make hazelcast aware of the tenant context - documentation is lacking. Some others have solved this with using custom name resolvers but it doesn't appear to be as dynamic as I was hoping (i.e. you have to know all of the tenants ahead of time). Thoughts?
Current cache config:
#Configuration
#EnableCaching
public class CacheConfiguration implements DisposableBean {
private final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);
private final Environment env;
private final ServerProperties serverProperties;
private final DiscoveryClient discoveryClient;
private Registration registration;
public CacheConfiguration(Environment env, ServerProperties serverProperties, DiscoveryClient discoveryClient) {
this.env = env;
this.serverProperties = serverProperties;
this.discoveryClient = discoveryClient;
}
#Autowired(required = false)
public void setRegistration(Registration registration) {
this.registration = registration;
}
#Override
public void destroy() throws Exception {
log.info("Closing Cache Manager");
Hazelcast.shutdownAll();
}
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
log.debug("Starting HazelcastCacheManager");
return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance);
}
#Bean
public HazelcastInstance hazelcastInstance(JHipsterProperties jHipsterProperties) {
log.debug("Configuring Hazelcast");
HazelcastInstance hazelCastInstance = Hazelcast.getHazelcastInstanceByName("SampleApp");
if (hazelCastInstance != null) {
log.debug("Hazelcast already initialized");
return hazelCastInstance;
}
Config config = new Config();
config.setInstanceName("SampleApp");
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
if (this.registration == null) {
log.warn("No discovery service is set up, Hazelcast cannot create a cluster.");
} else {
// The serviceId is by default the application's name,
// see the "spring.application.name" standard Spring property
String serviceId = registration.getServiceId();
log.debug("Configuring Hazelcast clustering for instanceId: {}", serviceId);
// In development, everything goes through 127.0.0.1, with a different port
if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
log.debug("Application is running with the \"dev\" profile, Hazelcast " +
"cluster will only work with localhost instances");
System.setProperty("hazelcast.local.localAddress", "127.0.0.1");
config.getNetworkConfig().setPort(serverProperties.getPort() + 5701);
config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
String clusterMember = "127.0.0.1:" + (instance.getPort() + 5701);
log.debug("Adding Hazelcast (dev) cluster member {}", clusterMember);
config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(clusterMember);
}
} else { // Production configuration, one host per instance all using port 5701
config.getNetworkConfig().setPort(5701);
config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
String clusterMember = instance.getHost() + ":5701";
log.debug("Adding Hazelcast (prod) cluster member {}", clusterMember);
config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(clusterMember);
}
}
}
config.getMapConfigs().put("default", initializeDefaultMapConfig(jHipsterProperties));
// Full reference is available at: http://docs.hazelcast.org/docs/management-center/3.9/manual/html/Deploying_and_Starting.html
config.setManagementCenterConfig(initializeDefaultManagementCenterConfig(jHipsterProperties));
config.getMapConfigs().put("com.test.sampleapp.domain.*", initializeDomainMapConfig(jHipsterProperties));
return Hazelcast.newHazelcastInstance(config);
}
private ManagementCenterConfig initializeDefaultManagementCenterConfig(JHipsterProperties jHipsterProperties) {
ManagementCenterConfig managementCenterConfig = new ManagementCenterConfig();
managementCenterConfig.setEnabled(jHipsterProperties.getCache().getHazelcast().getManagementCenter().isEnabled());
managementCenterConfig.setUrl(jHipsterProperties.getCache().getHazelcast().getManagementCenter().getUrl());
managementCenterConfig.setUpdateInterval(jHipsterProperties.getCache().getHazelcast().getManagementCenter().getUpdateInterval());
return managementCenterConfig;
}
private MapConfig initializeDefaultMapConfig(JHipsterProperties jHipsterProperties) {
MapConfig mapConfig = new MapConfig();
/*
Number of backups. If 1 is set as the backup-count for example,
then all entries of the map will be copied to another JVM for
fail-safety. Valid numbers are 0 (no backup), 1, 2, 3.
*/
mapConfig.setBackupCount(jHipsterProperties.getCache().getHazelcast().getBackupCount());
/*
Valid values are:
NONE (no eviction),
LRU (Least Recently Used),
LFU (Least Frequently Used).
NONE is the default.
*/
mapConfig.setEvictionPolicy(EvictionPolicy.LRU);
/*
Maximum size of the map. When max size is reached,
map is evicted based on the policy defined.
Any integer between 0 and Integer.MAX_VALUE. 0 means
Integer.MAX_VALUE. Default is 0.
*/
mapConfig.setMaxSizeConfig(new MaxSizeConfig(0, MaxSizeConfig.MaxSizePolicy.USED_HEAP_SIZE));
return mapConfig;
}
private MapConfig initializeDomainMapConfig(JHipsterProperties jHipsterProperties) {
MapConfig mapConfig = new MapConfig();
mapConfig.setTimeToLiveSeconds(jHipsterProperties.getCache().getHazelcast().getTimeToLiveSeconds());
return mapConfig;
}
}
Sample Repository using cacheNames...
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
String USERS_BY_LOGIN_CACHE = "usersByLogin";
String USERS_BY_EMAIL_CACHE = "usersByEmail";
String USERS_BY_ID_CACHE = "usersById";
Optional<User> findOneByActivationKey(String activationKey);
List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime);
Optional<User> findOneByResetKey(String resetKey);
Optional<User> findOneByEmailIgnoreCase(String email);
Optional<User> findOneByLogin(String login);
#EntityGraph(attributePaths = "roles")
#Cacheable(cacheNames = USERS_BY_ID_CACHE)
Optional<User> findOneWithRolesById(Long id);
#EntityGraph(attributePaths = "roles")
#Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
Optional<User> findOneWithRolesByLogin(String login);
#EntityGraph(attributePaths = { "roles", "roles.permissions" })
#Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
Optional<User> findOneWithRolesAndPermissionsByLogin(String login);
#EntityGraph(attributePaths = "roles")
#Cacheable(cacheNames = USERS_BY_EMAIL_CACHE)
Optional<User> findOneWithRolesByEmail(String email);
Page<User> findAllByLoginNot(Pageable pageable, String login);
}
I am using tenant per database (MySQL), but as long as you are setting a thread context above here is what I'm doing - I'm using Spring Boot. I've created a custom Cache Key generator which combines the tenant name + class + and method. You can really choose any combination. Whenever I pass that tenant back it pulls the correct entries. In the Hazelcast command center for my AppointmentType map type I see the number of entries increment per tenant.
Some other references that may be helpful:
https://www.javadevjournal.com/spring/spring-cache-custom-keygenerator/
https://docs.spring.io/spring-framework/docs/4.3.x/spring-framework-reference/html/cache.html (search for keyGenerator="myKeyGenerator")
In your class where you want to cache (mine is a service class):
#Service
public class AppointmentTypeService {
private static final Logger LOGGER = LoggerFactory.getLogger(AppointmentTypeService.class);
private final AppointmentTypeRepository appointmentTypeRepository;
#Autowired
AppointmentTypeService(AppointmentTypeRepository appointmentTypeRepository) {
this.appointmentTypeRepository = appointmentTypeRepository;
}
//ADD keyGenerator value. Name is the name of the bean of the class
#Cacheable(value="appointmentType", keyGenerator = "multiTenantCacheKeyGenerator")
public List<AppointmentType> list() {
return this.appointmentTypeRepository.findAll();
}
#CacheEvict(value="appointmentType", allEntries=true)
public Long create(AppointmentType request) {
this.appointmentTypeRepository.saveAndFlush(request);
return request.getAppointmentTypeId();
}
#CacheEvict(value="appointmentType", allEntries=true)
public void delete(Long id) {
this.appointmentTypeRepository.deleteById(id);
}
public Optional<AppointmentType> findById(Long id) {
return this.appointmentTypeRepository.findById(id);
}
}
Create key generator class
//setting the bean name here
#Component("multiTenantCacheKeyGenerator")
public class MultiTenantCacheKeyGenerator implements KeyGenerator {
#Override
public Object generate(Object o, Method method, Object... os) {
StringBuilder sb = new StringBuilder();
sb.append(TenantContext.getCurrentTenantInstanceName()) //my tenant context class which is using local thread. I set the value in the Spring filter.
.append("_")
.append(o.getClass().getSimpleName())
.append("-")
.append(method.getName());
}
return sb.toString();
}
}
One approach to defining different cache keys for the tenants is to override the method getCache in org.springframework.cache.CacheManager, as suggested here: Extended spring cache...
As of Jhipster 7.0.1, the CacheManager for Hazelcast is defined in the class CacheConfiguration as stated bellow:
#Configuration
#EnableCaching
public class CacheConfiguration {
//...
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance);
}
//...
}
To have the cache keys prefixed with the tenant id, the following code may be used as a starting point:
#Configuration
#EnableCaching
public class CacheConfiguration {
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance){
#Override
public Cache getCache(String name) {
String tenantId = TenantStorage.getTenantId();
if (StringUtils.isNotBlank(tenantId)){
return super.getCache(String.format("%s:%s", tenantId, name));
}
return super.getCache(name);
}
};
}
}
Note: in the above code, TenantStorage.getTenantId() is a static function one should implement and that returns the current tenant id.
Consider the class posted by the OP:
#Cacheable(cacheNames = "usersByLogin")
Optional<User> findOneWithRolesByLogin(String login);
The following cache values will be used by HazelCast:
tenant1 => tenant1:usersByLogin
tenant2 => tenant2:usersByLogin
null => usersByLogin