Apex Cache Tracker to remove session cache

Here is the problem that needed to be solved:
1. Use session cache to cache some user information like their profile or user settings
2. When a external systems update the information and try to remove the session cache as it got updated it fails because it does not have access to a users session cache

Solution:
Build a session tracker in Org Cache to track missed removal of session cache

Step 1: When a session cache remove fails add it to the Org level tracker cache. The format of session keys are the following:
key + userId (Salesforce userId)

That way I can keep track of what userId’s I need to remove session cache
String splitStrKey = key.substring(0, key.length()-18);
String splitUserId = key.substring(key.length()-18, key.length());

will split into key and userId

public Boolean remove(String key) {
      if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){
        Boolean removed = false;
        if (sessionCacheEnabled && sessionPartition!=null) {
              System.debug('inside session cache remove key: ' + key);
              removed = sessionPartition.remove(key);
              if (!removed && key!=null && key.length() > 18){
                String splitStrKey = key.substring(0, key.length()-18);
                String splitUserId = key.substring(key.length()-18, key.length());
                System.debug('Session remove failed key ' + splitStrKey + ' for ' + splitUserId + ' adding to tracker');
                removeFailedAddToTracker(splitStrKey, splitUserId, null);
              }
          } else if (orgPartition!=null){
              removed = orgPartition.remove(key);
          }

        if (removed) {
          System.debug(LoggingLevel.DEBUG, 'Removed key ' + key);
          return true;
        } else{
          System.debug(LoggingLevel.DEBUG, 'Not removed key not found ' + key);
          return false;
        }
      } else {
        System.debug(LoggingLevel.DEBUG, 'Skipping cache remove in batch ' + key);
        return false;
      }
    }

Step 2: Add session keys to Org level tracker cache (CACHE_USER_CACHE_TRACKER), we will add it as Map<Id, Map> where Map contains UserId as key and Map of all the session keys that needs to be removed

public void removeFailedAddToTracker(String keyPrefix, Id userId, Set<Id> clientIds){
      if (App_Service.isIntegrationUser(userId)){
        Map<String, String> mapOfContactUserIds = (Map<String, String>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CONTACT_MAP);
        Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER);
        if (clientIds!=null && !clientIds.isEmpty()){
          for (Id clientId : clientIds){
            if (mapOfContactUserIds!=null && mapOfContactUserIds.containsKey(clientId)){
              Id userIdToClearCache = mapOfContactUserIds.get(clientId);

              if (mapOfExistingCacheQueueTracker!=null){
                if (mapOfExistingCacheQueueTracker.containsKey(userIdToClearCache)){
                  Map<String, Id> existingCacheStrings = mapOfExistingCacheQueueTracker.get(userIdToClearCache);
                  existingCacheStrings.put(keyPrefix, userIdToClearCache);
                  mapOfExistingCacheQueueTracker.put(userIdToClearCache, existingCacheStrings);
                } else {
                  mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache});
                }
              } else {
                mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>();
                mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache});
              }
            }
          }
        } else {
          if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(userId)){
            Map<String, Id> existingMap = mapOfExistingCacheQueueTracker.get(userId);
            existingMap.put(keyPrefix, userId);
            mapOfExistingCacheQueueTracker.put(userId, existingMap);
          } else {
            if (mapOfExistingCacheQueueTracker==null){
              mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>();
            }
            mapOfExistingCacheQueueTracker.put(userId, new Map<String, Id>{keyPrefix=>userId});
          }
        }

        if (mapOfExistingCacheQueueTracker!=null)
          getOrgPartition().put(App_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker);
      }
    }

Step 3: Every time before we check if session cache contains a key we will see if the same key is contained in the CACHE_USER_CACHE_TRACKER cache. If yes remove it from the users session cache so that cache can be removed and new query can be done

  public Boolean containsKey(String cacheKey){
      Boolean containsKey = false;
      if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){
           if (sessionCacheEnabled==false && orgPartition!=null){
              if (orgPartition.get(cacheKey)!=null){
                containsKey = true;
              }
          } else if (sessionCacheEnabled && sessionPartition!=null) {
              if (sessionPartition.get(cacheKey)!=null){
                containsKey = true;

                Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER);
              	if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(UserInfo.getUserId())){
            			Map<String, Id> flagsToClear = mapOfExistingCacheQueueTracker.get(UserInfo.getUserId());

                  Boolean removeFlag = false;
            			for (String flagToClear : flagsToClear.keySet()){
            				if (flagToClear.equalsIgnoreCase(cacheKey)){
                      String keyToRemove = flagToClear + flagsToClear.get(flagToClear);
            					Boolean removeItemFromCache = getSessionPartition().remove(keyToRemove);
                      
                      removeFlag = true;
                      containsKey = false;
            				}
            			}

                  if (removeFlag){
                    for (String flagToClear : flagsToClear.keySet()){
                      flagsToClear.remove(cacheKey);
                      if (flagsToClear.isEmpty() && flagsToClear.size()==0){
                        mapOfExistingCacheQueueTracker.remove(UserInfo.getUserId());
                      } else {
                        mapOfExistingCacheQueueTracker.put(UserInfo.getUserId(), flagsToClear);
                      }
                    }
                    
                    getOrgPartition().put(App_Rest_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker);
                  }

            		}
              }
          }
       }
      return containsKey;
    }

Apex Cache RecordTypes for fast retrieval

Apex class to lookup record types and cache them

  private static Map<Schema.SObjectType,Map<String,Id>> rtypesCache;

static {
   rtypesCache = new Map<Schema.SObjectType,Map<String,Id>>();//convenient map, formatted from r    esults.
}

  public static Map<String, Id> getRecordTypeMapForObjectGeneric(Schema.SObjectType token) {
      Map<String, Id> mapRecordTypes = rtypesCache.get(token);
      if (mapRecordTypes == null) {
          mapRecordTypes = new Map<String, Id>();
          rtypesCache.put(token,mapRecordTypes);
      } else {
           return mapRecordTypes;
      }

      Schema.DescribeSObjectResult obj = token.getDescribe();
      if (results == null || results.isEmpty()) {
          String soql = 'SELECT Id, Name, DeveloperName, sObjectType FROM RecordType WHERE IsActive = TRUE';
          try {
              results = Database.query(soql);
          } catch (Exception ex) {
              results = new List<SObject>();
          }
      }

      Map<Id,Schema.RecordTypeInfo> recordTypeInfos = obj.getRecordTypeInfosByID();
      for (SObject rt : results) {
          if (recordTypeInfos.get(rt.Id) != null) {
              if (recordTypeInfos.get(rt.Id).isAvailable()) {
                  mapRecordTypes.put(String.valueOf(rt.get('DeveloperName')),rt.Id);
              }
              else {
                  System.debug('The record type ' + rt.get('DeveloperName') + ' for object ' + rt.get('sObjectType') + ' is not availiable for the user.');
              }
          }
      }
      return mapRecordTypes;
    }

Test Class

  @isTest(SeeAllData=true) static void testGetRecordTypeMapForObjectGeneric(){
      Test.startTest();
        Map<String, Id> caseRecordTypeId = App_Service.getRecordTypeMapForObjectGeneric(Case.SobjectType);
        System.Assert(caseRecordTypeId.get('Error')!=null);
        System.Assert(caseRecordTypeId.get('Internal')==null);
      Test.stopTest();
    }

Spring Redis Cache Service on Heroku


1. Setup Heroku RedisConnectionFactory Configuration
2. Setup RedisTemplate Configuration
3. Setup Redis Service
4. Setup Redis Controller

1. Setup Heroku RedisConnectionFactory Configuration

package com.example.redis;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;

import redis.clients.jedis.Protocol;

@Configuration
@Profile("default")
public class LocalRedisConfiguration {
	
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
    	
		try {
			URI redisUri = new URI(System.getenv("REDISCLOUD_URL"));
			JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
			redisConnectionFactory.setHostName(redisUri.getHost());
			redisConnectionFactory.setPort(redisUri.getPort());
			redisConnectionFactory.setTimeout(Protocol.DEFAULT_TIMEOUT);
			redisConnectionFactory.setPassword(redisUri.getUserInfo().split(":",2)[1]);
			
        return redisConnectionFactory;
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
    }
}

2. Setup RedisTemplate Configuration

package com.example.redis;

import javax.inject.Inject;

import org.springframework.context.annotation.*;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@Import({LocalRedisConfiguration.class})
public class RedisConfiguration {
	
	@Inject private RedisConnectionFactory redisConnectionFactory;
	
    @Bean
    public Topic topic() {
        return new ChannelTopic("pubsub:customer");
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(Topic topic, RedisConnectionFactory redisConnectionFactory, TaskExecutor taskExecutor) {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setTaskExecutor(taskExecutor);
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
        redisMessageListenerContainer.addMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message, byte[] pattern) {
                System.out.println("Received notification " + new String(message.getBody()) + ".");
                System.out.println(new String(pattern));
            }
        }, topic);
        return redisMessageListenerContainer;
    }

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setQueueCapacity(1);
        taskExecutor.setCorePoolSize(10);
        return taskExecutor;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() throws Exception {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

3. Setup Redis Service

package com.example.redis;

import com.example.service.CustomerService;
import com.example.model.Customer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigInteger;
import java.util.*;

@Service
public class RedisCustomerService implements CustomerService {

	@Autowired
	private RedisTemplate<String, Object> redisTemplate; 

    private String uniqueIdKey = "customerId";
 
    private BigInteger uniqueId() {
        long uniqueId = this.redisTemplate.opsForValue().increment(uniqueIdKey, 1);
        return BigInteger.valueOf(uniqueId);
    }
   
    private String lastNameKey(BigInteger id) {
        return "customer:ln:" + id;
    }

    private String firstNameKey(BigInteger id) {
        return "customer:fn:" + id;
    }

    public Collection<Customer> loadAllCustomers() {
        String keyPattern = "customer:fn:*";
        Set<String> keys = redisTemplate.keys(keyPattern);
        Set<Customer> customerSet = new HashSet<Customer>();
        for (String firstNameKey : keys) {
            long id = Long.parseLong(firstNameKey.split(":")[2]);
            customerSet.add(this.getCustomerById(BigInteger.valueOf(id)));
        }
        return customerSet;
    }

	@Override
	public Customer updateCustomer(BigInteger id, String fn, String ln) {
		 this.redisTemplate.opsForValue().set(lastNameKey(id), ln);
	     this.redisTemplate.opsForValue().set(firstNameKey(id), fn);
	     return getCustomerById(id);
	}

	@Override
	public List<Customer> updateCustomers(List<Customer> customersToUpdate) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Customer getCustomerById(BigInteger id) {
		String ln = (String) this.redisTemplate.opsForValue().get(lastNameKey(id));
	    String fn = (String) this.redisTemplate.opsForValue().get(firstNameKey(id));
	    return new Customer(id, fn, ln);
	}

	@Override
	public Collection<Customer> getAllCustomers() {
		 String keyPattern = "customer:fn:*";
	        Set<String> keys = redisTemplate.keys(keyPattern);
	        Set<Customer> customerSet = new HashSet<Customer>();
	        for (String firstNameKey : keys) {
	            long id = Long.parseLong(firstNameKey.split(":")[2]);
	            customerSet.add(this.getCustomerById(BigInteger.valueOf(id)));
	        }
	      return customerSet;
	}

	@Override
	public void deleteCustomer(BigInteger id) {
		redisTemplate.opsForValue().getOperations().delete(String.valueOf(id));
	}
	

	@Override
	public int getTotalRecords() {
		return redisTemplate.keys("customer:fn:*").size();
	}

	 private void setCustomerValues(BigInteger lid, String fn, String ln) {
	        this.redisTemplate.opsForValue().set(lastNameKey(lid), ln);
	        this.redisTemplate.opsForValue().set(firstNameKey(lid), fn);
	 }
	
	@Override
	public Customer createCustomer(String fn, String ln) {
		BigInteger lid = uniqueId();
        setCustomerValues(lid, fn, ln);
        return getCustomerById(lid);
	}
}

4. Setup Redis Controller

package com.example.controller;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Map;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.redis.RedisCustomerService;

@Controller
@RequestMapping("/redis")
public class RedisController {

	@Autowired
	RedisCustomerService redisCustomerService;
	
	@RequestMapping("")
	public String redisView(Map<String, Object> map){
		map.put("redisCustomerList", redisCustomerService.getAllCustomers());
		return "redis";
	}
	
	@RequestMapping(value="/insert")
	public String insertCustomersView(){
		return "insertredis";
	}
	
	@RequestMapping(value="/insert", method=RequestMethod.POST)
	public String insertCustomers(Map<String, Object> map, @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName) {
		redisCustomerService.createCustomer(firstName, lastName);
		return "redis";
	}
	
	@RequestMapping(value="/delete/{id}", method=RequestMethod.POST)
	public String deleteCustomer(@PathVariable("id") String id, Map<String, Object> map){
		redisCustomerService.deleteCustomer(new BigInteger(id));
		map.put("redisCustomerList", redisCustomerService.getAllCustomers());
		return "redis";
	}
	
	@RequestMapping(value="/update/{id}", method=RequestMethod.GET)
	public String updateCustomerView(@PathVariable("id") String id, Map<String, Object> map){
		map.put("redisupdate", Arrays.asList(redisCustomerService.getCustomerById(new BigInteger(id))));
		return "updateredis";
	}
	
	@RequestMapping(value="/update/{id}", method=RequestMethod.POST)
	public String updateCustomer(@PathVariable("id") String id, Map<String, Object> map, @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName){
		redisCustomerService.updateCustomer(new BigInteger(id), firstName, lastName);
		map.put("redisCustomerList", redisCustomerService.getAllCustomers());
		return "redis";
	}
}

IBMWebSphere eXtreme Scale V 7.1 andWebSphere DataPowerXC10 Appliance

IBM WebSphere® DataPower XC10 is a purpose-built appliance designed for simplified deployment and hardened security. Used in conjunction with WebSphere family products, this distributed caching solution enables business-critical applications to cost-effectively scale with consistent performance.

  • Accelerated Time to Value: Reduce install, setup and configuration time through out-of-the-box, “drop-in” use for HTTP Session Replication and the WebSphere Application Server dynamic cache service.
  • Simplified management and administration: Leverage a built-in, simplified administration and monitoring console to enable efficient setup, configuration and management of the appliance and transaction load within your datacenter.
  • Simplified monitoring of the runtime/health of the appliance: Experience status widgets reporting key metrics pertaining to your transaction load and memory.
  • Fast application performance: Use as a powerful distributed cache to speed application access to data.
  • Replace expensive disk operations with replicated memory operations for lower costs, easier scaling and higher performance.
  • Linear scalability: Scale out without limitations.
  • Support for non-Java client access: REST APIs for C/C++ and .NET client support via ADO.Net Data Services V1 specification support

Websphere XC10 Appliance Datasheet:

ftp://public.dhe.ibm.com/common/ssi/pm/sp/n/wsd14088usen/WSD14088USEN.PDF

Websphere XC10 Features and Benefits:

http://www-2000.ibm.com/software/webservers/appserv/xc10/features/