|
10 | 10 | use Windwalker\Cache\CachePool; |
11 | 11 | use Windwalker\Cache\TaggedCachePool; |
12 | 12 | use Windwalker\Cache\Exception\RuntimeException; |
| 13 | +use Windwalker\Cache\Serializer\PhpSerializer; |
13 | 14 | use Windwalker\Cache\Storage\ArrayStorage; |
14 | 15 |
|
15 | 16 | class TaggedCachePoolTest extends TestCase |
@@ -1080,6 +1081,136 @@ public function testDeleteItemAlsoRemovesEnvelope(): void |
1080 | 1081 | self::assertFalse($tagStorage->has($envKeyWithoutTags), 'Envelope should be deleted with item'); |
1081 | 1082 | } |
1082 | 1083 |
|
| 1084 | + // ----------------------------------------------------------------------- |
| 1085 | + // withGroup propagation |
| 1086 | + // ----------------------------------------------------------------------- |
| 1087 | + |
| 1088 | + /** |
| 1089 | + * Regression: withGroup() must propagate the group to the tagPool even when the tagPool |
| 1090 | + * is a plain CachePool (not a TaggedCachePool). |
| 1091 | + * |
| 1092 | + * Before the fix the condition was `$new->tagPool instanceof self`, which was always false |
| 1093 | + * for a plain CachePool tagPool, so the group was never applied to it. |
| 1094 | + * As a result every group-scoped pool shared the *same* ungrouped tagPool, meaning that |
| 1095 | + * invalidating a tag in one group would silently bust items in all other groups. |
| 1096 | + * |
| 1097 | + * @see TaggedCachePool::withGroup |
| 1098 | + */ |
| 1099 | + public function testWithGroupPropagatesGroupToPlainCachePoolTagPool(): void |
| 1100 | + { |
| 1101 | + // Passing an ArrayStorage lets TaggedCachePool wrap it in a CachePool with PhpSerializer |
| 1102 | + // (via applyTagPool). That internal tagPool is a plain CachePool – not a TaggedCachePool – |
| 1103 | + // which is the case that was broken before the fix. |
| 1104 | + $pool = new TaggedCachePool(new ArrayStorage(0.0), tagPool: new ArrayStorage(0.0)); |
| 1105 | + |
| 1106 | + $g1 = $pool->withGroup('g1'); |
| 1107 | + $g2 = $pool->withGroup('g2'); |
| 1108 | + |
| 1109 | + $calls = ['g1' => 0, 'g2' => 0]; |
| 1110 | + |
| 1111 | + // Populate both groups with tagged items. |
| 1112 | + $g1->fetch('item', function ($item) use (&$calls) { |
| 1113 | + $calls['g1']++; |
| 1114 | + $item->tags('users'); |
| 1115 | + |
| 1116 | + return 'G1_V1'; |
| 1117 | + }, 3600, 0.0, false); |
| 1118 | + |
| 1119 | + $g2->fetch('item', function ($item) use (&$calls) { |
| 1120 | + $calls['g2']++; |
| 1121 | + $item->tags('users'); |
| 1122 | + |
| 1123 | + return 'G2_V1'; |
| 1124 | + }, 3600, 0.0, false); |
| 1125 | + |
| 1126 | + self::assertSame(1, $calls['g1']); |
| 1127 | + self::assertSame(1, $calls['g2']); |
| 1128 | + |
| 1129 | + // Invalidate the 'users' tag *only* in group g1. |
| 1130 | + $g1->invalidateTags('users'); |
| 1131 | + |
| 1132 | + // g1 must recompute (its tag was invalidated). |
| 1133 | + $result = $g1->fetch('item', function ($item) use (&$calls) { |
| 1134 | + $calls['g1']++; |
| 1135 | + $item->tags('users'); |
| 1136 | + |
| 1137 | + return 'G1_V2'; |
| 1138 | + }, 3600, 0.0, false); |
| 1139 | + |
| 1140 | + self::assertSame('G1_V2', $result, 'g1 item must be recomputed after tag invalidation'); |
| 1141 | + self::assertSame(2, $calls['g1']); |
| 1142 | + |
| 1143 | + // g2 must NOT be affected — its tagPool is independently scoped. |
| 1144 | + $result = $g2->fetch('item', function ($item) use (&$calls) { |
| 1145 | + $calls['g2']++; |
| 1146 | + $item->tags('users'); |
| 1147 | + |
| 1148 | + return 'G2_V2'; |
| 1149 | + }, 3600, 0.0, false); |
| 1150 | + |
| 1151 | + self::assertSame( |
| 1152 | + 'G2_V1', |
| 1153 | + $result, |
| 1154 | + 'g2 item must NOT be busted when the tag is invalidated in g1 only' |
| 1155 | + ); |
| 1156 | + self::assertSame( |
| 1157 | + 1, |
| 1158 | + $calls['g2'], |
| 1159 | + 'g2 handler must not be called: group isolation requires a separate tagPool per group' |
| 1160 | + ); |
| 1161 | + } |
| 1162 | + |
| 1163 | + /** |
| 1164 | + * When the tagPool itself is a TaggedCachePool the group must still be propagated |
| 1165 | + * (the existing `instanceof self` path that was always correct). |
| 1166 | + * |
| 1167 | + * @see TaggedCachePool::withGroup |
| 1168 | + */ |
| 1169 | + public function testWithGroupPropagatesGroupWhenTagPoolIsTaggedCachePool(): void |
| 1170 | + { |
| 1171 | + // TaggedCachePool used as tagPool must use PhpSerializer so it can store |
| 1172 | + // the array tag-envelope produced by the outer TaggedCachePool. |
| 1173 | + $tagPool = new TaggedCachePool(new ArrayStorage(0.0), serializer: new PhpSerializer()); |
| 1174 | + $pool = new TaggedCachePool(new ArrayStorage(0.0), tagPool: $tagPool); |
| 1175 | + $g1 = $pool->withGroup('g1'); |
| 1176 | + $g2 = $pool->withGroup('g2'); |
| 1177 | + |
| 1178 | + $calls = ['g1' => 0, 'g2' => 0]; |
| 1179 | + |
| 1180 | + $g1->fetch('key', function ($item) use (&$calls) { |
| 1181 | + $calls['g1']++; |
| 1182 | + $item->tags('t'); |
| 1183 | + |
| 1184 | + return 'G1_V1'; |
| 1185 | + }, 3600, 0.0, false); |
| 1186 | + |
| 1187 | + $g2->fetch('key', function ($item) use (&$calls) { |
| 1188 | + $calls['g2']++; |
| 1189 | + $item->tags('t'); |
| 1190 | + |
| 1191 | + return 'G2_V1'; |
| 1192 | + }, 3600, 0.0, false); |
| 1193 | + |
| 1194 | + $g1->invalidateTags('t'); |
| 1195 | + |
| 1196 | + $g1->fetch('key', function ($item) use (&$calls) { |
| 1197 | + $calls['g1']++; |
| 1198 | + $item->tags('t'); |
| 1199 | + |
| 1200 | + return 'G1_V2'; |
| 1201 | + }, 3600, 0.0, false); |
| 1202 | + |
| 1203 | + $g2->fetch('key', function ($item) use (&$calls) { |
| 1204 | + $calls['g2']++; |
| 1205 | + $item->tags('t'); |
| 1206 | + |
| 1207 | + return 'G2_V2'; |
| 1208 | + }, 3600, 0.0, false); |
| 1209 | + |
| 1210 | + self::assertSame(2, $calls['g1'], 'g1 must recompute after tag invalidation'); |
| 1211 | + self::assertSame(1, $calls['g2'], 'g2 must not be affected by g1 tag invalidation'); |
| 1212 | + } |
| 1213 | + |
1083 | 1214 | /** @see CachePool::getMultiple — respects missing envelopes */ |
1084 | 1215 | public function testGetMultipleRespectsEnvelopeLogic(): void |
1085 | 1216 | { |
|
0 commit comments