Design and Implementation of a Time‑Travel Module for E‑commerce Promotion Validation
The team built a whitelist‑based time‑travel module that overrides the system clock for selected operators, letting them preview future promotional prices in Vivo’s e‑commerce flow without creating real orders, by propagating an openId context through Dubbo filters and replacing System.currentTimeMillis() with a custom TimeTravelUtil.
Background : During major sales events (e.g., Double‑11, Double‑12), the Vivo online mall configures many promotional activities. Mis‑configurations can cause order loss or unintended discounts, leading to significant revenue impact. The team wants a way for operators to verify that all expected promotions are correctly configured before the promotion starts.
Idea : Since promotional effects are reflected in the final price, the solution is to let operators view the discounted price "in advance". The pricing center already calculates real‑time promotional prices, so the idea is to make the pricing center compute prices for a future point in time by temporarily overriding the "current time". This must be done without affecting normal users, so a whitelist mechanism is introduced – only logged‑in users whose IDs are on the whitelist experience the time‑travel view.
Key Questions Discussed :
Whether the full shopping flow is needed for the time‑travel experience.
If a real order needs to be created – the answer is no; the order confirmation page can display the final price without actual order creation.
How to handle coupons obtained during time‑travel – two options (marking coupons specially or treating them as normal). The team prefers the simpler option (no special marking) to minimize implementation cost.
Implementation :
1. Core Flow Diagram – shows the main e‑commerce scenarios (product detail, cart, order confirmation, order submission) where the future price is displayed.
2. Refactoring Focus :
Whitelist information maintenance (openId: travelTime) stored in the configuration center.
Acquiring the "current time" via a common module so all downstream services can obtain the overridden time.
3. Time‑Travel Module (vivo‑xxx‑time‑travel) provides three capabilities:
Whitelist of travel users.
Method to get the overridden "current time".
Read/write of the openId context.
4. Dubbo Filters for OpenId Propagation – two filters are added to propagate the openId through Dubbo RPC calls.
/**
* Current business system as consumer filter
*/
@Activate(group = Constants.CONSUMER)
public class BizConsumerFilter implements Filter {
@Override
public Result invoke(Invoker
invoker, Invocation invocation) throws RpcException {
if (invocation instanceof RpcInvocation) {
String openId = invocation.getAttachment("tc_xxx_travel_openId");
if (openId == null && TimeTravelUtil.getContextOpenId() != null) {
// Set context openId before invoking if missing
((RpcInvocation) invocation).setAttachment(openIdAttachmentKey, TimeTravelUtil.getContextOpenId());
}
}
return invoker.invoke(invocation);
}
} /**
* Current business system as provider filter
*/
@Activate(group = Constants.PROVIDER)
public class BizProviderFilter implements Filter {
@Override
public Result invoke(Invoker
invoker, Invocation invocation) throws RpcException {
if (invocation instanceof RpcInvocation) {
String openId = invocation.getAttachment("tc_xxx_travel_openId");
if (openId != null) { // Get upstream openId
TimeTravelUtil.setContextOpenId(openId);
}
}
try {
return invoker.invoke(invocation);
} finally {
TimeTravelUtil.removeContextOpenId();
}
}
} /**
* Time travel utility class
*/
public final class TimeTravelUtil {
private static final ThreadLocal
currentUserTimeTravelInfoThreadLocal = new ThreadLocal<>();
private static final ThreadLocal
contextOpenId = new ThreadLocal<>();
public static void setContextOpenId(String openId) {
contextOpenId.set(openId);
setUserTravelInfoIfExists(openId);
}
public static String getContextOpenId() {
return contextOpenId.get();
}
public static void removeContextOpenId() {
contextOpenId.remove();
removeUserTimeTravelInfo();
}
public static void setUserTravelInfoIfExists(String openId) {
TimeTravelInfo userTimeTravelInfo = TimeTravellersConfig.getUserTimeTravelInfo(openId);
if (userTimeTravelInfo.isInTravel()) {
currentUserTimeTravelInfoThreadLocal.set(userTimeTravelInfo);
}
}
public static void removeUserTimeTravelInfo() {
currentUserTimeTravelInfoThreadLocal.remove();
}
public static boolean isInTimeTravel() {
return currentUserTimeTravelInfoThreadLocal.get() != null;
}
public static long getNow() {
TimeTravelInfo travelInfo = currentUserTimeTravelInfoThreadLocal.get();
return travelInfo != null ? travelInfo.getTravelTime() : System.currentTimeMillis();
}
} /**
* User travel information
*/
public class TimeTravelInfo {
private boolean isInTravel = false;
private Long travelTime;
public boolean isInTravel() { return isInTravel; }
public void setInTravel(boolean inTravel) { isInTravel = inTravel; }
public Long getTravelTime() { return travelTime; }
public void setTravelTime(Long travelTime) { this.travelTime = travelTime; }
}After integrating the module, any place that previously called System.currentTimeMillis() should be replaced with TimeTravelUtil.getNow() to obtain the overridden time when the user is in the whitelist.
Issues Encountered : Propagation of the openId context across threads can be lost. Solutions include copying ThreadLocal data in thread‑pool wrappers or using Hystrix plugins (referenced in an external article).
Conclusion : The time‑travel capability is demonstrated in the Vivo online mall but is generic enough for other business scenarios. It mainly supports read‑only operations (price checks). For write‑heavy scenarios, a shadow database approach can be combined.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.