|
20 | 20 | import java.util.concurrent.ThreadFactory; |
21 | 21 | import java.util.function.Function; |
22 | 22 | import java.util.function.Supplier; |
| 23 | +import org.junit.jupiter.api.AfterAll; |
| 24 | +import org.junit.jupiter.api.BeforeAll; |
23 | 25 | import org.junit.jupiter.api.DisplayName; |
24 | 26 | import org.junit.jupiter.api.Test; |
25 | 27 | import org.junit.jupiter.params.ParameterizedTest; |
@@ -550,6 +552,129 @@ public void removalChange() { |
550 | 552 | assertTrue(removalChange.isRemoval()); |
551 | 553 | } |
552 | 554 |
|
| 555 | + // --------------------------------------------------------------------------------------------- |
| 556 | + // Tag-id-constructed entries: the name is resolved lazily from the tagId via KnownTags on first |
| 557 | + // tag()/getKey(). That resolution (and the cache write to the volatile-free `tag` field) is a |
| 558 | + // benign race — these tests run tag-id entries through the existing multi-threaded harness so 4 |
| 559 | + // threads resolve the name concurrently; all must agree, and on the same interned constant. |
| 560 | + // --------------------------------------------------------------------------------------------- |
| 561 | + |
| 562 | + static final String[] TAG_NAMES = {"tag.alpha", "tag.beta", "tag.gamma"}; |
| 563 | + |
| 564 | + static long tagId(int serial, int fieldPos, String name) { |
| 565 | + long nameHash = TagMap.Entry._hash(name) & 0xFFFFFFFFL; |
| 566 | + return ((long) serial << 48) | ((long) fieldPos << 32) | nameHash; |
| 567 | + } |
| 568 | + |
| 569 | + @BeforeAll |
| 570 | + static void registerResolver() { |
| 571 | + KnownTags.register( |
| 572 | + new KnownTags.Resolver() { |
| 573 | + @Override |
| 574 | + public String nameOf(long tagId) { |
| 575 | + int serial = (int) (tagId >>> 48); |
| 576 | + // returns the same interned constant each call, so racing resolutions agree by identity |
| 577 | + return (serial >= 1 && serial <= TAG_NAMES.length) ? TAG_NAMES[serial - 1] : null; |
| 578 | + } |
| 579 | + |
| 580 | + @Override |
| 581 | + public long keyOf(String name) { |
| 582 | + for (int i = 0; i < TAG_NAMES.length; ++i) { |
| 583 | + if (TAG_NAMES[i].equals(name)) return tagId(i + 1, i, TAG_NAMES[i]); |
| 584 | + } |
| 585 | + return 0L; |
| 586 | + } |
| 587 | + }); |
| 588 | + } |
| 589 | + |
| 590 | + @AfterAll |
| 591 | + static void clearResolver() { |
| 592 | + KnownTags.register(null); |
| 593 | + } |
| 594 | + |
| 595 | + // resolved name must be the exact interned constant, and hash() must equal the tagId's low 32 |
| 596 | + // bits (nameHash) — both stressed concurrently by the shared-entry multi-threaded harness. |
| 597 | + static Check checkResolvedTagId(long id, String name, TagMap.Entry entry) { |
| 598 | + return multiCheck( |
| 599 | + checkKey(name, entry), |
| 600 | + checkTrue(() -> entry.tag() == name, "tag() returns interned constant"), |
| 601 | + checkEquals((int) (id & 0xFFFFFFFFL), () -> entry.hash(), "Entry::hash == nameHash")); |
| 602 | + } |
| 603 | + |
| 604 | + @Test |
| 605 | + @DisplayName("tag-id entry: Object resolves name lazily under race") |
| 606 | + public void tagIdEntryObject() { |
| 607 | + long id = tagId(1, 0, TAG_NAMES[0]); |
| 608 | + test( |
| 609 | + () -> TagMap.Entry.newAnyEntry(id, "bar"), |
| 610 | + TagMap.Entry.ANY, |
| 611 | + (entry) -> |
| 612 | + multiCheck( |
| 613 | + checkResolvedTagId(id, TAG_NAMES[0], entry), |
| 614 | + checkValue("bar", entry), |
| 615 | + checkTrue(entry::isObject), |
| 616 | + checkType(TagMap.Entry.OBJECT, entry))); |
| 617 | + } |
| 618 | + |
| 619 | + @Test |
| 620 | + @DisplayName("tag-id entry: int resolves name lazily under race") |
| 621 | + public void tagIdEntryInt() { |
| 622 | + long id = tagId(2, 1, TAG_NAMES[1]); |
| 623 | + test( |
| 624 | + () -> TagMap.Entry.newIntEntry(id, 42), |
| 625 | + TagMap.Entry.INT, |
| 626 | + (entry) -> |
| 627 | + multiCheck( |
| 628 | + checkResolvedTagId(id, TAG_NAMES[1], entry), |
| 629 | + checkValue(42, entry), |
| 630 | + checkIsNumericPrimitive(entry), |
| 631 | + checkType(TagMap.Entry.INT, entry))); |
| 632 | + } |
| 633 | + |
| 634 | + @Test |
| 635 | + @DisplayName("tag-id entry: boolean resolves name lazily under race") |
| 636 | + public void tagIdEntryBoolean() { |
| 637 | + long id = tagId(3, 2, TAG_NAMES[2]); |
| 638 | + test( |
| 639 | + () -> TagMap.Entry.newBooleanEntry(id, true), |
| 640 | + TagMap.Entry.BOOLEAN, |
| 641 | + (entry) -> |
| 642 | + multiCheck( |
| 643 | + checkResolvedTagId(id, TAG_NAMES[2], entry), |
| 644 | + checkValue(true, entry), |
| 645 | + checkType(TagMap.Entry.BOOLEAN, entry))); |
| 646 | + } |
| 647 | + |
| 648 | + @Test |
| 649 | + @DisplayName("string entry: lazy hash() under race") |
| 650 | + public void stringEntryLazyHash() { |
| 651 | + // string-constructed entry computes hash() lazily, writing into the low 32 bits of the `tagId` |
| 652 | + // field (formerly a separate `int lazyTagHash`). Stress concurrent first-resolution. |
| 653 | + String name = "some.unknown.tag.name"; |
| 654 | + test( |
| 655 | + () -> TagMap.Entry.newObjectEntry(name, "v"), |
| 656 | + TagMap.Entry.OBJECT, |
| 657 | + (entry) -> |
| 658 | + multiCheck( |
| 659 | + checkEquals(TagMap.Entry._hash(name), () -> entry.hash(), "lazy hash()"), |
| 660 | + checkKey(name, entry), |
| 661 | + checkValue("v", entry))); |
| 662 | + } |
| 663 | + |
| 664 | + @Test |
| 665 | + @DisplayName("tag-id entry: matches() resolves the name under race") |
| 666 | + public void tagIdEntryMatches() { |
| 667 | + long id = tagId(1, 0, TAG_NAMES[0]); |
| 668 | + test( |
| 669 | + () -> TagMap.Entry.newObjectEntry(id, "bar"), |
| 670 | + TagMap.Entry.OBJECT, |
| 671 | + (entry) -> |
| 672 | + multiCheck( |
| 673 | + checkTrue(() -> entry.matches(TAG_NAMES[0]), "matches(name)"), |
| 674 | + checkFalse(() -> entry.matches("nope"), "!matches(other)"), |
| 675 | + checkKey(TAG_NAMES[0], entry))); |
| 676 | + } |
| 677 | + |
553 | 678 | static final int NUM_THREADS = 4; |
554 | 679 | static final ExecutorService EXECUTOR = |
555 | 680 | Executors.newFixedThreadPool( |
|
0 commit comments