the browser-facing portion of osu!
at master 4.2 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Console\Commands; 7 8use App\Models\Forum\Post; 9use App\Models\Forum\Topic; 10use App\Models\User; 11use App\Models\UsernameChangeHistory; 12use Carbon\Carbon; 13use Exception; 14use Illuminate\Console\Command; 15 16class FixUsernameChangeTopicCache extends Command 17{ 18 /** 19 * The name and signature of the console command. 20 * 21 * @var string 22 */ 23 protected $signature = 'fix:username-change-topic-cache'; 24 25 /** 26 * The console command description. 27 * 28 * @var string 29 */ 30 protected $description = 'Refresh Topic cache from username changes changing wrong field'; 31 32 /** 33 * Execute the console command. 34 * 35 * @return mixed 36 */ 37 public function handle() 38 { 39 $continue = $this->confirm('WARNING! This will refresh the cache for forum topics effected by username changes after 2017/08/09'); 40 41 if (!$continue) { 42 $this->error('User aborted!'); 43 return; 44 } 45 46 $start = time(); 47 48 $date = Carbon::parse('2017/08/09'); 49 $ids = UsernameChangeHistory::where('timestamp', '>', $date) 50 ->distinct() 51 ->pluck('user_id'); // pluck is faster than select for this. 52 53 $userCount = count($ids); 54 $this->warn("{$userCount} users effected"); 55 56 // where the user is the topic creator. 57 $this->info('Getting first poster counts...'); 58 $topicsFirstPoster = Topic::withTrashed()->whereIn('topic_poster', $ids); 59 $count = $topicsFirstPoster->count(); 60 $this->warn("Found {$count}"); 61 62 // topics where they posted in - possible last posts. 63 // select is faster than pluck for the the whereNotIn(). 64 $this->info('Getting possible last poster counts...'); 65 $topicIds = Post::withTrashed() 66 ->whereIn('poster_id', $ids) 67 ->whereNotIn('topic_id', (clone $topicsFirstPoster)->select('topic_id')) 68 ->distinct() 69 ->pluck('topic_id'); 70 71 $count += $topicIds->count(); 72 $this->warn("Total {$count}"); 73 74 $this->warn((time() - $start).'s to scan.'); 75 76 // reconfirm. 77 if (!$this->confirm("This will affect an estimated {$count} Topics.")) { 78 $this->error('User aborted!'); 79 return; 80 } 81 82 $start = time(); 83 $bar = $this->output->createProgressBar($count); 84 85 $this->chunkAndProcess($topicsFirstPoster->pluck('topic_id')->toArray(), $bar); 86 $this->warn("\n".(time() - $start).'s taken.'); 87 88 $this->chunkAndProcess($topicIds->toArray(), $bar); 89 $bar->finish(); 90 91 $this->warn("\n".(time() - $start).'s taken.'); 92 } 93 94 private function chunkAndProcess($array, $bar) 95 { 96 // This is a lot faster than Laravel's whereIn()->chunk for the number of records here. 97 // chunk() freezes every 1000 records as it queries and offsets. 98 $chunks = array_chunk($array, 1000); 99 foreach ($chunks as $chunk) { 100 $this->processChunk($chunk, $bar); 101 } 102 } 103 104 private function processChunk($chunk, $bar) 105 { 106 $topics = Topic::withTrashed()->whereIn('topic_id', $chunk)->get(); 107 foreach ($topics as $topic) { 108 try { 109 if ($topic->topic_poster) { 110 $user = User::withoutGlobalScopes()->select('username')->find($topic->topic_poster); 111 if ($user) { 112 $username = $user->username; 113 if ($topic->topic_first_poster_name !== $username) { 114 $topic->update(['topic_first_poster_name' => $username]); 115 } 116 } else { 117 $this->warn("topic_poster not found for Topic {$topic->topic_id}"); 118 } 119 } 120 } catch (Exception $e) { 121 $this->error("Exception caught, Topic {$topic->topic_id}"); 122 $this->error($e->getMessage()); 123 } 124 125 $bar->advance(); 126 } 127 } 128}