@@ -908,6 +908,229 @@ public function test_application_manual_locking(): void {
908908 $ this ->assertTrue ($ cache2 ->set ('testkey ' , 'test data ' ));
909909 }
910910
911+ /**
912+ * Tests that when you get an item from a 2-layer cache, where it is present in the shared but
913+ * not local cache, the system correctly locks the cache before writing it to local cache if
914+ * required.
915+ */
916+ public function test_automatic_locking_multiple_layers_get (): void {
917+ $ instance = cache_config_testing::instance (true );
918+ $ instance ->phpunit_add_definition ('phpunit/test_application_locking ' , [
919+ 'mode ' => store::MODE_APPLICATION ,
920+ 'component ' => 'phpunit ' ,
921+ 'area ' => 'test_application_locking ' ,
922+ 'staticacceleration ' => true ,
923+ 'staticaccelerationsize ' => 1 ,
924+ 'requirelockingbeforewrite ' => true ,
925+ ], false );
926+ $ instance ->phpunit_add_file_store ('phpunittest1 ' );
927+ $ instance ->phpunit_add_file_store ('phpunittest2 ' );
928+ $ instance ->phpunit_add_definition_mapping ('phpunit/test_application_locking ' , 'phpunittest1 ' , 1 );
929+ $ instance ->phpunit_add_definition_mapping ('phpunit/test_application_locking ' , 'phpunittest2 ' , 2 );
930+
931+ $ cache = cache::make ('phpunit ' , 'test_application_locking ' );
932+
933+ // We need to get the individual stores so as to set up the right behaviour here.
934+ $ ref = new \ReflectionClass ('\cache ' );
935+ $ definitionprop = $ ref ->getProperty ('definition ' );
936+ $ storeprop = $ ref ->getProperty ('store ' );
937+ $ loaderprop = $ ref ->getProperty ('loader ' );
938+
939+ $ definition = $ definitionprop ->getValue ($ cache );
940+ $ localstore = $ storeprop ->getValue ($ cache );
941+ $ sharedcache = $ loaderprop ->getValue ($ cache );
942+ $ sharedstore = $ storeprop ->getValue ($ sharedcache );
943+
944+ // Set the lock waiting time to 1 second so it doesn't take forever to run the 'fail' test.
945+ $ ref = new \ReflectionClass ('\cachestore_file ' );
946+ $ lockwaitprop = $ ref ->getProperty ('lockwait ' );
947+ $ lockwaitprop ->setValue ($ localstore , 1 );
948+
949+ // The shared store contains the value, but local store does not.
950+ $ key = 'frog ' ;
951+ $ hashedkey = helper::hash_key ($ key , $ definition );
952+ $ sharedstore ->set ($ hashedkey , 'kermit ' );
953+
954+ // A 'get' returns value from shared store...
955+ $ this ->assertEquals ('kermit ' , $ cache ->get ($ key ));
956+
957+ // ...and value is set to local store now.
958+ $ this ->assertEquals ('kermit ' , $ localstore ->get ($ hashedkey ));
959+
960+ // Continue testing with a different key.
961+ $ key = 'toad ' ;
962+ $ hashedkey = helper::hash_key ($ key , $ definition );
963+ $ sharedstore ->set ($ hashedkey , 'mr ' );
964+
965+ // The same scenario also works if we already hold a lock on the value.
966+ $ cache ->acquire_lock ($ key );
967+ try {
968+ $ this ->assertEquals ('mr ' , $ cache ->get ($ key ));
969+ } finally {
970+ $ cache ->release_lock ($ key );
971+ }
972+ $ this ->assertEquals ('mr ' , $ localstore ->get ($ hashedkey ));
973+
974+ // Continue testing with a different key.
975+ $ key = 'squirrel ' ;
976+ $ hashedkey = helper::hash_key ($ key , $ definition );
977+ $ sharedstore ->set ($ hashedkey , 'nutkin ' );
978+
979+ // If somebody else holds a lock on the value, on the shared not local store, this is OK.
980+ $ sharedstore ->acquire_lock ($ hashedkey , 'somebodyelse ' );
981+ try {
982+ $ this ->assertEquals ('nutkin ' , $ cache ->get ($ key ));
983+ } finally {
984+ $ sharedstore ->release_lock ($ hashedkey , 'somebodyelse ' );
985+ }
986+ $ this ->assertEquals ('nutkin ' , $ localstore ->get ($ hashedkey ));
987+
988+ // Continue testing with a different key.
989+ $ key = 'rabbit ' ;
990+ $ hashedkey = helper::hash_key ($ key , $ definition );
991+ $ sharedstore ->set ($ hashedkey , 'peter ' );
992+
993+ // If somebody else holds a lock on the value, on the local store, it should fail.
994+ $ localstore ->acquire_lock ($ hashedkey , 'somebodyelse ' );
995+ try {
996+ $ cache ->get ($ key );
997+ $ this ->fail ();
998+ } catch (\moodle_exception $ e ) {
999+ $ this ->assertStringContainsString ('Unable to acquire a lock ' , $ e ->getMessage ());
1000+ } finally {
1001+ $ localstore ->release_lock ($ hashedkey , 'somebodyelse ' );
1002+ }
1003+ $ this ->assertEquals (false , $ localstore ->get ($ hashedkey ));
1004+ }
1005+
1006+ /**
1007+ * Tests that when you get_many items from a 2-layer cache, where it is present in the shared but
1008+ * not local cache, the system correctly locks the cache before writing it to local cache if
1009+ * required.
1010+ */
1011+ public function test_automatic_locking_multiple_layers_get_many (): void {
1012+ $ instance = cache_config_testing::instance (true );
1013+ $ instance ->phpunit_add_definition ('phpunit/test_application_locking ' , [
1014+ 'mode ' => store::MODE_APPLICATION ,
1015+ 'component ' => 'phpunit ' ,
1016+ 'area ' => 'test_application_locking ' ,
1017+ 'staticacceleration ' => true ,
1018+ 'staticaccelerationsize ' => 1 ,
1019+ 'requirelockingbeforewrite ' => true ,
1020+ ], false );
1021+ $ instance ->phpunit_add_file_store ('phpunittest1 ' );
1022+ $ instance ->phpunit_add_file_store ('phpunittest2 ' );
1023+ $ instance ->phpunit_add_definition_mapping ('phpunit/test_application_locking ' , 'phpunittest1 ' , 1 );
1024+ $ instance ->phpunit_add_definition_mapping ('phpunit/test_application_locking ' , 'phpunittest2 ' , 2 );
1025+
1026+ $ cache = cache::make ('phpunit ' , 'test_application_locking ' );
1027+
1028+ // We need to get the individual stores so as to set up the right behaviour here.
1029+ $ ref = new \ReflectionClass ('\cache ' );
1030+ $ definitionprop = $ ref ->getProperty ('definition ' );
1031+ $ storeprop = $ ref ->getProperty ('store ' );
1032+ $ loaderprop = $ ref ->getProperty ('loader ' );
1033+
1034+ $ definition = $ definitionprop ->getValue ($ cache );
1035+ $ localstore = $ storeprop ->getValue ($ cache );
1036+ $ sharedcache = $ loaderprop ->getValue ($ cache );
1037+ $ sharedstore = $ storeprop ->getValue ($ sharedcache );
1038+
1039+ // Set the lock waiting time to 1 second so it doesn't take forever to run the 'fail' test.
1040+ $ ref = new \ReflectionClass ('\cachestore_file ' );
1041+ $ lockwaitprop = $ ref ->getProperty ('lockwait ' );
1042+ $ lockwaitprop ->setValue ($ localstore , 1 );
1043+
1044+ // Initialises a set of keys for testing.
1045+ // Param: int $index Index used as a suffix on key names.
1046+ // Returns: Array of hashed keys.
1047+ $ initkeys = function (int $ index ) use ($ definition , $ sharedstore ): array {
1048+ $ keys = ['a ' . $ index , 'b ' . $ index , 'c ' . $ index ];
1049+ $ hashedkeys = [];
1050+ foreach ($ keys as $ key ) {
1051+ $ hashedkeys [$ key ] = helper::hash_key ($ key , $ definition );
1052+ }
1053+ $ sharedstore ->set ($ hashedkeys ['a ' . $ index ], 'a ' );
1054+ $ sharedstore ->set ($ hashedkeys ['c ' . $ index ], 'c ' );
1055+ return $ hashedkeys ;
1056+ };
1057+ $ hashedkeys = $ initkeys (1 );
1058+
1059+ // A 'get_many' returns value from shared store...
1060+ $ this ->assertEquals (['a1 ' => 'a ' , 'b1 ' => false , 'c1 ' => 'c ' ],
1061+ $ cache ->get_many (['a1 ' , 'b1 ' , 'c1 ' ]));
1062+
1063+ // ...and values are set to local store now.
1064+ $ this ->assertEquals ('a ' , $ localstore ->get ($ hashedkeys ['a1 ' ]));
1065+ $ this ->assertEquals ('c ' , $ localstore ->get ($ hashedkeys ['c1 ' ]));
1066+
1067+ // Continue testing with different keys.
1068+ $ hashedkeys = $ initkeys (2 );
1069+
1070+ // The same scenario also works if we already hold a lock on all values.
1071+ $ cache ->acquire_lock ('a2 ' );
1072+ $ cache ->acquire_lock ('b2 ' );
1073+ $ cache ->acquire_lock ('c2 ' );
1074+ try {
1075+ $ this ->assertEquals (['a2 ' => 'a ' , 'b2 ' => false , 'c2 ' => 'c ' ],
1076+ $ cache ->get_many (['a2 ' , 'b2 ' , 'c2 ' ]));
1077+ } finally {
1078+ $ cache ->release_lock ('c2 ' );
1079+ $ cache ->release_lock ('b2 ' );
1080+ $ cache ->release_lock ('a2 ' );
1081+ }
1082+ $ this ->assertEquals ('a ' , $ localstore ->get ($ hashedkeys ['a2 ' ]));
1083+ $ this ->assertEquals ('c ' , $ localstore ->get ($ hashedkeys ['c2 ' ]));
1084+
1085+ // Continue testing with different keys.
1086+ $ hashedkeys = $ initkeys (3 );
1087+
1088+ // If somebody else holds a lock on the values, on the shared not local store, this is OK.
1089+ $ sharedstore ->acquire_lock ($ hashedkeys ['a3 ' ], 'somebodyelse ' );
1090+ $ sharedstore ->acquire_lock ($ hashedkeys ['b3 ' ], 'somebodyelse ' );
1091+ $ sharedstore ->acquire_lock ($ hashedkeys ['c3 ' ], 'somebodyelse ' );
1092+ try {
1093+ $ this ->assertEquals (['a3 ' => 'a ' , 'b3 ' => false , 'c3 ' => 'c ' ],
1094+ $ cache ->get_many (['a3 ' , 'b3 ' , 'c3 ' ]));
1095+ } finally {
1096+ $ sharedstore ->release_lock ($ hashedkeys ['c3 ' ], 'somebodyelse ' );
1097+ $ sharedstore ->release_lock ($ hashedkeys ['b3 ' ], 'somebodyelse ' );
1098+ $ sharedstore ->release_lock ($ hashedkeys ['a3 ' ], 'somebodyelse ' );
1099+ }
1100+ $ this ->assertEquals ('a ' , $ localstore ->get ($ hashedkeys ['a3 ' ]));
1101+ $ this ->assertEquals ('c ' , $ localstore ->get ($ hashedkeys ['c3 ' ]));
1102+
1103+ // Continue testing with different keys.
1104+ $ hashedkeys = $ initkeys (4 );
1105+
1106+ // If somebody else holds a lock on the C value, on the local store, it will fail.
1107+ $ localstore ->acquire_lock ($ hashedkeys ['c4 ' ], 'somebodyelse ' );
1108+ try {
1109+ $ cache ->get_many (['a4 ' , 'b4 ' , 'c4 ' ]);
1110+ $ this ->fail ();
1111+ } catch (\moodle_exception $ e ) {
1112+ $ this ->assertStringContainsString ('Unable to acquire a lock ' , $ e ->getMessage ());
1113+ } finally {
1114+ $ localstore ->release_lock ($ hashedkeys ['c4 ' ], 'somebodyelse ' );
1115+ }
1116+ $ this ->assertEquals ('a ' , $ localstore ->get ($ hashedkeys ['a4 ' ]));
1117+ $ this ->assertEquals (false , $ localstore ->get ($ hashedkeys ['c4 ' ]));
1118+
1119+ // Continue testing with different keys.
1120+ $ hashedkeys = $ initkeys (5 );
1121+
1122+ // If somebody else holds a lock on the B value, on the local store, it doesn't care.
1123+ $ localstore ->acquire_lock ($ hashedkeys ['b5 ' ], 'somebodyelse ' );
1124+ try {
1125+ $ this ->assertEquals (['a5 ' => 'a ' , 'b5 ' => false , 'c5 ' => 'c ' ],
1126+ $ cache ->get_many (['a5 ' , 'b5 ' , 'c5 ' ]));
1127+ } finally {
1128+ $ localstore ->release_lock ($ hashedkeys ['b5 ' ], 'somebodyelse ' );
1129+ }
1130+ $ this ->assertEquals ('a ' , $ localstore ->get ($ hashedkeys ['a5 ' ]));
1131+ $ this ->assertEquals ('c ' , $ localstore ->get ($ hashedkeys ['c5 ' ]));
1132+ }
1133+
9111134 /**
9121135 * Tests application cache event invalidation
9131136 */
0 commit comments