Skip to content

Commit 69ab1b9

Browse files
authored
Add configurable Arrow memory limit - withArrowMemoryLimitMB (#32)
1 parent a9b888a commit 69ab1b9

6 files changed

Lines changed: 140 additions & 15 deletions

File tree

.github/workflows/build.yaml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,14 @@ jobs:
4141
if: matrix.os == 'windows-latest'
4242
run: mvn --% install -DskipTests=true -Dgpg.skip -B -V # tell powershell to stop parsing with --% so it doesn't error with "Unknown lifecycle phase .skip"
4343

44-
- name: Install Spice (https://install.spiceai.org) (Linux)
45-
if: matrix.os == 'ubuntu-latest'
44+
- name: Install Spice (https://install.spiceai.org) (Unix)
45+
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest'
4646
env:
4747
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4848
run: |
4949
curl https://install.spiceai.org | /bin/bash
5050
echo "$HOME/.spice/bin" >> $GITHUB_PATH
5151
$HOME/.spice/bin/spice install
52-
53-
- name: Install Spice (https://install.spiceai.org) (MacOS)
54-
if: matrix.os == 'macos-latest'
55-
run: |
56-
brew install spiceai/spiceai/spice
57-
brew install spiceai/spiceai/spiced
5852
5953
- name: install Spice (Windows)
6054
if: matrix.os == 'windows-latest'

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ SpiceClient client = SpiceClient.builder()
122122
Retries are performed for connection and system internal errors. It is the SDK user's responsibility to properly
123123
handle other errors, for example RESOURCE_EXHAUSTED (HTTP 429).
124124

125+
### Memory Configuration
126+
127+
The `SpiceClient` uses an Arrow `RootAllocator` for managing off-heap memory. By default, it uses all available memory. You can configure the memory limit using megabytes:
128+
129+
```java
130+
SpiceClient client = SpiceClient.builder()
131+
.withArrowMemoryLimitMB(1024) // 1GB limit
132+
.build();
133+
```
134+
125135
### Spice.ai Runtime commands
126136

127137
#### Accelerated dataset refresh

src/main/java/ai/spice/Config.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ public static String getUserAgent() {
127127
+ System.getProperty("os.version") + " "
128128
+ osArch + ")";
129129
}
130-
}
130+
}

src/main/java/ai/spice/SpiceClient.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ of this software and associated documentation files (the "Software"), to deal
6161
*/
6262
public class SpiceClient implements AutoCloseable {
6363

64+
private static final long BYTES_PER_MB = 1024L * 1024L;
65+
6466
private String appId;
6567
private String apiKey;
6668
private URI flightAddress;
@@ -86,13 +88,19 @@ public static SpiceClientBuilder builder() throws URISyntaxException {
8688
* application
8789
* @param apiKey the API key used for authentication with Spice.ai
8890
* services
89-
* @param flightAddress the URI of the flight address for connecting to Spice.ai
91+
* @param flightAddress the URI of the flight address for connecting to
92+
* Spice.ai
9093
* services
9194
* @param httpAddress the URI of the Spice.ai runtime HTTP address
9295
*
93-
* @param maxRetries the maximum number of connection retries for the client
96+
* @param maxRetries the maximum number of connection retries for the
97+
* client
98+
* @param userAgent the user agent string
99+
* @param memoryLimitMB the memory limit in megabytes for the Arrow
100+
* RootAllocator
94101
*/
95-
public SpiceClient(String appId, String apiKey, URI flightAddress, URI httpAddress, int maxRetries, String userAgent) {
102+
public SpiceClient(String appId, String apiKey, URI flightAddress, URI httpAddress, int maxRetries,
103+
String userAgent, long memoryLimitMB) {
96104
this.appId = appId;
97105
this.apiKey = apiKey;
98106
this.maxRetries = maxRetries;
@@ -108,7 +116,12 @@ public SpiceClient(String appId, String apiKey, URI flightAddress, URI httpAddre
108116
this.flightAddress = flightAddress;
109117
}
110118

111-
Builder builder = FlightClient.builder(new RootAllocator(Long.MAX_VALUE), new Location(this.flightAddress));
119+
// Convert megabytes to bytes for RootAllocator:
120+
// https://arrow.apache.org/java/main/reference/org.apache.arrow.memory.core/org/apache/arrow/memory/RootAllocator.html
121+
long memoryLimitBytes = (memoryLimitMB > Long.MAX_VALUE / BYTES_PER_MB)
122+
? Long.MAX_VALUE
123+
: memoryLimitMB * BYTES_PER_MB;
124+
Builder builder = FlightClient.builder(new RootAllocator(memoryLimitBytes), new Location(this.flightAddress));
112125

113126
if (Strings.isNullOrEmpty(apiKey)) {
114127
this.flightClient = new FlightSqlClient(builder.build());

src/main/java/ai/spice/SpiceClientBuilder.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class SpiceClientBuilder {
3838
private URI flightAddress;
3939
private URI httpAddress;
4040
private int maxRetries = 3;
41+
private long memoryLimitMB = Long.MAX_VALUE; // Default is all available memory.
4142

4243
/**
4344
* Constructs a new SpiceClientBuilder instance
@@ -139,12 +140,39 @@ public SpiceClientBuilder withMaxRetries(int maxRetries) {
139140
return this;
140141
}
141142

143+
/**
144+
* Sets the memory limit for Apache Arrow allocator in megabytes.
145+
* This controls the maximum amount of off-heap memory that can be allocated
146+
* for Arrow Flight operations. If not set, the allocator will use all available
147+
* memory.
148+
*
149+
* @param memoryLimitMB Maximum memory limit in megabytes. Default is all
150+
* available memory.
151+
* Must be positive.
152+
* @return The current instance of SpiceClientBuilder for method chaining.
153+
* @throws IllegalArgumentException if memoryLimitMB is not positive
154+
*
155+
* @see org.apache.arrow.memory.RootAllocator
156+
*/
157+
public SpiceClientBuilder withArrowMemoryLimitMB(long memoryLimitMB) {
158+
if (memoryLimitMB <= 0) {
159+
throw new IllegalArgumentException("Memory limit must be positive, got: " + memoryLimitMB + " MB");
160+
}
161+
if (memoryLimitMB > Long.MAX_VALUE / 1024L / 1024L) {
162+
throw new IllegalArgumentException(
163+
"Memory limit is too large: " + memoryLimitMB + " MB");
164+
}
165+
166+
this.memoryLimitMB = memoryLimitMB;
167+
return this;
168+
}
169+
142170
/**
143171
* Creates SpiceClient with provided parameters.
144172
*
145173
* @return The SpiceClient instance
146174
*/
147175
public SpiceClient build() {
148-
return new SpiceClient(appId, apiKey, flightAddress, httpAddress, maxRetries, userAgent);
176+
return new SpiceClient(appId, apiKey, flightAddress, httpAddress, maxRetries, userAgent, memoryLimitMB);
149177
}
150-
}
178+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package ai.spice;
2+
3+
import junit.framework.TestCase;
4+
5+
/**
6+
* Test memory configuration functionality
7+
*/
8+
public class MemoryConfigurationTest extends TestCase {
9+
10+
public void testMemoryLimitMBConfiguration() throws Exception {
11+
try {
12+
// Test that memory limit in MB is properly configured
13+
SpiceClient client = SpiceClient.builder()
14+
.withArrowMemoryLimitMB(128) // 128 MB
15+
.build();
16+
17+
// If we reach here without exception, the configuration worked
18+
assertTrue("Memory configuration should not throw exception", true);
19+
20+
client.close();
21+
} catch (Exception e) {
22+
fail("Should not throw exception: " + e.getMessage());
23+
}
24+
}
25+
26+
public void testInvalidMemoryLimitMB() throws Exception {
27+
try {
28+
SpiceClient.builder()
29+
.withArrowMemoryLimitMB(0) // Invalid: must be positive
30+
.build();
31+
fail("Should throw IllegalArgumentException for zero memory limit");
32+
} catch (IllegalArgumentException e) {
33+
// Expected exception
34+
assertTrue("Should throw IllegalArgumentException for zero memory limit", true);
35+
}
36+
}
37+
38+
public void testNegativeMemoryLimitMB() throws Exception {
39+
try {
40+
SpiceClient.builder()
41+
.withArrowMemoryLimitMB(-100) // Invalid: negative
42+
.build();
43+
fail("Should throw IllegalArgumentException for negative memory limit");
44+
} catch (IllegalArgumentException e) {
45+
assertTrue("Should throw IllegalArgumentException for negative memory limit", true);
46+
}
47+
}
48+
49+
public void testOverflowProtection() throws Exception {
50+
// Test overflow protection - values that would cause overflow when converted to
51+
// bytes
52+
long maxSafeMB = Long.MAX_VALUE / (1024L * 1024L);
53+
long overflowValue = maxSafeMB + 1;
54+
55+
try {
56+
SpiceClient.builder()
57+
.withArrowMemoryLimitMB(overflowValue) // Would cause overflow
58+
.build();
59+
fail("Should throw IllegalArgumentException for overflow-causing memory limit");
60+
} catch (IllegalArgumentException e) {
61+
assertTrue("Should throw IllegalArgumentException for overflow protection", true);
62+
}
63+
}
64+
65+
public void testMaxSafeMemoryLimit() throws Exception {
66+
// Test that the maximum safe value works without throwing
67+
long maxSafeMB = Long.MAX_VALUE / (1024L * 1024L);
68+
69+
try {
70+
SpiceClient client = SpiceClient.builder()
71+
.withArrowMemoryLimitMB(maxSafeMB) // Maximum safe value
72+
.build();
73+
74+
assertTrue("Maximum safe memory limit should work", true);
75+
client.close();
76+
} catch (Exception e) {
77+
fail("Should not throw exception: " + e.getMessage());
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)