1/*
2 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.incubator.http;
27
28import java.util.Map;
29import java.util.HashMap;
30import java.util.concurrent.locks.Condition;
31import java.util.concurrent.locks.ReentrantLock;
32
33/**
34 * A Simple blocking Send Window Flow-Controller that is used to control
35 * outgoing Connection and Stream flows, per HTTP/2 connection.
36 *
37 * A Http2Connection has its own unique single instance of a WindowController
38 * that it shares with its Streams. Each stream must acquire the appropriate
39 * amount of Send Window from the controller before sending data.
40 *
41 * WINDOW_UPDATE frames, both connection and stream specific, must notify the
42 * controller of their increments. SETTINGS frame's INITIAL_WINDOW_SIZE must
43 * notify the controller so that it can adjust the active stream's window size.
44 */
45final class WindowController {
46
47    /**
48     * Default initial connection Flow-Control Send Window size, as per HTTP/2.
49     */
50    private static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024 - 1;
51
52    /** The connection Send Window size. */
53    private int connectionWindowSize;
54    /** A Map of the active streams, where the key is the stream id, and the
55     *  value is the stream's Send Window size, which may be negative. */
56    private final Map<Integer,Integer> streams = new HashMap<>();
57
58    private final ReentrantLock controllerLock = new ReentrantLock();
59
60    private final Condition notExhausted = controllerLock.newCondition();
61
62    /** A Controller with the default initial window size. */
63    WindowController() {
64        connectionWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
65    }
66
67    /** A Controller with the given initial window size. */
68    WindowController(int initialConnectionWindowSize) {
69        connectionWindowSize = initialConnectionWindowSize;
70    }
71
72    /** Registers the given stream with this controller. */
73    void registerStream(int streamid, int initialStreamWindowSize) {
74        controllerLock.lock();
75        try {
76            Integer old = streams.put(streamid, initialStreamWindowSize);
77            if (old != null)
78                throw new InternalError("Unexpected entry [" + old + "] for streamid: " + streamid);
79        } finally {
80            controllerLock.unlock();
81        }
82    }
83
84    /** Removes/De-registers the given stream with this controller. */
85    void removeStream(int streamid) {
86        controllerLock.lock();
87        try {
88            Integer old = streams.remove(streamid);
89            // Odd stream numbers (client streams) should have been registered.
90            // Even stream numbers (server streams - aka Push Streams) should
91            // not be registered
92            final boolean isClientStream = (streamid % 2) == 1;
93            if (old == null && isClientStream) {
94                throw new InternalError("Expected entry for streamid: " + streamid);
95            } else if (old != null && !isClientStream) {
96                throw new InternalError("Unexpected entry for streamid: " + streamid);
97            }
98        } finally {
99            controllerLock.unlock();
100        }
101    }
102
103    /**
104     * Attempts to acquire the requested amount of Send Window for the given
105     * stream.
106     *
107     * The actual amount of Send Window available may differ from the requested
108     * amount. The actual amount, returned by this method, is the minimum of,
109     * 1) the requested amount, 2) the stream's Send Window, and 3) the
110     * connection's Send Window.
111     *
112     * This method ( currently ) blocks until some positive amount of Send
113     * Window is available.
114     */
115    int tryAcquire(int requestAmount, int streamid) throws InterruptedException {
116        controllerLock.lock();
117        try {
118            int x = 0;
119            Integer streamSize = 0;
120            while (x <= 0) {
121                streamSize = streams.get(streamid);
122                if (streamSize == null)
123                    throw new InternalError("Expected entry for streamid: " + streamid);
124                x = Math.min(requestAmount,
125                             Math.min(streamSize, connectionWindowSize));
126
127                if (x <= 0)  // stream window size may be negative
128                    notExhausted.await();
129            }
130
131            streamSize -= x;
132            streams.put(streamid, streamSize);
133            connectionWindowSize -= x;
134            return x;
135        } finally {
136            controllerLock.unlock();
137        }
138    }
139
140    /**
141     * Increases the Send Window size for the connection.
142     *
143     * @return false if, and only if, the addition of the given amount would
144     *         cause the Send Window to exceed 2^31-1 (overflow), otherwise true
145     */
146    boolean increaseConnectionWindow(int amount) {
147        controllerLock.lock();
148        try {
149            int size = connectionWindowSize;
150            size += amount;
151            if (size < 0)
152                return false;
153            connectionWindowSize = size;
154            notExhausted.signalAll();
155        } finally {
156            controllerLock.unlock();
157        }
158        return true;
159    }
160
161    /**
162     * Increases the Send Window size for the given stream.
163     *
164     * @return false if, and only if, the addition of the given amount would
165     *         cause the Send Window to exceed 2^31-1 (overflow), otherwise true
166     */
167    boolean increaseStreamWindow(int amount, int streamid) {
168        controllerLock.lock();
169        try {
170            Integer size = streams.get(streamid);
171            if (size == null)
172                throw new InternalError("Expected entry for streamid: " + streamid);
173            size += amount;
174            if (size < 0)
175                return false;
176            streams.put(streamid, size);
177            notExhausted.signalAll();
178        } finally {
179            controllerLock.unlock();
180        }
181        return true;
182    }
183
184    /**
185     * Adjusts, either increases or decreases, the active streams registered
186     * with this controller.  May result in a stream's Send Window size becoming
187     * negative.
188     */
189    void adjustActiveStreams(int adjustAmount) {
190        assert adjustAmount != 0;
191
192        controllerLock.lock();
193        try {
194            for (Map.Entry<Integer,Integer> entry : streams.entrySet()) {
195                int streamid = entry.getKey();
196                // the API only supports sending on Streams initialed by
197                // the client, i.e. odd stream numbers
198                if (streamid != 0 && (streamid % 2) != 0) {
199                    Integer size = entry.getValue();
200                    size += adjustAmount;
201                    streams.put(streamid, size);
202                }
203            }
204        } finally {
205            controllerLock.unlock();
206        }
207    }
208
209    /** Returns the Send Window size for the connection. */
210    int connectionWindowSize() {
211        controllerLock.lock();
212        try {
213            return connectionWindowSize;
214        } finally {
215            controllerLock.unlock();
216        }
217    }
218
219    /** Returns the Send Window size for the given stream. */
220    int streamWindowSize(int streamid) {
221        controllerLock.lock();
222        try {
223            Integer size = streams.get(streamid);
224            if (size == null)
225                throw new InternalError("Expected entry for streamid: " + streamid);
226            return size;
227        } finally {
228            controllerLock.unlock();;
229        }
230    }
231}
232