Skip to content

Commit 6887ded

Browse files
committed
video listing optimization:
- indexes in migration - videos count sql optimization - video listings cache system
1 parent 5d9bda9 commit 6887ded

File tree

9 files changed

+344
-41
lines changed

9 files changed

+344
-41
lines changed

lib/Caching/VideosCaching.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Opencast\Caching;
4+
5+
use Opencast\Models\Videos;
6+
7+
class VideosCaching
8+
{
9+
private $cache_factory;
10+
private $cache_name;
11+
const OC_CACHE_KEY_DOMAIN_USERS = 'OpencastV3/videos/users/';
12+
const OC_CACHE_KEY_DOMAIN_COURSES = 'OpencastV3/videos/courses/';
13+
const OC_CACHE_KEY_DOMAIN_PLAYLIST = 'OpencastV3/videos/playlist/';
14+
15+
public function __construct() {
16+
$this->cache_factory = \StudipCacheFactory::getCache();
17+
}
18+
19+
public function userVideos(string $user_id)
20+
{
21+
$this->cache_name = self::OC_CACHE_KEY_DOMAIN_USERS . $user_id;
22+
return $this;
23+
}
24+
25+
public function courseVideos(string $course_id)
26+
{
27+
$this->cache_name = self::OC_CACHE_KEY_DOMAIN_COURSES . $course_id;
28+
return $this;
29+
}
30+
31+
public function playlistVideos(int $playlist_id)
32+
{
33+
$this->cache_name = self::OC_CACHE_KEY_DOMAIN_PLAYLIST . $playlist_id;
34+
return $this;
35+
}
36+
37+
public function readAll()
38+
{
39+
if (empty($this->cache_name)) {
40+
throw new \Error('Unable to read the cache due to missing cache name!');
41+
}
42+
43+
$content = $this->cache_factory->read($this->cache_name);
44+
return $content ? unserialize($content) : [];
45+
}
46+
47+
public function read(string $unique_query_id)
48+
{
49+
if (empty($unique_query_id)) {
50+
throw new \Error('Unable to read the cache due to missing cache name!');
51+
}
52+
53+
$all = $this->readAll();
54+
55+
if (!isset($all[$unique_query_id])) {
56+
return false;
57+
}
58+
59+
return $all[$unique_query_id];
60+
}
61+
62+
public function write($unique_query_id, $content)
63+
{
64+
if (empty($unique_query_id)) {
65+
throw new \Error('Unable to write the cache data due to missing cache name!');
66+
}
67+
68+
$all = $this->readAll();
69+
70+
$all[$unique_query_id] = $content;
71+
72+
$serialized_records = serialize($all);
73+
74+
return $this->cache_factory->write($this->cache_name, $serialized_records);
75+
}
76+
77+
public function delete($unique_query_id)
78+
{
79+
if (empty($unique_query_id)) {
80+
throw new \Error('Unable to expire the cache data due to missing cache name!');
81+
}
82+
83+
$all = $this->readAll();
84+
85+
if (empty($all)) {
86+
return true;
87+
}
88+
89+
if (isset($all[$unique_query_id])) {
90+
unset($all[$unique_query_id]);
91+
}
92+
93+
if (empty($all)) {
94+
$this->expire();
95+
return true;
96+
}
97+
98+
return $this->cache_factory->write($this->cache_name, serialize($all));
99+
}
100+
101+
public function expire()
102+
{
103+
$this->cache_factory->expire($this->cache_name);
104+
}
105+
106+
public static function expireAllVideoCaches(Videos $video)
107+
{
108+
$cache = \StudipCacheFactory::getCache();
109+
if (!empty($video->perms)) {
110+
foreach ($video->perms->pluck('user_id') as $user_id) {
111+
$cache->expire(self::OC_CACHE_KEY_DOMAIN_USERS . $user_id);
112+
}
113+
}
114+
115+
if (!empty($video->playlists)) {
116+
foreach ($video->playlists as $playlist) {
117+
$cache->expire(self::OC_CACHE_KEY_DOMAIN_PLAYLIST . $playlist->id);
118+
119+
if (!empty($playlist->courses)) {
120+
foreach ($playlist->courses as $course) {
121+
$cache->expire(self::OC_CACHE_KEY_DOMAIN_COURSES . $course->id);
122+
}
123+
}
124+
}
125+
}
126+
127+
// We need to also look for root accounts!
128+
$stmt = \DBManager::get()->prepare($q = "SELECT `user_id` FROM `auth_user_md5` WHERE `perms` = :perm");
129+
$stmt->execute([
130+
':perm' => 'root',
131+
]);
132+
$root_user_ids = $stmt->fetchAll(\PDO::FETCH_COLUMN);
133+
foreach ($root_user_ids as $root_user_id) {
134+
$cache->expire(self::OC_CACHE_KEY_DOMAIN_USERS . $root_user_id);
135+
}
136+
}
137+
}

lib/Models/Filter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,9 @@ public function getTrashed()
105105
{
106106
return $this->trashed;
107107
}
108+
109+
public function decodeVars()
110+
{
111+
return base64_encode(json_encode(get_object_vars($this)));
112+
}
108113
}

lib/Models/Playlists.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Opencast\Models\REST\ApiPlaylistsClient;
66
use Opencast\Helpers\PlaylistMigration;
77
use Opencast\Errors\Error;
8+
use Opencast\Caching\VideosCaching;
89

910
class Playlists extends UPMap
1011
{
@@ -575,6 +576,8 @@ public function delete()
575576
$playlist_client->deletePlaylist($this->service_playlist_id);
576577
}
577578

579+
(new VideosCaching())->playlistVideos($this->id)->expire();
580+
578581
return parent::delete();
579582
}
580583

lib/Models/ScheduledRecordings.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Opencast\Models;
44

55
use Opencast\Models\PlaylistSeminars;
6-
use Opencast\Models\Video;
6+
use Opencast\Models\Videos;
77

88
class ScheduledRecordings extends \SimpleORMap
99
{

lib/Models/Videos.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Opencast\Models\REST\ApiWorkflowsClient;
1111
use Opencast\Models\Helpers;
1212
use Opencast\Models\ScheduledRecordings;
13+
use Opencast\Caching\VideosCaching;
1314

1415
class Videos extends UPMap
1516
{
@@ -297,9 +298,14 @@ protected static function getFilteredVideos($query, $filters)
297298
$course_ids = [];
298299
$lecturer_ids = [];
299300

301+
$count_selected_columns = [
302+
'id', 'token', 'config_id', 'created'
303+
];
304+
300305
foreach ($filters->getFilters() as $filter) {
301306
switch ($filter['type']) {
302307
case 'text':
308+
$count_selected_columns[] = 'title';
303309
$pname = ':text' . sizeof($params);
304310
$where .= " AND (title LIKE $pname OR description LIKE $pname)";
305311
$params[$pname] = '%' . $filter['value'] .'%';
@@ -446,7 +452,11 @@ protected static function getFilteredVideos($query, $filters)
446452

447453
$sql .= ' GROUP BY oc_video.id';
448454

449-
$stmt = \DBManager::get()->prepare($s = "SELECT COUNT(*) FROM (SELECT oc_video.* FROM oc_video $sql) t");
455+
// Preparing count sql, specifically define columns to select in order to increase performance.
456+
$count_selected_columns = array_map(function($clmn) { return 'oc_video.' . $clmn; }, array_unique($count_selected_columns));
457+
$count_select_columns_sql = implode(', ', $count_selected_columns);
458+
$s = "SELECT COUNT(*) FROM (SELECT $count_select_columns_sql FROM oc_video $sql) t";
459+
$stmt = \DBManager::get()->prepare($s);
450460
$stmt->execute($params);
451461
$count = $stmt->fetchColumn();
452462

@@ -1245,4 +1255,26 @@ public static function addToCoursePlaylist($episode, $video)
12451255
}
12461256
}
12471257
}
1258+
1259+
/**
1260+
* @inheritDoc
1261+
*
1262+
* Overriding store method, in order to expire caches.
1263+
*/
1264+
public function store()
1265+
{
1266+
VideosCaching::expireAllVideoCaches($this);
1267+
return parent::store();
1268+
}
1269+
1270+
/**
1271+
* @inheritDoc
1272+
*
1273+
* Overriding delete method, in order to expire caches.
1274+
*/
1275+
public function delete()
1276+
{
1277+
VideosCaching::expireAllVideoCaches($this);
1278+
return parent::delete();
1279+
}
12481280
}

lib/Routes/Course/CourseVideoList.php

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Opencast\OpencastController;
99
use Opencast\Models\Filter;
1010
use Opencast\Models\Videos;
11+
use Opencast\Caching\VideosCaching;
1112

1213
class CourseVideoList extends OpencastController
1314
{
@@ -29,22 +30,38 @@ public function __invoke(Request $request, Response $response, $args)
2930
throw new \AccessDeniedException();
3031
}
3132

32-
// show videos for this course and filter them with optional additional filters
33-
$videos = Videos::getCourseVideos($course_id, new Filter($params));
33+
$response_result = [
34+
'videos' => [],
35+
'count' => 0,
36+
];
3437

35-
$ret = [];
36-
foreach ($videos['videos'] as $video) {
37-
$video_array = $video->toSanitizedArray($params['cid']);
38-
if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write'))
39-
{
40-
$video_array['perms'] = $video->perms->toSanitizedArray();
38+
$filter = new Filter($params);
39+
$video_caching = new VideosCaching();
40+
$course_videos_cache = $video_caching->courseVideos($course_id);
41+
$unique_query_id = $filter->decodeVars();
42+
$response_result = $course_videos_cache->read($unique_query_id);
43+
if (empty($response_result)) {
44+
// show videos for this course and filter them with optional additional filters
45+
$videos = Videos::getCourseVideos($course_id, $filter);
46+
47+
$ret = [];
48+
foreach ($videos['videos'] as $video) {
49+
$video_array = $video->toSanitizedArray($params['cid']);
50+
if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write'))
51+
{
52+
$video_array['perms'] = $video->perms->toSanitizedArray();
53+
}
54+
$ret[] = $video_array;
4155
}
42-
$ret[] = $video_array;
56+
57+
$response_result = [
58+
'videos' => $ret,
59+
'count' => $videos['count'],
60+
];
61+
62+
$course_videos_cache->write($unique_query_id, $response_result);
4363
}
4464

45-
return $this->createResponse([
46-
'videos' => $ret,
47-
'count' => $videos['count'],
48-
], $response);
65+
return $this->createResponse($response_result, $response);
4966
}
5067
}

lib/Routes/Playlist/PlaylistVideoList.php

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Opencast\Models\Playlists;
1313
use Opencast\Models\Videos;
1414
use Opencast\Models\PlaylistSeminarVideos;
15+
use Opencast\Caching\VideosCaching;
1516

1617
class PlaylistVideoList extends OpencastController
1718
{
@@ -49,22 +50,38 @@ public function __invoke(Request $request, Response $response, $args)
4950
}
5051
}
5152

52-
// show videos for this playlist and filter them with optional additional filters
53-
$videos = Videos::getPlaylistVideos($playlist->id, new Filter($params));
53+
$response_result = [
54+
'videos' => [],
55+
'count' => 0,
56+
];
5457

55-
$ret = [];
56-
foreach ($videos['videos'] as $video) {
57-
$video_array = $video->toSanitizedArray($course_id, $playlist->id);
58-
if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write'))
59-
{
60-
$video_array['perms'] = $video->perms->toSanitizedArray();
58+
$filter = new Filter($params);
59+
$video_caching = new VideosCaching();
60+
$playlist_videos_cache = $video_caching->playlistVideos($playlist->id);
61+
$unique_query_id = $filter->decodeVars();
62+
$response_result = $playlist_videos_cache->read($unique_query_id);
63+
if (empty($response_result)) {
64+
// show videos for this playlist and filter them with optional additional filters
65+
$videos = Videos::getPlaylistVideos($playlist->id, $filter);
66+
67+
$ret = [];
68+
foreach ($videos['videos'] as $video) {
69+
$video_array = $video->toSanitizedArray($course_id, $playlist->id);
70+
if (!empty($video_array['perm']) && ($video_array['perm'] == 'owner' || $video_array['perm'] == 'write'))
71+
{
72+
$video_array['perms'] = $video->perms->toSanitizedArray();
73+
}
74+
$ret[] = $video_array;
6175
}
62-
$ret[] = $video_array;
76+
77+
$response_result = [
78+
'videos' => $ret,
79+
'count' => $videos['count'],
80+
];
81+
82+
$playlist_videos_cache->write($unique_query_id, $response_result);
6383
}
6484

65-
return $this->createResponse([
66-
'videos' => $ret,
67-
'count' => $videos['count']
68-
], $response);
85+
return $this->createResponse($response_result, $response);
6986
}
7087
}

0 commit comments

Comments
 (0)