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