001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.util; 020 021import java.time.Instant; 022 023/** 024 * ThroughputControl limits the throughput 025 * of a process to a maximum number of transactions in 026 * a given period of time. 027 * 028 * As an example, the following code will cap the transaction count 029 * at 15 every second (a.k.a. 15 TPS). 030 * 031 * <pre> 032 * 033 * ThroughputControl throughput = new ThroughputControl(15, 1000); 034 * 035 * while (isConditionTrue()) { 036 * throughput.control(); 037 * // Do stuff. 038 * } 039 * 040 * </pre> 041 */ 042public class ThroughputControl { 043 private int[] period; 044 private int[] max; 045 private int[] cnt; 046 private long[] start; 047 private long[] sleep; 048 049 /** 050 * @param maxTransactions Transaction count threshold. 051 * @param periodInMillis Time window, expressed in milliseconds. 052 */ 053 public ThroughputControl (int maxTransactions, int periodInMillis) { 054 this (new int[] { maxTransactions }, 055 new int[] { periodInMillis }); 056 } 057 /** 058 * @param maxTransactions An array with transaction count thresholds. 059 * @param periodInMillis An array of time windows, expressed in milliseconds. 060 */ 061 public ThroughputControl (int[] maxTransactions, int[] periodInMillis) { 062 super(); 063 int l = maxTransactions.length; 064 period = new int[l]; 065 max = new int[l]; 066 cnt = new int[l]; 067 start = new long[l]; 068 sleep = new long[l]; 069 for (int i=0; i<l; i++) { 070 this.max[i] = maxTransactions[i]; 071 this.period[i] = periodInMillis[i]; 072 this.sleep[i] = Math.min(Math.max (periodInMillis[i]/10, 500L),50L); 073 this.start[i] = Instant.now().toEpochMilli(); 074 } 075 } 076 077 /** 078 * This method should be called on every transaction. 079 * It will pause the thread for a while when the threshold is reached 080 * in order to control the process throughput. 081 * 082 * @return Returns sleep time in milliseconds when threshold is reached. Otherwise, zero. 083 */ 084 public long control() { 085 boolean delayed = false; 086 long init = Instant.now().toEpochMilli(); 087 for (int i=0; i<cnt.length; i++) { 088 synchronized (this) { 089 cnt[i]++; 090 } 091 do { 092 if (cnt[i] > max[i]) { 093 delayed = true; 094 try { 095 Thread.sleep (sleep[i]); 096 } catch (InterruptedException e) { } 097 } 098 synchronized (this) { 099 long now = Instant.now().toEpochMilli(); 100 if (now - start[i] > period[i]) { 101 long elapsed = now - start[i]; 102 int allowed = (int) (elapsed * max[i] / period[i]); 103 start[i] = now; 104 cnt[i] = Math.max (cnt[i] - allowed, 0); 105 } 106 } 107 } while (cnt[i] > max[i]); 108 } 109 return delayed ? Instant.now().toEpochMilli() - init : 0L; 110 } 111 112 @Override 113 public String toString() { 114 StringBuilder sb = new StringBuilder("ThroughputControl ["); 115 for (int i = 0; i < max.length; i++) { 116 sb.append(String.format( 117 "%d: max = %d, period = %dms", 118 i, max[i], period[i] 119 )); 120 if (i < max.length - 1) { 121 sb.append("; "); 122 } 123 } 124 sb.append("]"); 125 return sb.toString(); 126 } 127} 128