#Override
#Transactional
public UserDetails loadUserByUsername(String username) {
log.debug("Load user {}", username);
UserDetails userDetails = userService.findUserDetailsByName(username).get();
return userDetails;
}
When the user authorizes, the loadUserByUsername method calls the findUserDetailsByName method from the userService. In this method, when authorizing a user, Spring writes the user's data to cache and does not contact the database the next time.
My task is to delete this cache within 10 minutes, so that Spring can re-enter the database, since roles can be changed. I know a solution using #Scheduled, but I think the given approach is wrong. Can you suggest an acceptable option for clearing the cache every 10 minutes?
#Component
#Slf4j
public class Customizer implements CacheManagerCustomizer<ConcurrentMapCacheManager> {
#Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(List.of(Caches.UD_CACHE));
}
#CacheEvict(allEntries = true, value = {Caches.UD_CACHE})
#Scheduled(fixedDelay = 10 * 60000)
public void reportCacheEvict() {
log.info("Flush cache " + Caches.UD_CACHE + " at " + new Date());
}
}
private final Cache userDetailsCache;
public UserService(CacheManager cacheManager) {
this.userDetailsCache = cacheManager.getCache(Caches.USER_DETAILS_CACHE);
}
public Optional<UserDetails> findUserDetailsByName(String username) {
ValueWrapper v= userDetailsCache.get(username);
if (v== null) {
Optional<UserDetails> userDetailsOpt =
userRepository.findOne(QUser.user.userName.eq(username)).map(UserService::createFrom);
userDetailsOpt.ifPresent(u -> userDetailsCache.put(username, u));
return userDetailsOpt.map(u -> withUserDetails(u).build());
} else {
return Optional.ofNullable((UserDetails) v.get()).map(u -> withUserDetails(u).build());
}
}
public final class Caches {
public static final String UD_CACHE = "udCache";
}
In you example you use fixedDelay, but since you want to clear the cache every 10 mins you should use a fixedRate.
public class Customizer implements CacheManagerCustomizer<ConcurrentMapCacheManager>{
#Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(List.of(Caches.UD_CACHE));
}
#CacheEvict(allEntries = true, value = {Caches.UD_CACHE})
#Scheduled(fixedRate = 10 * 60000)
public void evictUDCache() {
log.info("Flush cache " + Caches.UD_CACHE + " at " + new Date());
}
}
EDIT
thanks. but I'm interested in the option without #Scheduled
You can inject the TaskScheduler and then use it when you need it.
TaskScheduler taskScheduler = ....;
Cache udCache = cacheManager.getCache(Caches.UD_CACHE);
Duration clearCacheInterval = Duration.of(10L, ChronoUnit.MINUTES);
taskScheduler.scheduleAtFixedRate(udCache::clear, clearCacheInterval);
or schedule with a fixed delay
taskScheduler.scheduleWithFixedDelay(udCache::clear, clearCacheInterval);
Related
I'm trying to make a mute command for a discord bot where it removes all the user's roles and gives them the Muted Role and then after a certain amount of time it gives them their old roles back. I am trying to use a Hashmap to store the player's roles and then give it back but it seems to not work. If anyone here could help I would be really thankful. Here is my code:
public static HashMap<List, Role> roleMap = new HashMap<>();
#Override
public void onSlashCommandInteraction(#NotNull SlashCommandInteractionEvent event) {
String command = event.getName();
if (command.equalsIgnoreCase("mute")) {
Member user = event.getOption("user").getAsMember();
roleMap.put(event.getOption("user").getAsMember().getRoles(), event.getGuild().getRoleById(id));
event.getGuild().modifyMemberRoles(event.getOption("user").getAsMember(), event.getGuild().getRoleById(id)).queue();
event.getChannel().sendMessage(event.getOption("user").getAsUser().getAsMention() + " has been muted").queue();
if (!event.getMember().hasPermission(Permission.MANAGE_ROLES)) {
event.reply("You do not have the required permissions to use this command!").setEphemeral(true).queue();
}
new java.util.Timer().schedule(
new java.util.TimerTask() {
#Override
public void run() {
event.getGuild().removeRoleFromMember(event.getOption("user").getAsUser(), event.getGuild().getRoleById("id")).queue();
event.getGuild().addRoleToMember(event.getOption("user").getAsUser(), roleMap.get(event.getOption("user").getAsMember().getRoles()));
}
},
event.getOption("duration").getAsLong() * 1000
);
}
}
First, let’s make this more readable. You are calling event.getOption("user") in seven different places, which makes the code hard to read. So let’s just call it once:
Guild guild = event.getGuild();
OptionMapping userOption = event.getOption("user");
User user = userOption.getAsUser();
Member member = userOption.getAsMember();
Now we can clean things up considerably:
#Override
public void onSlashCommandInteraction(#NotNull SlashCommandInteractionEvent event) {
String command = event.getName();
if (command.equalsIgnoreCase("mute")) {
Guild guild = event.getGuild();
OptionMapping userOption = event.getOption("user");
User user = userOption.getAsUser();
Member member = userOption.getAsMember();
if (!event.getMember().hasPermission(Permission.MANAGE_ROLES)) {
event.reply("You do not have the required permissions to use this command!").setEphemeral(true).queue();
return;
}
Iterable<Role> oldRoles = member.getRoles();
Role mutedRole = guild.getRoleById(id);
guild.modifyMemberRoles(member, mutedRole).queue();
event.getChannel().sendMessage(
user.getAsMention() + " has been muted").queue();
new java.util.Timer().schedule(
new java.util.TimerTask() {
#Override
public void run() {
guild.removeRoleFromMember(user, mutedRole).queue();
for (Role role : oldRoles) {
guild.addRoleToMember(user, role));
}
}
},
event.getOption("duration").getAsLong() * 1000
);
}
}
Notice that the permission check should be the first step performed. There is no reason to continue with any other logic if the user doesn’t have permission to execute the command.
Also notice the use of for (Role role : oldRoles) to re-add the roles one by one.
You should not make multiple Timers. Just create one Timer, and keep it in a private field:
private final java.util.Timer timer = new java.util.Timer();
#Override
public void onSlashCommandInteraction(#NotNull SlashCommandInteractionEvent event) {
And then, of course, use it in your method:
timer.schedule(
new java.util.TimerTask() {
So the final version looks like this:
private final java.util.Timer timer = new java.util.Timer();
#Override
public void onSlashCommandInteraction(#NotNull SlashCommandInteractionEvent event) {
String command = event.getName();
if (command.equalsIgnoreCase("mute")) {
Guild guild = event.getGuild();
OptionMapping userOption = event.getOption("user");
User user = userOption.getAsUser();
Member member = userOption.getAsMember();
if (!event.getMember().hasPermission(Permission.MANAGE_ROLES)) {
event.reply("You do not have the required permissions to use this command!").setEphemeral(true).queue();
return;
}
Iterable<Role> oldRoles = member.getRoles();
Role mutedRole = guild.getRoleById(id);
guild.modifyMemberRoles(member, mutedRole).queue();
event.getChannel().sendMessage(
user.getAsMention() + " has been muted").queue();
timer.schedule(
new java.util.TimerTask() {
#Override
public void run() {
guild.removeRoleFromMember(user, mutedRole).queue();
for (Role role : oldRoles) {
guild.addRoleToMember(user, role));
}
}
},
event.getOption("duration").getAsLong() * 1000
);
}
}
Your code is using guild.getRoleById(id) to obtain the “muted” role. I assume you have set id elsewhere in your class and it does in fact refer to a special role intended for muted users.
Disclaimer: I have never done Discord programming, and I have no way to test any of the above code. I mainly based this on the SlashCommandInteractionEvent documentation.
does anybody know how I can add instrumentation to a GraphQL execution when using graphql-spring-boot (https://github.com/graphql-java-kickstart/graphql-spring-boot) ? I know how this is possible with plain-vanilla graphql-java: https://www.graphql-java.com/documentation/v13/instrumentation/
However, I don't know how to do this when graphql-spring-boot is used and takes control over the execution. Due to lack of documentation I tried it simply this way:
#Service
public class GraphQLInstrumentationProvider implements InstrumentationProvider {
#Override
public Instrumentation getInstrumentation() {
return SimpleInstrumentation.INSTANCE;
}
}
But the method getInstrumentation on my InstrumentationProvider bean is (as expected) never called. Any help appreciated.
Answering my own question. In the meantime I managed to do it this way:
final class RequestLoggingInstrumentation extends SimpleInstrumentation {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingInstrumentation.class);
#Override
public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
long startMillis = System.currentTimeMillis();
var executionId = parameters.getExecutionInput().getExecutionId();
if (logger.isInfoEnabled()) {
logger.info("GraphQL execution {} started", executionId);
var query = parameters.getQuery();
logger.info("[{}] query: {}", executionId, query);
if (parameters.getVariables() != null && !parameters.getVariables().isEmpty()) {
logger.info("[{}] variables: {}", executionId, parameters.getVariables());
}
}
return new SimpleInstrumentationContext<>() {
#Override
public void onCompleted(ExecutionResult executionResult, Throwable t) {
if (logger.isInfoEnabled()) {
long endMillis = System.currentTimeMillis();
if (t != null) {
logger.info("GraphQL execution {} failed: {}", executionId, t.getMessage(), t);
} else {
var resultMap = executionResult.toSpecification();
var resultJSON = ObjectMapper.pojoToJSON(resultMap).replace("\n", "\\n");
logger.info("[{}] completed in {}ms", executionId, endMillis - startMillis);
logger.info("[{}] result: {}", executionId, resultJSON);
}
}
}
};
}
}
#Service
class InstrumentationService {
private final ContextFactory contextFactory;
InstrumentationService(ContextFactory contextFactory) {
this.contextFactory = contextFactory;
}
/**
* Return all instrumentations as a bean.
* The result will be used in class {#link com.oembedler.moon.graphql.boot.GraphQLWebAutoConfiguration}.
*/
#Bean
List<Instrumentation> instrumentations() {
// Note: Due to a bug in GraphQLWebAutoConfiguration, the returned list has to be modifiable (it will be sorted)
return new ArrayList<>(
List.of(new RequestLoggingInstrumentation()));
}
}
It helped me to have a look into the class GraphQLWebAutoConfiguration. There I found out that the framework expects a bean of type List<Instrumentation>, which contains all the instrumentations that will be added to the GraphQL execution.
There is a simpler way to add instrumentation with spring boot:
#Configuration
public class InstrumentationConfiguration {
#Bean
public Instrumentation someFieldCheckingInstrumentation() {
return new FieldValidationInstrumentation(env -> {
// ...
});
}
}
Spring boot will collect all beans which implement Instrumentation (see GraphQLWebAutoConfiguration).
I am new to Reactor framework and trying to utilize it in one of our existing implementations. LocationProfileService and InventoryService both return a Mono and are to executed in parallel and have no dependency on each other (from the MainService). Within LocationProfileService - there are 4 queries issued and the last 2 queries have a dependency on the first query.
What is a better way to write this? I see the calls getting executed sequentially, while some of them should be executed in parallel. What is the right way to do it?
public class LocationProfileService {
static final Cache<String, String> customerIdCache //define Cache
#Override
public Mono<LocationProfileInfo> getProfileInfoByLocationAndCustomer(String customerId, String location) {
//These 2 are not interdependent and can be executed immediately
Mono<String> customerAccountMono = getCustomerArNumber(customerId,location) LocationNumber).subscribeOn(Schedulers.parallel()).switchIfEmpty(Mono.error(new CustomerNotFoundException(location, customerId))).log();
Mono<LocationProfile> locationProfileMono = Mono.fromFuture(//location query).subscribeOn(Schedulers.parallel()).log();
//Should block be called, or is there a better way to do ?
String custAccount = customerAccountMono.block(); // This is needed to execute and the value from this is needed for the next 2 calls
Mono<Customer> customerMono = Mono.fromFuture(//query uses custAccount from earlier step).subscribeOn(Schedulers.parallel()).log();
Mono<Result<LocationPricing>> locationPricingMono = Mono.fromFuture(//query uses custAccount from earlier step).subscribeOn(Schedulers.parallel()).log();
return Mono.zip(locationProfileMono,customerMono,locationPricingMono).flatMap(tuple -> {
LocationProfileInfo locationProfileInfo = new LocationProfileInfo();
//populate values from tuple
return Mono.just(locationProfileInfo);
});
}
private Mono<String> getCustomerAccount(String conversationId, String customerId, String location) {
return CacheMono.lookup((Map)customerIdCache.asMap(),customerId).onCacheMissResume(Mono.fromFuture(//query).subscribeOn(Schedulers.parallel()).map(x -> x.getAccountNumber()));
}
}
public class InventoryService {
#Override
public Mono<InventoryInfo> getInventoryInfo(String inventoryId) {
Mono<Inventory> inventoryMono = Mono.fromFuture(//inventory query).subscribeOn(Schedulers.parallel()).log();
Mono<List<InventorySale>> isMono = Mono.fromFuture(//inventory sale query).subscribeOn(Schedulers.parallel()).log();
return Mono.zip(inventoryMono,isMono).flatMap(tuple -> {
InventoryInfo inventoryInfo = new InventoryInfo();
//populate value from tuple
return Mono.just(inventoryInfo);
});
}
}
public class MainService {
#Autowired
LocationProfileService locationProfileService;
#Autowired
InventoryService inventoryService
public void mainService(String customerId, String location, String inventoryId) {
Mono<LocationProfileInfo> locationProfileMono = locationProfileService.getProfileInfoByLocationAndCustomer(....);
Mono<InventoryInfo> inventoryMono = inventoryService.getInventoryInfo(....);
//is using block fine or is there a better way to do?
Mono.zip(locationProfileMono,inventoryMono).subscribeOn(Schedulers.parallel()).block();
}
}
You don't need to block in order to get the pass that parameter your code is very close to the solution. I wrote the code using the class names that you provided. Just replace all the Mono.just(....) with the call to the correct service.
public Mono<LocationProfileInfo> getProfileInfoByLocationAndCustomer(String customerId, String location) {
Mono<String> customerAccountMono = Mono.just("customerAccount");
Mono<LocationProfile> locationProfileMono = Mono.just(new LocationProfile());
return Mono.zip(customerAccountMono, locationProfileMono)
.flatMap(tuple -> {
Mono<Customer> customerMono = Mono.just(new Customer(tuple.getT1()));
Mono<Result<LocationPricing>> result = Mono.just(new Result<LocationPricing>());
Mono<LocationProfile> locationProfile = Mono.just(tuple.getT2());
return Mono.zip(customerMono, result, locationProfile);
})
.map(LocationProfileInfo::new)
;
}
public static class LocationProfileInfo {
public LocationProfileInfo(Tuple3<Customer, Result<LocationPricing>, LocationProfile> tuple){
//do wathever
}
}
public static class LocationProfile {}
private static class Customer {
public Customer(String cutomerAccount) {
}
}
private static class Result<T> {}
private static class LocationPricing {}
Pleas remember that the first zip is not necessary. I re write it to mach your solution. But I would solve the problem a little bit differently. It would be clearer.
public Mono<LocationProfileInfo> getProfileInfoByLocationAndCustomer(String customerId, String location) {
return Mono.just("customerAccount") //call the service
.flatMap(customerAccount -> {
//declare the call to get the customer
Mono<Customer> customerMono = Mono.just(new Customer(customerAccount));
//declare the call to get the location pricing
Mono<Result<LocationPricing>> result = Mono.just(new Result<LocationPricing>());
//declare the call to get the location profile
Mono<LocationProfile> locationProfileMono = Mono.just(new LocationProfile());
//in the zip call all the services actually are executed
return Mono.zip(customerMono, result, locationProfileMono);
})
.map(LocationProfileInfo::new)
;
}
I have been investigating how to change the frequency of a job on runtime with Java 8 and spring. This question was very useful but it did not totally solve my issue.
I can now configure the date when to job should be executed next. But If set the delay to 1 year, then I need to wait 1 year before the new configuration in taken into account.
My idea would be to stop the scheduled task if the configuration value is changed (so from another class). Then recalculate the next time the task should be executed. Perhaps there is an easier way of doing this.
Here is the code I have so far.
#Configuration
#EnableScheduling
public class RequestSchedulerConfig implements SchedulingConfigurer {
#Autowired
SchedulerConfigService schedulerConfigService;
#Bean
public RequestScheduler myBean() {
return new RequestScheduler();
}
#Bean(destroyMethod = "shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(100);
}
#Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
taskRegistrar.addTriggerTask(
new Runnable() {
#Override public void run() {
myBean().startReplenishmentComputation();
}
},
new Trigger() {
#Override public Date nextExecutionTime(TriggerContext triggerContext) {
Duration d = schedulerConfigService.getIntervalFromDB();
return DateTime.now().plus(d).toDate();
}
}
);
}
}
This would be what I would like to do.
#RestController
#RequestMapping("/api/config/scheduler")
public class RequestSchedulerController {
#Autowired
ApplicationConfigWrapper applicationConfigWrapper;
#RequestMapping("/set/")
#ResponseBody
public String setRequestSchedulerConfig(#RequestParam(value = "frequency", defaultValue = "") final String frequencyInSeconds){
changeValueInDb(frequencyInSeconds);
myJob.restart();
return "Yeah";
}
}
Create a singleton bean that gets an injected TaskScheduler. This will hold as state variables all ScheduledFutures, like private ScheduledFuture job1;
On deployment, load from databases all schedule data and start the jobs, filling in all state variables like job1.
On change of scheduling data, cancel the corresponding Future (e.g job1) and then start it again with the new scheduling data.
The key idea here is to get control on the Futures as they are created, so to save them in some state variables, so that when something in scheduling data changes, you can cancel them.
Here is the working code:
applicationContext.xml
<task:annotation-driven />
<task:scheduler id="infScheduler" pool-size="10"/>
The singleton bean, that holds the Futures
#Component
public class SchedulerServiceImpl implements SchedulerService {
private static final Logger logger = LoggerFactory.getLogger(SchedulerServiceImpl.class);
#Autowired
#Qualifier(value="infScheduler")
private TaskScheduler taskScheduler;
#Autowired
private MyService myService;
private ScheduledFuture job1;//for other jobs you can add new private state variables
//Call this on deployment from the ScheduleDataRepository and everytime when schedule data changes.
#Override
public synchronized void scheduleJob(int jobNr, long newRate) {//you are free to change/add new scheduling data, but suppose for now you only want to change the rate
if (jobNr == 1) {//instead of if/else you could use a map with all job data
if (job1 != null) {//job was already scheduled, we have to cancel it
job1.cancel(true);
}
//reschedule the same method with a new rate
job1 = taskScheduler.scheduleAtFixedRate(new ScheduledMethodRunnable(myService, "methodInMyServiceToReschedule"), newRate);
}
}
}
What about using Set<ScheduledTask> ScheduledTaskRegistrar.getScheduledTasks() to get all schedules tasks and calling ScheduledTask::cancel() ?
or maybe executing ThreadPoolTaskScheduler::shutdown()
and recreating ThreadPoolTaskScheduler and setting it again in ScheduledTaskRegistrar ?
The following, an improved version of this code, seems a working POC based on Spring Boot. You can start and stop the scheduled tasks any number of times based on a table configuration. But you can't start a stopped job from where it was stopped.
1) In the main class, make sure scheduling is enabled, and perhaps configure a ThreadPoolTaskScheduler with size more than one so scheduled tasks may run in parallel.
#SpringBootApplication
#EnableScheduling
#Bean
public TaskScheduler poolScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
scheduler.setPoolSize(10);
scheduler.initialize();
return scheduler;
}
2) an object that contains the schedule configuration, e.g. a cron like configuration in this case:
public class ScheduleConfigVo {
//some constructors, getter/setters
private String taskName;
private String configValue; // like */10 * * * * * for cron
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScheduleConfigVo that = (ScheduleConfigVo) o;
return taskName.equals(that.taskName) &&
configValue.equals(that.configValue) ;
}
#Override
public int hashCode() {
return Objects.hash(taskName, configValue);
}
}
equals and hashCode are needed since object comparison will be conducted.
3) I use mybatis, so the sheduled selection is something like:
#Mapper
public interface ScheduleConfigMapper {
List<ScheduleConfigVo> getAllConfigure();
}
and
public class ScheduleConfigMapperImpl implements ScheduleConfigMapper {
#Override
public List<ScheduleConfigVo>getAllConfigure() {
return getAllConfigure();
}
}
with a simple companion mybatis xml configuration (not shown here but can find it anywhere in the internet).
4) create a table and populate it with a record
CREATE TABLE "SCHEDULER"
( "CLASS_NAME" VARCHAR2(100), --PK
"VALUE" VARCHAR2(20 BYTE) --not null
)
and populated it with a record class_name=Task1, value=*/10 * * * * * etc. => run like a cron every ten seconds
5) the scheduler part:
#Service
public class DynamicScheduler implements SchedulingConfigurer {
#Autowired
private ScheduleConfigMapper repo;
#Autowired
private Runnable [] tsks;
#Autowired
private TaskScheduler tsch;
private ScheduledTaskRegistrar scheduledTaskRegistrar;
private ScheduledFuture future;
private Map<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>(); // for the moment it has only class name
List<ScheduleConfigVo> oldList = new ArrayList<>();
List<ScheduleConfigVo> newList;
List<ScheduleConfigVo> addList = new ArrayList<>();
List<ScheduleConfigVo> removeList = new ArrayList<>();
#Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
if (scheduledTaskRegistrar == null) {
scheduledTaskRegistrar = taskRegistrar;
}
if (taskRegistrar.getScheduler() == null) {
taskRegistrar.setScheduler(tsch);
}
updateJobList();
}
#Scheduled(fixedDelay = 5000)
public void updateJobList() {
newList = repo.getConfigure()== null ? new ArrayList<>() : repo.getConfigure();
addList.clear();
removeList.clear();
if (!newList.isEmpty()) {
//compare new List with oldList
if (!oldList.isEmpty()) {
addList = newList.stream().filter(e -> !oldList.contains(e)).collect(Collectors.toList());
removeList = oldList.stream().filter(e -> !newList.contains(e)).collect(Collectors.toList());
} else {
addList = new ArrayList<>(newList); // nothing to remove
}
} else { // nothing to add
if (!oldList.isEmpty()) {
removeList = new ArrayList<>(oldList);
} // else removeList = 0
}
log.info("addList="+ addList.toString());
log.info("removeList="+ removeList.toString());
//re-schedule here
for ( ScheduleConfigVo conf : removeList ) {
if ( !futureMap.isEmpty()){
future = futureMap.get(conf.getTaskName());
if (future != null) {
log.info("cancelling task "+conf.getTaskName() +" ...");
future.cancel(true);
log.info(conf.getTaskName() + " isCancelled = " + future.isCancelled());
futureMap.remove(conf.getTaskName());
}
}
}
for ( ScheduleConfigVo conf : addList ) {
for (Runnable o: tsks) {
if (o.getClass().getName().contains(conf.getTaskName())) { // o has fqn whereas conf has class name only
log.info("find " + o.getClass().getName() + " to add to scheduler");
future = scheduledTaskRegistrar.getScheduler().schedule(o, (TriggerContext a) -> {
CronTrigger crontrigger = new CronTrigger(conf.getConfigValue());
return crontrigger.nextExecutionTime(a);
});
futureMap.put(o.getClass().getName().substring(o.getClass().getName().lastIndexOf('.')+1), future);
}
}
}
oldList.clear();
oldList= newList;
}
6) one or more Runnable tasks that actually does the cron work, for instance:
#Slf4j
#Service
public class Task1 implements Runnable {
#Override
public void run() {
log.info("Task1 is running...");
}
}
Once the application is started, the cron job will run. The running interval changes as the value in the table changes, and the job stops as the table entry is removed.
Note that if the job runs longer than the cron interval, the next run is after the previous job finishes. You can simulate this situation by adding, for instance, sleep 15 seconds in Task1 above to test it. Sometimes after being cancelled, a job maybe still run till it's done.
***Just edit to add that if folks like lambda to save some lines, the above removeList and addList can be modified as:
removeList.stream().filter(conf -> {
future = futureMap.get(conf.getTaskName());
return future != null;
}).forEach((conf) -> {
log.info("cancelling task " + conf.getTaskName() + " ...");
future.cancel(true);
log.info(conf.getTaskName() + " isCancelled = " + future.isCancelled());
});
and
Arrays.stream(tsks).forEach(task -> {
addList.stream().filter(conf -> task.getClass().getName().contains(conf.getTaskName())).forEach(conf -> {
log.info("find " + task.getClass().getName() + " to add to scheduler");
future = scheduledTaskRegistrar.getScheduler().schedule(task, (TriggerContext a) -> {
CronTrigger crontrigger = new CronTrigger(conf.getConfigValue());
return crontrigger.nextExecutionTime(a);
});
futureMap.put(task.getClass().getName().substring(task.getClass().getName().lastIndexOf('.') + 1), future);
});
});
One simple approach is to only ever add new tasks, not to try and cancel or restart the scheduler.
Each time the configuration changes, just add a new task with its new configuration.
Then, whenever a task runs, it must first check some state (by querying database, or lookup up in a concurrent map, or whatever) to decide if it is the latest version. If it is, then it should proceed. Otherwise, it should end immediately.
The only downside is that if you are changing job configuration frequently compared to how often they run, then of course the list of scheduled tasks will keep growing in memory.
#CachePut or #Cacheable(value = "CustomerCache", key = "#id")
public Customer updateCustomer(Customer customer) {
sysout("i am inside updateCustomer");
....
return customer;
}
I found below documentation under CachePut source code
CachePut annotation does not cause the target method to be skipped -
rather it always causes the method to be invoked and its result to be
placed into the cache.
Does it mean if I use #Cacheable , updateCustomer method will be executed only once and result will be updated in cache. Subsequent calls to
updateCustomer will not execute updateCustomer , it will just update the cache.
While in case of #CachePut, updateCustomer method will be executed on each call and result will be updated in cache.
Is my understanding correct?
Yes.
I even made a test to be sure:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = CacheableTest.CacheConfigurations.class)
public class CacheableTest {
public static class Customer {
final private String id;
final private String name;
public Customer(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
final public static AtomicInteger cacheableCalled = new AtomicInteger(0);
final public static AtomicInteger cachePutCalled = new AtomicInteger(0);
public static class CustomerCachedService {
#Cacheable("CustomerCache")
public Customer cacheable(String v) {
cacheableCalled.incrementAndGet();
return new Customer(v, "Cacheable " + v);
}
#CachePut("CustomerCache")
public Customer cachePut(String b) {
cachePutCalled.incrementAndGet();
return new Customer(b, "Cache put " + b);
}
}
#Configuration
#EnableCaching()
public static class CacheConfigurations {
#Bean
public CustomerCachedService customerCachedService() {
return new CustomerCachedService();
}
#Bean
public CacheManager cacheManager() {
return new GuavaCacheManager("CustomerCache");
}
}
#Autowired
public CustomerCachedService cachedService;
#Test
public void testCacheable() {
for(int i = 0; i < 1000; i++) {
cachedService.cacheable("A");
}
Assert.assertEquals(cacheableCalled.get(), 1);
}
#Test
public void testCachePut() {
for(int i = 0; i < 1000; i++) {
cachedService.cachePut("B");
}
Assert.assertEquals(cachePutCalled.get(), 1000);
}
}
#CachePut always lets the method execute. It is generally used if you want your cache to be updated with the result of the method execution.
Example: When you want to update a stale data which is cached, instead of blowing the cache completely.
#Cacheable will be executed only once for the given cachekey and subsequent requests won't execute the method, until the cache expires or gets flushed.
Yes, you are absolutely correct.
#Cacheput and #Cacheable are used in conjunction.
#Cacheable will not update the cache on every call. In order to remove the stale data, there must be a service that uses the #Cacheput that clears the stale data.
Below answer is for the ones who are using guava caching to build cache.
Using guava caching, the time interval that is applied will empty the cache after a certain period of time which is not the case with #Cacheput. #Cacheput will only update the values that are stale and hence it calls the method every time to update the cache.
I hope my answer clears your question.