1+ /*
2+ * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
3+ *
4+ * This software is licensed under the Apache License, Version 2.0 (the
5+ * "License") as published by the Apache Software Foundation.
6+ *
7+ * You may not use this file except in compliance with the License. You may
8+ * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+ * License for the specific language governing permissions and limitations
14+ * under the License.
15+ */
16+
17+ package io .supertokens .webserver ;
18+
19+ import com .google .gson .JsonArray ;
20+ import com .google .gson .JsonObject ;
21+ import com .google .gson .JsonPrimitive ;
22+ import io .supertokens .Main ;
23+ import io .supertokens .ResourceDistributor ;
24+ import io .supertokens .multitenancy .Multitenancy ;
25+ import io .supertokens .pluginInterface .multitenancy .AppIdentifier ;
26+ import io .supertokens .pluginInterface .multitenancy .exceptions .TenantOrAppNotFoundException ;
27+
28+ public class RequestStats extends ResourceDistributor .SingletonResource {
29+ public static final String RESOURCE_KEY = "io.supertokens.webserver.RequestStats" ;
30+
31+ private final int MAX_MINUTES = 24 * 60 ;
32+
33+ private long currentMinute ; // current minute since epoch
34+ private final int [] currentMinuteRequestCounts ; // array of 60 items representing number of requests at each second in the current minute
35+
36+ // The 2 arrays below contains stats for a day for every minute
37+ // the array is stored in such a way that array[currentMinute % MAX_MINUTES] contains the stats for a day ago
38+ // until array[(currentMinute - 1) % MAX_MINUTES] which contains the stats for the last minute, circling around
39+ // from end of array to the beginning
40+ // for e.g. if currentMinute % MAX_MINUTES = 250,
41+ // then array[250] contains stats for now - 1440 minutes
42+ // array[251] contains stats for now - 1439 minutes
43+ // ...
44+ // array[1439] contains stats for now - 1191 minutes
45+ // array[0] contains stats for now - 1190 minutes
46+ // array[1] contains stats for now - 1189 minutes
47+ // ...
48+ // array[249] contains stats for now - 1 minute
49+ private final double [] averageRequestsPerSecond ;
50+ private final int [] peakRequestsPerSecond ;
51+
52+ private RequestStats () {
53+ currentMinute = System .currentTimeMillis () / 60000 ;
54+ currentMinuteRequestCounts = new int [60 ];
55+
56+ averageRequestsPerSecond = new double [MAX_MINUTES ];
57+ peakRequestsPerSecond = new int [MAX_MINUTES ];
58+ for (int i = 0 ; i < MAX_MINUTES ; i ++) {
59+ averageRequestsPerSecond [i ] = -1 ;
60+ peakRequestsPerSecond [i ] = -1 ;
61+ }
62+ }
63+
64+ private void checkAndUpdateMinute (long currentSecond ) {
65+ if (currentSecond / 60 == currentMinute ) {
66+ return ; // stats update not required
67+ }
68+
69+ int sum = 0 ;
70+ int max = 0 ;
71+ for (int i = 0 ; i < 60 ; i ++) {
72+ sum += currentMinuteRequestCounts [i ];
73+ max = Math .max (max , currentMinuteRequestCounts [i ]);
74+ }
75+
76+ averageRequestsPerSecond [(int ) (currentMinute % MAX_MINUTES )] = sum / 60.0 ;
77+ peakRequestsPerSecond [(int ) (currentMinute % MAX_MINUTES )] = max ;
78+
79+ // fill zeros for passed minutes
80+ for (long i = currentMinute + 1 ; i < currentSecond / 60 ; i ++) {
81+ averageRequestsPerSecond [(int ) (i % MAX_MINUTES )] = 0 ;
82+ peakRequestsPerSecond [(int ) (i % MAX_MINUTES )] = 0 ;
83+ }
84+
85+ currentMinute = currentSecond / 60 ;
86+ for (int i = 0 ; i < 60 ; i ++) {
87+ currentMinuteRequestCounts [i ] = 0 ;
88+ }
89+ }
90+
91+ private void updateCounts (long currentSecond ) {
92+ currentMinuteRequestCounts [(int ) (currentSecond % 60 )]++;
93+ }
94+
95+ public static RequestStats getInstance (Main main , AppIdentifier appIdentifier ) throws TenantOrAppNotFoundException {
96+ try {
97+ return (RequestStats ) main .getResourceDistributor ()
98+ .getResource (appIdentifier .getAsPublicTenantIdentifier (), RESOURCE_KEY );
99+ } catch (TenantOrAppNotFoundException e ) {
100+ // appIdentifier parameter is coming from the API request and hence we need to check if the app exists
101+ // before creating a resource for it, otherwise someone could fill up memory by making requests for apps
102+ // that don't exist.
103+ // The other resources are created during init or while refreshing tenants from the db, so we don't need
104+ // this kind of pattern for those resources.
105+ if (Multitenancy .getTenantInfo (main , appIdentifier .getAsPublicTenantIdentifier ()) == null ) {
106+ throw e ;
107+ }
108+ return (RequestStats ) main .getResourceDistributor ()
109+ .setResource (appIdentifier .getAsPublicTenantIdentifier (), RESOURCE_KEY , new RequestStats ());
110+ }
111+ }
112+
113+ public void updateRequestStats () {
114+ this .updateRequestStats (true );
115+ }
116+
117+ synchronized private void updateRequestStats (boolean updateCounts ) {
118+ long now = System .currentTimeMillis () / 1000 ;
119+ this .checkAndUpdateMinute (now );
120+ if (updateCounts ) { this .updateCounts (now ); }
121+ }
122+
123+ public JsonObject getStats () {
124+ this .updateRequestStats (false );
125+
126+ JsonArray avgRps = new JsonArray ();
127+ JsonArray peakRps = new JsonArray ();
128+
129+ long atMinute = System .currentTimeMillis () / 60000 ;
130+
131+ int offset = (int ) (atMinute % MAX_MINUTES );
132+ for (int i = 0 ; i < MAX_MINUTES ; i ++) {
133+ avgRps .add (new JsonPrimitive (this .averageRequestsPerSecond [(i + offset ) % MAX_MINUTES ]));
134+ peakRps .add (new JsonPrimitive (this .peakRequestsPerSecond [(i + offset ) % MAX_MINUTES ]));
135+ }
136+
137+ JsonObject result = new JsonObject ();
138+ result .addProperty ("atMinute" , atMinute );
139+ result .add ("averageRequestsPerSecond" , avgRps );
140+ result .add ("peakRequestsPerSecond" , peakRps );
141+ return result ;
142+ }
143+ }
0 commit comments