View Javadoc
1   /*
2    * Copyright 2024 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://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  package io.netty.resolver.dns;
17  
18  import io.netty.util.internal.MathUtil;
19  
20  import java.security.SecureRandom;
21  
22  /**
23   * Special data-structure that will allow to retrieve the next query id to use, while still guarantee some sort
24   * of randomness.
25   * The query id will be between 0 (inclusive) and 65535 (inclusive) as defined by the RFC.
26   */
27  final class DnsQueryIdSpace {
28      private static final int MAX_ID = 65535;
29      private static final int BUCKETS = 4;
30      // Each bucket is 16kb of size.
31      private static final int BUCKET_SIZE = (MAX_ID + 1) / BUCKETS;
32  
33      // If there are other buckets left that have at least 500 usable ids we will drop an unused bucket.
34      private static final int BUCKET_DROP_THRESHOLD = 500;
35      private final DnsQueryIdRange[] idBuckets = new DnsQueryIdRange[BUCKETS];
36      private final SecureRandom random = new SecureRandom();
37  
38      DnsQueryIdSpace() {
39          assert idBuckets.length == MathUtil.findNextPositivePowerOfTwo(idBuckets.length);
40          // We start with 1 bucket.
41          idBuckets[0] = newBucket(0, random);
42      }
43  
44      private static DnsQueryIdRange newBucket(int idBucketsIdx, SecureRandom random) {
45          return new DnsQueryIdRange(BUCKET_SIZE, idBucketsIdx * BUCKET_SIZE, random);
46      }
47  
48      /**
49       * Returns the next ID to use for a query or {@code -1} if there is none left to use.
50       *
51       * @return next id to use.
52       */
53      int nextId() {
54          int freeIdx = -1;
55          for (int bucketIdx = 0; bucketIdx < idBuckets.length; bucketIdx++) {
56              DnsQueryIdRange bucket = idBuckets[bucketIdx];
57              if (bucket != null) {
58                  int id = bucket.nextId();
59                  if (id != -1) {
60                      return id;
61                  }
62              } else if (freeIdx == -1 ||
63                      // Let's make it somehow random which free slot is used.
64                      random.nextBoolean()) {
65                  // We have a slot that we can use to create a new bucket if we need to.
66                  freeIdx = bucketIdx;
67              }
68          }
69          if (freeIdx == -1) {
70              // No ids left and no slot left to create a new bucket.
71              return -1;
72          }
73  
74          // We still have some slots free to store a new bucket. Let's do this now and use it to generate the next id.
75          DnsQueryIdRange bucket = newBucket(freeIdx, random);
76          idBuckets[freeIdx] = bucket;
77          int id = bucket.nextId();
78          assert id >= 0;
79          return id;
80      }
81  
82      /**
83       * Push back the id, so it can be used again for the next query.
84       *
85       * @param id the id.
86       */
87      void pushId(int id) {
88          int bucketIdx = id / BUCKET_SIZE;
89          if (bucketIdx >= idBuckets.length) {
90              throw new IllegalArgumentException("id too large: " + id);
91          }
92          DnsQueryIdRange bucket = idBuckets[bucketIdx];
93          assert bucket != null;
94          bucket.pushId(id);
95  
96          if (bucket.usableIds() == bucket.maxUsableIds()) {
97              // All ids are usable in this bucket. Let's check if there are other buckets left that have still
98              // some space left and if so drop this bucket.
99              for (int idx = 0; idx < idBuckets.length; idx++) {
100                 if (idx != bucketIdx) {
101                     DnsQueryIdRange otherBucket = idBuckets[idx];
102                     if (otherBucket != null && otherBucket.usableIds() > BUCKET_DROP_THRESHOLD) {
103                         // Drop bucket on the floor to reduce memory usage, there is another bucket left we can
104                         // use that still has enough ids to use.
105                         idBuckets[bucketIdx] = null;
106                         return;
107                     }
108                 }
109             }
110         }
111     }
112 
113     /**
114      * Return how much more usable ids are left.
115      *
116      * @return the number of ids that are left for usage.
117      */
118     int usableIds() {
119         int usableIds = 0;
120         for (DnsQueryIdRange bucket: idBuckets) {
121             // If there is nothing stored in the index yet we can assume the whole bucket is usable
122             usableIds += bucket == null ? BUCKET_SIZE : bucket.usableIds();
123         }
124         return usableIds;
125     }
126 
127     /**
128      * Return the maximum number of ids that are supported.
129      *
130      * @return the maximum number of ids.
131      */
132     int maxUsableIds() {
133         return BUCKET_SIZE * idBuckets.length;
134     }
135 
136     /**
137      * Provides a query if from a range of possible ids.
138      */
139     private static final class DnsQueryIdRange {
140 
141         // Holds all possible ids which are stored as unsigned shorts
142         private final short[] ids;
143         private final int startId;
144         private final SecureRandom random;
145         private int count;
146 
147         DnsQueryIdRange(int bucketSize, int startId, SecureRandom random) {
148             this.ids = new short[bucketSize];
149             this.startId = startId;
150             this.random = random;
151             for (int v = startId; v < bucketSize + startId; v++) {
152                 pushId(v);
153             }
154         }
155 
156         /**
157          * Returns the next ID to use for a query or {@code -1} if there is none left to use.
158          *
159          * @return next id to use.
160          */
161         int nextId() {
162             assert count >= 0;
163             if (count == 0) {
164                 return -1;
165             }
166             short id = ids[count - 1];
167             count--;
168 
169             return id & 0xFFFF;
170         }
171 
172         /**
173          * Push back the id, so it can be used again for the next query.
174          *
175          * @param id the id.
176          */
177         void pushId(int id) {
178             if (count == ids.length) {
179                 throw new IllegalStateException("overflow");
180             }
181             assert id <= startId + ids.length && id >= startId;
182             // pick a slot for our index, and whatever was in that slot before will get moved to the tail.
183             int insertionPosition = random.nextInt(count + 1);
184             short moveId = ids[insertionPosition];
185             short insertId = (short) id;
186 
187             // Assert that the ids are different or its the same index.
188             assert moveId != insertId || insertionPosition == count;
189 
190             ids[count] = moveId;
191             ids[insertionPosition] = insertId;
192             count++;
193         }
194 
195         /**
196          * Return how much more usable ids are left.
197          *
198          * @return the number of ids that are left for usage.
199          */
200         int usableIds() {
201             return count;
202         }
203 
204         /**
205          * Return the maximum number of ids that are supported.
206          *
207          * @return the maximum number of ids.
208          */
209         int maxUsableIds() {
210             return ids.length;
211         }
212     }
213 }