CompileStatsCommand.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. /*
  3. * This file is part of Packagist.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. * Nils Adermann <naderman@naderman.de>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Packagist\WebBundle\Command;
  12. use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
  13. use Symfony\Component\Console\Input\InputInterface;
  14. use Symfony\Component\Console\Input\InputOption;
  15. use Symfony\Component\Console\Output\OutputInterface;
  16. /**
  17. * @author Jordi Boggiano <j.boggiano@seld.be>
  18. */
  19. class CompileStatsCommand extends ContainerAwareCommand
  20. {
  21. protected $redis;
  22. /**
  23. * {@inheritdoc}
  24. */
  25. protected function configure()
  26. {
  27. $this
  28. ->setName('packagist:stats:compile')
  29. ->setDefinition(array(
  30. new InputOption('force', null, InputOption::VALUE_NONE, 'Force a re-build of all stats'),
  31. ))
  32. ->setDescription('Updates the redis stats indices')
  33. ;
  34. }
  35. /**
  36. * {@inheritdoc}
  37. */
  38. protected function execute(InputInterface $input, OutputInterface $output)
  39. {
  40. $verbose = $input->getOption('verbose');
  41. $force = $input->getOption('force');
  42. $doctrine = $this->getContainer()->get('doctrine');
  43. $this->redis = $redis = $this->getContainer()->get('snc_redis.default');
  44. $minMax = $doctrine->getManager()->getConnection()->fetchAssoc('SELECT MAX(id) maxId, MIN(id) minId FROM package');
  45. if (!isset($minMax['minId'])) {
  46. return 0;
  47. }
  48. $ids = range($minMax['minId'], $minMax['maxId']);
  49. $res = $doctrine->getManager()->getConnection()->fetchAssoc('SELECT MIN(createdAt) minDate FROM package');
  50. $date = new \DateTime($res['minDate']);
  51. $date->modify('00:00:00');
  52. $yesterday = new \DateTime('yesterday 00:00:00');
  53. if ($force) {
  54. if ($verbose) {
  55. $output->writeln('Clearing aggregated DB');
  56. }
  57. $clearDate = clone $date;
  58. $keys = array();
  59. while ($clearDate <= $yesterday) {
  60. $keys['downloads:'.$clearDate->format('Ymd')] = true;
  61. $keys['downloads:'.$clearDate->format('Ym')] = true;
  62. $clearDate->modify('+1day');
  63. }
  64. $redis->del(array_keys($keys));
  65. }
  66. while ($date <= $yesterday) {
  67. // skip months already computed
  68. if (null !== $this->getMonthly($date) && $date->format('m') !== $yesterday->format('m')) {
  69. $date->setDate($date->format('Y'), $date->format('m')+1, 1);
  70. continue;
  71. }
  72. // skip days already computed
  73. if (null !== $this->getDaily($date) && $date != $yesterday) {
  74. $date->modify('+1day');
  75. continue;
  76. }
  77. $sum = $this->sum($date->format('Ymd'), $ids);
  78. $redis->set('downloads:'.$date->format('Ymd'), $sum);
  79. if ($verbose) {
  80. $output->writeln('Wrote daily data for '.$date->format('Y-m-d').': '.$sum);
  81. }
  82. $nextDay = clone $date;
  83. $nextDay->modify('+1day');
  84. // update the monthly total if we just computed the last day of the month or the last known day
  85. if ($date->format('Ymd') === $yesterday->format('Ymd') || $date->format('Ym') !== $nextDay->format('Ym')) {
  86. $sum = $this->sum($date->format('Ym'), $ids);
  87. $redis->set('downloads:'.$date->format('Ym'), $sum);
  88. if ($verbose) {
  89. $output->writeln('Wrote monthly data for '.$date->format('Y-m').': '.$sum);
  90. }
  91. }
  92. $date = $nextDay;
  93. }
  94. // fetch existing ids
  95. $doctrine = $this->getContainer()->get('doctrine');
  96. $packages = $doctrine->getManager()->getConnection()->fetchAll('SELECT id FROM package ORDER BY id ASC');
  97. $ids = array();
  98. foreach ($packages as $row) {
  99. $ids[] = $row['id'];
  100. }
  101. if ($verbose) {
  102. $output->writeln('Writing new trendiness data into redis');
  103. }
  104. while ($id = array_shift($ids)) {
  105. $trendiness = $this->sumLastNDays(7, $id, $yesterday);
  106. $redis->zadd('downloads:trending:new', $trendiness, $id);
  107. $redis->zadd('downloads:absolute:new', $redis->get('dl:'.$id), $id);
  108. }
  109. $redis->rename('downloads:trending:new', 'downloads:trending');
  110. $redis->rename('downloads:absolute:new', 'downloads:absolute');
  111. }
  112. // TODO could probably run faster with lua scripting
  113. protected function sumLastNDays($days, $id, \DateTime $yesterday)
  114. {
  115. $date = clone $yesterday;
  116. $keys = array();
  117. for ($i = 0; $i < $days; $i++) {
  118. $keys[] = 'dl:'.$id.':'.$date->format('Ymd');
  119. $date->modify('-1day');
  120. }
  121. return array_sum($this->redis->mget($keys));
  122. }
  123. protected function sum($date, array $ids)
  124. {
  125. $sum = 0;
  126. while ($ids) {
  127. $batch = array_splice($ids, 0, 500);
  128. $keys = array();
  129. foreach ($batch as $id) {
  130. $keys[] = 'dl:'.$id.':'.$date;
  131. }
  132. $sum += array_sum($res = $this->redis->mget($keys));
  133. }
  134. return $sum;
  135. }
  136. protected function getMonthly(\DateTime $date)
  137. {
  138. return $this->redis->get('downloads:'.$date->format('Ym'));
  139. }
  140. protected function getDaily(\DateTime $date)
  141. {
  142. return $this->redis->get('downloads:'.$date->format('Ymd'));
  143. }
  144. }