Java tutorial
/* * Copyright 2015 Evgeny Dolganov (evgenij.dolganov@gmail.com). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package och.comp.billing.standalone; import static java.math.BigDecimal.*; import static java.util.Collections.*; import static och.api.model.PropKey.*; import static och.api.model.RemoteCache.*; import static och.api.model.RemoteChats.*; import static och.api.model.billing.PaymentBase.*; import static och.api.model.billing.PaymentExt.*; import static och.api.model.billing.PaymentType.*; import static och.api.model.chat.account.PrivilegeType.*; import static och.api.model.tariff.TariffMath.*; import static och.comp.db.main.table.MainTables.*; import static och.comp.ops.BillingOps.*; import static och.comp.ops.ServersOps.*; import static och.comp.web.JsonOps.*; import static och.util.DateUtil.*; import static och.util.ExceptionUtil.*; import static och.util.FileUtil.*; import static och.util.StringUtil.*; import static och.util.Util.*; import static och.util.model.HoursAndMinutes.*; import static och.util.sql.SingleTx.*; import java.io.File; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import och.api.exception.ExpectedException; import och.api.model.billing.AdminSyncResp; import och.api.model.billing.LastSyncInfo; import och.api.model.billing.PaymentExt; import och.api.model.billing.UserBalance; import och.api.model.chat.account.ChatAccount; import och.api.model.chat.account.ChatAccountPrivileges; import och.api.model.server.ServerRow; import och.api.model.tariff.Tariff; import och.api.remote.chats.GetPausedStateReq; import och.api.remote.chats.GetUnblockedAccsReq; import och.api.remote.chats.ResultAccsResp; import och.comp.cache.Cache; import och.comp.cache.server.CacheServerContext; import och.comp.cache.server.CacheServerContextHolder; import och.comp.db.base.universal.UniversalQueries; import och.comp.db.main.MainDb; import och.comp.db.main.table._f.TariffLastPay; import och.comp.db.main.table.billing.CreatePayment; import och.comp.db.main.table.billing.GetAllBlockedUsers; import och.comp.db.main.table.billing.UpdateUserAccsBlocked; import och.comp.db.main.table.chat.GetAllChatAccounts; import och.comp.db.main.table.chat.UpdateChatAccountByUid; import och.comp.db.main.table.chat.privilege.GetAllChatAccountPrivileges; import och.comp.db.main.table.tariff.GetAllTariffs; import och.comp.mail.MailService; import och.comp.ops.BillingOps.PausedStateResp; import och.service.props.Props; import och.util.model.CallableVoid; import och.util.model.HasInitState; import och.util.model.HoursAndMinutes; import och.util.sql.ConcurrentUpdateSqlException; import och.util.timer.TimerExt; import org.apache.commons.logging.Log; public class BillingSyncService implements HasInitState, CacheServerContextHolder { private Log log = getLog(getClass()); private Props props; private MainDb db; private UniversalQueries universal; private MailService mailService; private Cache cache; private TimerExt paySyncTimer; private TimerExt cacheMonitorTimer; private TimerExt blockSyncTimer; public CallableVoid syncAccsListener; @Override public void setCacheServerContext(CacheServerContext c) { this.props = c.props; this.db = c.mainDb; this.universal = db.universal; this.mailService = c.mailService; this.cache = c.cache; } @Override public void init() throws Exception { checkStateForEmpty(props, "props"); checkStateForEmpty(cache, "cache"); checkStateForEmpty(mailService, "mailService"); checkStateForEmpty(universal, "universal"); //block and pause sync if (props.getBoolVal(billing_sync_fillBlockedCacheOnStart)) { long delay = props.getLongVal(billing_sync_fillBlockedCacheOnStartDelay); if (delay < 1) { reinitAccsBlocked(props, db, cache); reinitAccsPaused(props, db); } else { TimerExt timer = new TimerExt("BillingSyncService-fillBlockedCache", false); timer.trySchedule(() -> { reinitAccsBlocked(props, db, cache); reinitAccsPaused(props, db); timer.cancel(); }, delay); } } loadLastSyncInfo(); //skip timer if (props.getBoolVal(billing_sync_debug_DisableTimer)) return; paySyncTimer = new TimerExt("BillingSyncService-pay-sync", false); paySyncTimer.tryScheduleAtFixedRate(() -> doSyncWork(true, null, null), props.getLongVal(billing_sync_timerDelay), props.getLongVal(billing_sync_timerDelta)); blockSyncTimer = new TimerExt("BillingSyncService-block-sync", false); blockSyncTimer.tryScheduleAtFixedRate(() -> { checkAccBlocks(); checkAccPaused(); }, props.getLongVal(billing_sync_blockCheckTimerDelay), props.getLongVal(billing_sync_blockCheckTimerDelta)); cacheMonitorTimer = new TimerExt("BillingSyncService-tasks-checker", false); cacheMonitorTimer.tryScheduleAtFixedRate(() -> checkTasksFromCache(), props.getLongVal(billing_sync_taskMonitorTimerDelay), props.getLongVal(billing_sync_taskMonitorTimerDelta)); } private void loadLastSyncInfo() { File file = new File(props.getStrVal(billing_sync_lastSyncFile)); String lastSyncInfo = file.exists() ? tryReadFileUTF8(file) : null; cache.tryPutCache(BILLING_LAST_SYNC, lastSyncInfo); } private void saveLastSyncInfo(int updated) { if (!props.getBoolVal(billing_sync_lastSyncStore)) return; String lastSyncInfo = toJson(new LastSyncInfo(updated), true); cache.tryPutCache(BILLING_LAST_SYNC, lastSyncInfo); tryWriteFileUTF8(new File(props.getStrVal(billing_sync_lastSyncFile)), lastSyncInfo); } public void checkTasksFromCache() throws Exception { String reqId = cache.tryRemoveCache(BILLING_SYNC_REQ); if (reqId == null) return; cache.tryPutCache(BILLING_SYNC_RESP, BILLING_SYNC_RESP_START_PREFIX + "started at " + new Date()); log.info("doSyncWork by admin req: " + reqId); long start = System.currentTimeMillis(); int updatedCount = doSyncWork(false, null, null); long worktime = System.currentTimeMillis() - start; log.info("done. worktime: " + worktime + "ms"); AdminSyncResp result = new AdminSyncResp(reqId, updatedCount, worktime); cache.tryPutCache(BILLING_SYNC_RESP, toJson(result, true)); } public int doSyncWork(boolean checkWorkTime) throws Exception { return doSyncWork(checkWorkTime, null, null); } public int doSyncWork(boolean checkWorkTime, Date nowPreset) throws Exception { return doSyncWork(checkWorkTime, nowPreset, null); } public int doSyncWork(boolean checkWorkTime, Date nowPreset, CallableVoid beforeDbUpdateListener) throws Exception { Date now = nowPreset != null ? nowPreset : new Date(); if (props.getBoolVal(billing_sync_debug_DisableSync)) return -1; if (props.getBoolVal(toolMode)) return -1; // ? ? if (checkWorkTime && props.getBoolVal(billing_sync_debug_CheckWorkTime)) { int dayOfMonth = dayOfMonth(now); int endDay = props.getIntVal(billing_sync_endSyncDay); int startDay = props.getIntVal(billing_sync_startSyncDay); if (dayOfMonth < startDay) return -2; if (dayOfMonth > endDay) return -3; HoursAndMinutes nowHHmm = getHoursAndMinutes(now); if (dayOfMonth == startDay) { HoursAndMinutes startHHmm = tryParseHHmm(props.getStrVal(billing_sync_startSyncTime), null); if (startHHmm != null && nowHHmm.compareTo(startHHmm) < 0) return -2; } if (dayOfMonth == endDay) { HoursAndMinutes endHHmm = tryParseHHmm(props.getStrVal(billing_sync_endSyncTime), null); if (endHHmm != null && nowHHmm.compareTo(endHHmm) > 0) return -3; } } Date curMonthStart = monthStart(now); //get all accs HashSet<Long> needPayAccs = new HashSet<Long>(); HashMap<Long, ChatAccount> accsById = new HashMap<>(); List<ChatAccount> allAccs = universal.select(new GetAllChatAccounts()); for (ChatAccount acc : allAccs) { accsById.put(acc.id, acc); if (isNeedToPay(acc, curMonthStart)) needPayAccs.add(acc.id); } if (props.getBoolVal(billing_sync_log)) log.info("sync accs to pay (" + needPayAccs.size() + "): " + needPayAccs); if (isEmpty(needPayAccs)) { saveLastSyncInfo(0); return 0; } //get tariffs List<Tariff> tariffs = universal.select(new GetAllTariffs()); HashMap<Long, Tariff> tariffsById = new HashMap<>(); for (Tariff t : tariffs) tariffsById.put(t.id, t); //find owners HashMap<Long, Set<ChatAccount>> accsByUser = new HashMap<>(); List<ChatAccountPrivileges> allUsersPrivs = universal.select(new GetAllChatAccountPrivileges()); for (ChatAccountPrivileges data : allUsersPrivs) { if (data.privileges.contains(CHAT_OWNER)) { ChatAccount acc = accsById.get(data.accId); if (acc == null) continue; putToSetMap(accsByUser, data.userId, acc); } } if (beforeDbUpdateListener != null) beforeDbUpdateListener.call(); //sync by owners ArrayList<SyncPayError> syncErrors = new ArrayList<>(); for (Entry<Long, Set<ChatAccount>> entry : accsByUser.entrySet()) { Long userId = entry.getKey(); Set<ChatAccount> userAccs = entry.getValue(); try { if (syncAccsListener != null) syncAccsListener.call(); List<SyncPayError> curErrors = syncUserAccs(userId, userAccs, tariffsById, curMonthStart, now); if (curErrors.size() > 0) syncErrors.addAll(curErrors); } // ? , ? ? //? ? ? catch (ConcurrentUpdateSqlException e) { //? for (ChatAccount acc : userAccs) needPayAccs.remove(acc.id); } catch (Throwable t) { log.error("can't sync accs for user=" + userId + ": " + t); syncErrors.add(new SyncPayError(userId, userAccs, t)); } } if (syncErrors.size() > 0) sendSyncErrorMailToAdmin("Sync billing errors", syncErrors); int updated = needPayAccs.size(); saveLastSyncInfo(updated); return updated; } private List<SyncPayError> syncUserAccs(long userId, Set<ChatAccount> userAccs, Map<Long, Tariff> tariffs, Date curMonthStart, Date now) throws ConcurrentUpdateSqlException, Exception { List<SyncPayError> errors = new ArrayList<>(); BigDecimal totalPrice = ZERO; Date lastMonthStart = addMonths(curMonthStart, -1); Date lastMonthEnd = monthEnd(lastMonthStart); ArrayList<ChatAccount> accsToUpdate = new ArrayList<>(); for (ChatAccount acc : userAccs) { if (isNeedToPay(acc, curMonthStart)) { Tariff tariff = tariffs.get(acc.tariffId); if (tariff == null) { errors.add(new SyncPayError(userId, singleton(acc), new IllegalStateException("can't find tariff"))); continue; } accsToUpdate.add(acc); BigDecimal price = tariff.price; if (price.compareTo(ZERO) < 1) continue; //? ?? - , ? BigDecimal accAmount; Date oldPayDate = acc.tariffLastPay; if (oldPayDate.compareTo(lastMonthStart) == 0) accAmount = price; else accAmount = calcForPeriod(price, oldPayDate, lastMonthEnd, ZERO); totalPrice = totalPrice.add(accAmount); } } //final amount BigDecimal amount = totalPrice; boolean hasBill = ZERO.compareTo(amount) != 0; BigDecimal[] updatedBalance = { null }; BigDecimal minActiveBalance = props.getBigDecimalVal(billing_minActiveBalance); boolean[] accBlocked = { false }; //update db doInSingleTxMode(() -> { boolean isBlocked = findBalance(universal, userId).accsBlocked; //bill if (!isBlocked && hasBill) { long payId = universal.nextSeqFor(payments); PaymentExt payment = createSystemBill(payId, userId, amount, now, TARIFF_MONTH_BIll, collectionToStr(accsToUpdate)); universal.update(new CreatePayment(payment)); updatedBalance[0] = appendBalance(universal, userId, payment.amount); //? ? ? if (updatedBalance[0].compareTo(minActiveBalance) < 0) { accBlocked[0] = true; universal.update(new UpdateUserAccsBlocked(userId, true)); } } //update last pay dates if (accsToUpdate.size() > 0) { for (ChatAccount acc : accsToUpdate) { int result = universal.updateOne(new UpdateChatAccountByUid(acc.uid, acc.tariffLastPay, new TariffLastPay(curMonthStart))); //concurrent check if (result == 0) throw new ConcurrentUpdateSqlException("UpdateChatAccountByUid: uid=" + acc.uid); } } }); //update cache if (updatedBalance[0] != null) { cache.tryPutCache(getBalanceCacheKey(userId), updatedBalance[0].toString()); } //update chat servers if (accBlocked[0]) { sendAccsBlocked(props, db, cache, userId, true); } return errors; } private boolean isNeedToPay(ChatAccount acc, Date curMonthStart) { if (acc.tariffStart.compareTo(curMonthStart) >= 0) return false; return acc.tariffLastPay.before(curMonthStart); } /** ?? ? ? */ public void checkAccBlocks() throws Exception { List<UserBalance> list = universal.select(new GetAllBlockedUsers()); if (isEmpty(list)) return; List<SyncBlockError> syncErrors = new ArrayList<>(); for (UserBalance userBalance : list) { long userId = userBalance.userId; Map<String, List<String>> unblockedAccs = getUnblockedAccs(userId); if (!isEmpty(unblockedAccs)) { for (Entry<String, List<String>> entry : unblockedAccs.entrySet()) { String serverUrl = entry.getKey(); List<String> accs = entry.getValue(); syncErrors.add(new SyncBlockError(userId, serverUrl, accs)); } } } if (syncErrors.size() > 0) sendSyncErrorMailToAdmin("Unblocked accs error", syncErrors); } public void checkAccPaused() throws Exception { Map<Long, ServerRow> servers = getServersMap(universal); PausedStateResp state = getPausedState(universal); checkAccPaused(servers, state.pausedAccs, true); checkAccPaused(servers, state.unpausedAccs, false); } private void checkAccPaused(Map<Long, ServerRow> servers, Set<ChatAccount> accs, boolean expectedPaused) { HashSet<String> serverUrls = new HashSet<String>(); HashSet<String> uids = new HashSet<String>(); for (ChatAccount acc : accs) { ServerRow server = servers.get(acc.serverId); if (server == null) continue; uids.add(acc.uid); serverUrls.add(server.createUrl(URL_CHAT_GET_PAUSED_STATE)); } Map<String, List<String>> errorAccs = new HashMap<String, List<String>>(); for (String url : serverUrls) { try { GetPausedStateReq req = new GetPausedStateReq(uids, expectedPaused); ResultAccsResp result = postEncryptedJson(props, url, req, ResultAccsResp.class); if (result == null) continue; if (isEmpty(result.accs)) continue; errorAccs.put(url, result.accs); } catch (Exception e) { ExpectedException.logError(log, e, "can't connect to " + url); } } if (isEmpty(errorAccs)) return; List<SyncPausedError> syncErrors = new ArrayList<>(); for (Entry<String, List<String>> entry : errorAccs.entrySet()) { syncErrors.add(new SyncPausedError(entry.getKey(), expectedPaused, entry.getValue())); } String msg = expectedPaused ? "Unpaused accs error" : "Paused accs error"; sendSyncErrorMailToAdmin(msg, syncErrors); } private Map<String, List<String>> getUnblockedAccs(long userId) { List<ChatAccount> accs = db.chats.getOwnerAccsInfo(userId); Map<String, List<String>> out = new HashMap<String, List<String>>(); HashSet<String> serverUrls = new HashSet<String>(); HashSet<String> uids = new HashSet<String>(); for (ChatAccount acc : accs) { uids.add(acc.uid); serverUrls.add(acc.server.createUrl(URL_CHAT_GET_UNBLOKED)); } for (String url : serverUrls) { try { ResultAccsResp result = postEncryptedJson(props, url, new GetUnblockedAccsReq(uids), ResultAccsResp.class); if (result == null) continue; if (isEmpty(result.accs)) continue; out.put(url, result.accs); } catch (Exception e) { ExpectedException.logError(log, e, "can't connect to " + url); } } return out; } private void sendSyncErrorMailToAdmin(String title, List<?> syncErrors) { if (isEmpty(syncErrors)) return; if (props.getBoolVal(billing_sync_debug_DisableSendErrors)) return; List<String> msgs = convert(syncErrors, (d) -> toJson(d, true)); mailService.sendAsyncWarnData(title, msgs); } public static class SyncPayError { public Long userId; public Set<ChatAccount> userAccs; public String errorMsg; public SyncPayError(Long userId, Set<ChatAccount> userAccs, Throwable t) { this.userId = userId; this.userAccs = userAccs; this.errorMsg = printStackTrace(t); } } public static class SyncBlockError { public Long userId; public String serverUrl; public List<String> unblockedAccs; public SyncBlockError(Long userId, String serverUrl, List<String> unblockedAccs) { super(); this.userId = userId; this.serverUrl = serverUrl; this.unblockedAccs = unblockedAccs; } } public static class SyncPausedError { public String serverUrl; public boolean expectedPaused; public List<String> accs; public SyncPausedError(String serverUrl, boolean expectedPaused, List<String> accs) { this.serverUrl = serverUrl; this.expectedPaused = expectedPaused; this.accs = accs; } } }