Skip to content

Commit b5eea88

Browse files
committed
Fix jni
1 parent 457092d commit b5eea88

8 files changed

Lines changed: 228 additions & 106 deletions

File tree

crates/vespera/src/multipart.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,13 @@ const DEFAULT_STRING_FIELD_LIMIT_BYTES: usize = 1024 * 1024; // 1 MiB
785785
/// `usize::MAX` through the derive-generated parser. Applications can tune the
786786
/// process-wide default before handling requests with
787787
/// [`set_default_temp_file_field_limit_bytes`].
788+
///
789+
/// Note: `"unlimited"` lifts only this **per-field** cap. The request-wide
790+
/// aggregate budget ([`DEFAULT_MULTIPART_MAX_TOTAL_BYTES`], 64 MiB by default)
791+
/// still applies, so a single `"unlimited"` field is bounded by the aggregate
792+
/// rather than being truly unbounded. To raise the aggregate, use
793+
/// [`TypedMultipartWithLimits`] (per-route) or [`set_default_multipart_limits`]
794+
/// (process-wide); genuinely large uploads should stream instead.
788795
pub const DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES: usize = 16 * 1024 * 1024; // 16 MiB
789796

790797
static DEFAULT_TEMP_FILE_FIELD_LIMIT: AtomicUsize =

crates/vespera_macro/src/schema_macro/same_file_override.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,20 @@ pub(super) fn find_same_file_struct_metadata<'a>(
6060
}
6161

6262
pub(super) fn related_model_type_from_schema_path(schema_path: &TokenStream) -> Option<syn::Type> {
63-
let schema_path_str = schema_path.to_string().replace("Schema", "Model");
64-
syn::parse_str(&schema_path_str).ok()
63+
// Map a schema path (`…::UserSchema`) to its model path (`…::UserModel`)
64+
// by renaming ONLY the final path segment's identifier. The previous
65+
// `to_string().replace("Schema", "Model")` rewrote EVERY "Schema"
66+
// substring in the whole path string, so a *module* segment that itself
67+
// contained "Schema" (e.g. `crate::SchemaStore::UserSchema`) was silently
68+
// corrupted into `crate::ModelStore::UserModel`, producing a dangling /
69+
// wrong `From<Model>` target. Parsing to a `TypePath` and editing only
70+
// the last segment keeps every module segment verbatim and preserves any
71+
// generic arguments on the segment.
72+
let mut type_path: syn::TypePath = syn::parse2(schema_path.clone()).ok()?;
73+
let last = type_path.path.segments.last_mut()?;
74+
let renamed = last.ident.to_string().replace("Schema", "Model");
75+
last.ident = syn::Ident::new(&renamed, last.ident.span());
76+
Some(syn::Type::Path(type_path))
6577
}
6678

6779
pub(super) fn has_derive(struct_item: &syn::ItemStruct, derive_name: &str) -> bool {
@@ -451,6 +463,29 @@ mod tests {
451463
assert!(result.is_none());
452464
}
453465

466+
#[test]
467+
fn related_model_type_renames_only_final_segment() {
468+
// A module segment that itself contains "Schema" (capital S) must be
469+
// left verbatim — only the trailing `…Schema` ident becomes `…Model`.
470+
// The previous `to_string().replace("Schema","Model")` corrupted
471+
// `SchemaStore` into `ModelStore`; this locks the regression.
472+
let ty = related_model_type_from_schema_path(&quote!(crate::SchemaStore::user::UserSchema))
473+
.expect("valid schema path resolves to a model type");
474+
assert_eq!(
475+
quote!(#ty).to_string(),
476+
quote!(crate::SchemaStore::user::UserModel).to_string(),
477+
);
478+
// A bare trailing `Schema` ident maps to `Model`.
479+
let ty2 = related_model_type_from_schema_path(&quote!(crate::models::user::Schema))
480+
.expect("valid schema path");
481+
assert_eq!(
482+
quote!(#ty2).to_string(),
483+
quote!(crate::models::user::Model).to_string(),
484+
);
485+
// A non-path token stream (e.g. a stray `?`) yields None, not a panic.
486+
assert!(related_model_type_from_schema_path(&quote!(?)).is_none());
487+
}
488+
454489
#[test]
455490
fn test_generate_schema_type_code_normal_mode_relation_rename_and_custom_name() {
456491
let storage = to_storage(vec![create_test_struct_metadata(

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/VesperaDirectBufferPool.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ private static void recordDirectPoolUse(ByteBuffer[] pool, int requestLen, int r
162162
DIRECT_UNDER_RETAIN_STREAK.set(streak);
163163
return;
164164
}
165-
boolean requestGrown = pool[0].capacity() > DIRECT_INITIAL_CAPACITY;
166-
boolean responseGrown = pool[1].capacity() > DIRECT_INITIAL_CAPACITY;
165+
boolean requestGrown = pool[0].capacity() > DIRECT_RETAIN_CAPACITY;
166+
boolean responseGrown = pool[1].capacity() > DIRECT_RETAIN_CAPACITY;
167167
if (requestGrown) {
168168
pool[0] = ByteBuffer.allocateDirect(DIRECT_INITIAL_CAPACITY);
169169
}

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/WireHeaderReader.java

Lines changed: 96 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -62,41 +62,40 @@ static void apply(
6262
BiConsumer<String, String> headerSink) {
6363
WireHeaderReader r = new WireHeaderReader(buf, off, len);
6464
int status = 500;
65-
if (r.peek() == '{') {
66-
r.beginObject();
67-
int seen = 0;
68-
int key;
69-
while ((key = r.nextRootKey()) != KEY_END) {
70-
seen = r.rejectDuplicateRootKey(seen, key);
71-
switch (key) {
72-
case KEY_STATUS -> status = r.readInt();
73-
case KEY_HEADERS -> {
74-
if (r.isObjectStart()) {
75-
r.beginObject();
76-
String k;
77-
// Canonical keys reuse one shared String per common
78-
// header name (content-type, content-length, …) —
79-
// the same allocation-free path decode() uses, so
80-
// the per-request DIRECT/streaming apply() no longer
81-
// allocates a fresh key String for each header.
82-
while ((k = r.nextKeyCanonical()) != null) {
83-
if (r.isArrayStart()) {
84-
r.beginArray();
85-
while (r.hasNextElement()) {
86-
headerSink.accept(k, r.readString());
87-
}
88-
} else {
65+
r.requireObjectStart();
66+
r.beginObject();
67+
int seen = 0;
68+
int key;
69+
while ((key = r.nextRootKey()) != KEY_END) {
70+
seen = r.rejectDuplicateRootKey(seen, key);
71+
switch (key) {
72+
case KEY_STATUS -> status = r.readInt();
73+
case KEY_HEADERS -> {
74+
if (r.isObjectStart()) {
75+
r.beginObject();
76+
String k;
77+
// Canonical keys reuse one shared String per common
78+
// header name (content-type, content-length, …) —
79+
// the same allocation-free path decode() uses, so
80+
// the per-request DIRECT/streaming apply() no longer
81+
// allocates a fresh key String for each header.
82+
while ((k = r.nextKeyCanonical()) != null) {
83+
if (r.isArrayStart()) {
84+
r.beginArray();
85+
while (r.hasNextElement()) {
8986
headerSink.accept(k, r.readString());
9087
}
88+
} else {
89+
headerSink.accept(k, r.readString());
9190
}
92-
} else {
93-
r.skipValue();
9491
}
92+
} else {
93+
r.skipValue();
9594
}
96-
// KEY_OTHER: "v", "metadata", "validation_errors", … —
97-
// matched by bytes, value skipped, never materialised.
98-
default -> r.skipValue();
9995
}
96+
// KEY_OTHER: "v", "metadata", "validation_errors", … —
97+
// matched by bytes, value skipped, never materialised.
98+
default -> r.skipValue();
10099
}
101100
}
102101
statusSink.accept(status);
@@ -133,76 +132,75 @@ static final class Decoded {
133132
static Decoded decode(ByteBuffer buf, int off, int len) {
134133
WireHeaderReader r = new WireHeaderReader(buf, off, len);
135134
Decoded out = new Decoded();
136-
if (r.peek() == '{') {
137-
r.beginObject();
138-
int seen = 0;
139-
int key;
140-
while ((key = r.nextRootKey()) != KEY_END) {
141-
seen = r.rejectDuplicateRootKey(seen, key);
142-
switch (key) {
143-
case KEY_STATUS -> out.status = r.readInt();
144-
case KEY_HEADERS -> {
145-
if (r.isObjectStart()) {
146-
r.beginObject();
147-
String k;
148-
while ((k = r.nextKeyCanonical()) != null) {
149-
if (out.headers == null) {
150-
// Pre-size for a typical response header
151-
// count (content-type, content-length, …).
152-
out.headers = new LinkedHashMap<>(8);
153-
}
154-
if (r.isArrayStart()) {
155-
r.beginArray();
156-
List<String> list = new ArrayList<>();
157-
while (r.hasNextElement()) {
158-
list.add(r.readString());
159-
}
160-
out.headers.put(k, list);
161-
} else {
162-
out.headers.put(k, r.readString());
135+
r.requireObjectStart();
136+
r.beginObject();
137+
int seen = 0;
138+
int key;
139+
while ((key = r.nextRootKey()) != KEY_END) {
140+
seen = r.rejectDuplicateRootKey(seen, key);
141+
switch (key) {
142+
case KEY_STATUS -> out.status = r.readInt();
143+
case KEY_HEADERS -> {
144+
if (r.isObjectStart()) {
145+
r.beginObject();
146+
String k;
147+
while ((k = r.nextKeyCanonical()) != null) {
148+
if (out.headers == null) {
149+
// Pre-size for a typical response header
150+
// count (content-type, content-length, …).
151+
out.headers = new LinkedHashMap<>(8);
152+
}
153+
if (r.isArrayStart()) {
154+
r.beginArray();
155+
List<String> list = new ArrayList<>();
156+
while (r.hasNextElement()) {
157+
list.add(r.readString());
163158
}
159+
out.headers.put(k, list);
160+
} else {
161+
out.headers.put(k, r.readString());
164162
}
165-
} else {
166-
r.skipValue();
167163
}
164+
} else {
165+
r.skipValue();
168166
}
169-
case KEY_METADATA -> {
170-
if (r.isObjectStart()) {
171-
r.beginObject();
172-
out.metadata = r.readStringMap();
173-
} else {
174-
r.skipValue();
175-
}
167+
}
168+
case KEY_METADATA -> {
169+
if (r.isObjectStart()) {
170+
r.beginObject();
171+
out.metadata = r.readStringMap();
172+
} else {
173+
r.skipValue();
176174
}
177-
case KEY_VALIDATION -> {
178-
if (r.isArrayStart()) {
179-
r.beginArray();
180-
out.validationErrors = new ArrayList<>();
181-
while (r.hasNextElement()) {
182-
if (!r.isObjectStart()) {
183-
// Fixed schema is an array of objects; a
184-
// non-object element (only on malformed
185-
// input) is skipped so the cursor still
186-
// reaches the array end cleanly.
187-
r.skipValue();
188-
continue;
189-
}
190-
r.beginObject();
191-
Map<String, Object> entry = new LinkedHashMap<>(4);
192-
String k;
193-
while ((k = r.nextKeyCanonical()) != null) {
194-
entry.put(k, r.readPrimitiveValue());
195-
}
196-
out.validationErrors.add(entry);
175+
}
176+
case KEY_VALIDATION -> {
177+
if (r.isArrayStart()) {
178+
r.beginArray();
179+
out.validationErrors = new ArrayList<>();
180+
while (r.hasNextElement()) {
181+
if (!r.isObjectStart()) {
182+
// Fixed schema is an array of objects; a
183+
// non-object element (only on malformed
184+
// input) is skipped so the cursor still
185+
// reaches the array end cleanly.
186+
r.skipValue();
187+
continue;
188+
}
189+
r.beginObject();
190+
Map<String, Object> entry = new LinkedHashMap<>(4);
191+
String k;
192+
while ((k = r.nextKeyCanonical()) != null) {
193+
entry.put(k, r.readPrimitiveValue());
197194
}
198-
} else {
199-
r.skipValue();
195+
out.validationErrors.add(entry);
200196
}
197+
} else {
198+
r.skipValue();
201199
}
202-
// KEY_OTHER: "v" and any unknown field — value skipped,
203-
// never materialised.
204-
default -> r.skipValue();
205200
}
201+
// KEY_OTHER: "v" and any unknown field — value skipped,
202+
// never materialised.
203+
default -> r.skipValue();
206204
}
207205
}
208206
return out;
@@ -261,6 +259,12 @@ private IllegalArgumentException err(String what) {
261259
return new IllegalArgumentException("wire header JSON: " + what + " at offset " + pos);
262260
}
263261

262+
private void requireObjectStart() {
263+
if (peek() != '{') {
264+
throw err("expected object");
265+
}
266+
}
267+
264268
private int rejectDuplicateRootKey(int seen, int key) {
265269
if (key < 0) {
266270
return seen;
@@ -310,8 +314,8 @@ String nextKey() {
310314
* construction (HTTP field names + the fixed metadata / validation keys).
311315
*/
312316
/**
313-
* If the upcoming quoted member key is a plain-ASCII {@link #CANONICAL_KEYS}
314-
* entry, consume it (key + closing quote) and return the shared instance;
317+
* If the upcoming quoted member key is a plain-ASCII canonical-key entry,
318+
* consume it (key + closing quote) and return the shared instance;
315319
* otherwise leave {@code pos} untouched and return {@code null} so the
316320
* caller falls back to {@link #readString()} — escaped / non-ASCII /
317321
* unknown keys still allocate exactly as before.

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/WireHeaderStringSupport.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ final class WireHeaderStringSupport {
1111
private static final ThreadLocal<byte[]> DIRECT_STRING_SCRATCH =
1212
ThreadLocal.withInitial(() -> new byte[DIRECT_STRING_SCRATCH_INITIAL]);
1313

14-
private static final String[] CANONICAL_KEYS = {
15-
"content-type", "content-length", "content-encoding",
16-
"content-disposition", "cache-control", "set-cookie", "location",
17-
"etag", "date", "vary", "access-control-allow-origin",
18-
"version", "path", "code", "message",
19-
};
20-
2114
private WireHeaderStringSupport() {}
2215

2316
static void clearCurrentThreadBuffers() {

libs/vespera-bridge/src/test/java/com/devfive/vespera/bridge/PerfAllocBench.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,56 @@ void directPoolRetention_reallocations() {
409409
afterReallocs, afterRehandlers);
410410
}
411411

412+
/** Regression model for retaining steady medium responses below the cap. */
413+
@Test
414+
void directPoolMediumRetention_reallocations() {
415+
final int initial = 64 * 1024;
416+
final int retainCap = 2 * 1024 * 1024;
417+
final int respSize = 1024 * 1024;
418+
final int dispatches = 50;
419+
420+
int beforeReallocs = 0;
421+
int beforeRehandlers = 0;
422+
int beforeCap = initial;
423+
int beforeIdle = 0;
424+
for (int i = 0; i < dispatches; i++) {
425+
if (beforeCap < respSize) {
426+
beforeCap = respSize;
427+
beforeReallocs++;
428+
beforeRehandlers++;
429+
}
430+
beforeIdle = respSize <= retainCap ? beforeIdle + 1 : 0;
431+
if (beforeIdle >= 8 && beforeCap > initial) {
432+
beforeCap = initial;
433+
beforeIdle = 0;
434+
}
435+
}
436+
437+
int afterReallocs = 0;
438+
int afterRehandlers = 0;
439+
int afterCap = initial;
440+
int afterIdle = 0;
441+
for (int i = 0; i < dispatches; i++) {
442+
if (afterCap < respSize) {
443+
afterCap = respSize;
444+
afterReallocs++;
445+
afterRehandlers++;
446+
}
447+
afterIdle = respSize <= retainCap ? afterIdle + 1 : 0;
448+
if (afterIdle >= 8 && afterCap > retainCap) {
449+
afterCap = initial;
450+
afterIdle = 0;
451+
}
452+
}
453+
454+
System.out.printf(
455+
"VESPERA_ALLOC direct_pool_medium_reallocs_before count=%d handler_reruns=%d (%d dispatches, %d KiB each)%n",
456+
beforeReallocs, beforeRehandlers, dispatches, respSize / 1024);
457+
System.out.printf(
458+
"VESPERA_ALLOC direct_pool_medium_reallocs_after count=%d handler_reruns=%d retained_bytes=%d%n",
459+
afterReallocs, afterRehandlers, afterCap);
460+
}
461+
412462
private static MockHttpServletRequest realisticHeaderRequest() {
413463
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/x");
414464
req.addHeader("Host", "api.example.test");

0 commit comments

Comments
 (0)