wip
This commit is contained in:
		
							parent
							
								
									a8fad1b61c
								
							
						
					
					
						commit
						51873bbf16
					
				
					 1 changed files with 58 additions and 22 deletions
				
			
		| 
						 | 
				
			
			@ -4,8 +4,10 @@
 | 
			
		|||
 * Tests located in test/chart
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
import * as nestedProperty from 'nested-property';
 | 
			
		||||
import autobind from 'autobind-decorator';
 | 
			
		||||
import { redisClient } from '@/db/redis';
 | 
			
		||||
import Logger from '../logger';
 | 
			
		||||
import { SimpleSchema } from '@/misc/simple-schema';
 | 
			
		||||
import { EntitySchema, getRepository, Repository, LessThan, Between } from 'typeorm';
 | 
			
		||||
| 
						 | 
				
			
			@ -82,11 +84,6 @@ export default abstract class Chart<T extends Record<string, any>> {
 | 
			
		|||
					columns[this.columnPrefix + p] = {
 | 
			
		||||
						type: 'bigint',
 | 
			
		||||
					};
 | 
			
		||||
				} else if (v.type === 'array' && v.items.type === 'string') {
 | 
			
		||||
					columns[this.columnPrefix + p] = {
 | 
			
		||||
						type: 'varchar',
 | 
			
		||||
						array: true,
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +104,7 @@ export default abstract class Chart<T extends Record<string, any>> {
 | 
			
		|||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private static convertObjectToFlattenColumns(x: Record<string, unknown>) {
 | 
			
		||||
		const columns = {} as Record<string, number | unknown[]>;
 | 
			
		||||
		const columns = {} as Record<string, unknown>;
 | 
			
		||||
		const flatten = (x: Obj, path?: string) => {
 | 
			
		||||
			for (const [k, v] of Object.entries(x)) {
 | 
			
		||||
				const p = path ? `${path}${this.columnDot}${k}` : k;
 | 
			
		||||
| 
						 | 
				
			
			@ -129,8 +126,6 @@ export default abstract class Chart<T extends Record<string, any>> {
 | 
			
		|||
			for (const [k, v] of Object.entries(x)) {
 | 
			
		||||
				if (typeof v === 'object' && !Array.isArray(v)) {
 | 
			
		||||
					res[k] = exec(v);
 | 
			
		||||
				} else if (Array.isArray(v)) {
 | 
			
		||||
					res[k] = Array.from(new Set(v)).length;
 | 
			
		||||
				} else {
 | 
			
		||||
					res[k] = v;
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -141,19 +136,12 @@ export default abstract class Chart<T extends Record<string, any>> {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private static convertQuery(diff: Record<string, number | unknown[]>) {
 | 
			
		||||
	private static convertQuery(diff: Record<string, number>) {
 | 
			
		||||
		const query: Record<string, () => string> = {};
 | 
			
		||||
 | 
			
		||||
		for (const [k, v] of Object.entries(diff)) {
 | 
			
		||||
			if (typeof v === 'number') {
 | 
			
		||||
				if (v > 0) query[k] = () => `"${k}" + ${v}`;
 | 
			
		||||
				if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`;
 | 
			
		||||
			} else if (Array.isArray(v)) {
 | 
			
		||||
				// TODO: item が文字列以外の場合も対応
 | 
			
		||||
				// TODO: item をSQLエスケープ
 | 
			
		||||
				const items = v.map(item => `"${item}"`).join(',');
 | 
			
		||||
				query[k] = () => `array_cat("${k}", '{${items}}'::varchar[])`;
 | 
			
		||||
			}
 | 
			
		||||
			if (v > 0) query[k] = () => `"${k}" + ${v}`;
 | 
			
		||||
			if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return query;
 | 
			
		||||
| 
						 | 
				
			
			@ -368,13 +356,59 @@ export default abstract class Chart<T extends Record<string, any>> {
 | 
			
		|||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	protected commitUnique(diff: DeepPartial<T>, group: string | null = null): void {
 | 
			
		||||
		const [y, m, d, h] = Chart.getCurrentDate();
 | 
			
		||||
 | 
			
		||||
		const currentHour = dateUTC([y, m, d, h]);
 | 
			
		||||
		const currentDay = dateUTC([y, m, d]);
 | 
			
		||||
 | 
			
		||||
		const diffFlat = Chart.convertObjectToFlattenColumns(diff);
 | 
			
		||||
		for (const [k, v] of Object.entries(diffFlat)) {
 | 
			
		||||
			const keyHour = `chart_unique_buffer:${this.name}:${k}:${group || ''}:hour:${currentHour.getTime()}`;
 | 
			
		||||
			const keyDay = `chart_unique_buffer:${this.name}:${k}:${group || ''}:day:${currentDay.getTime()}`;
 | 
			
		||||
			redisClient.sadd(keyHour, v as string);
 | 
			
		||||
			redisClient.sadd(keyDay, v as string);
 | 
			
		||||
			redisClient.sadd(`chart_unique_buffer_index:${this.name}:hour:${currentHour.getTime()}`, keyHour);
 | 
			
		||||
			redisClient.sadd(`chart_unique_buffer_index:${this.name}:day:${currentDay.getTime()}`, keyDay);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async save(): Promise<void> {
 | 
			
		||||
		if (this.buffer.length === 0) {
 | 
			
		||||
			logger.info(`${this.name}: Write skipped`);
 | 
			
		||||
		// TODO: 現在のrangeのひとつ前のrangeもbake(claimPreviousLog?)
 | 
			
		||||
 | 
			
		||||
		const [y, m, d, h] = Chart.getCurrentDate();
 | 
			
		||||
 | 
			
		||||
		const currentHour = dateUTC([y, m, d, h]);
 | 
			
		||||
		const currentDay = dateUTC([y, m, d]);
 | 
			
		||||
 | 
			
		||||
		const smembers = promisify(redisClient.smembers).bind(redisClient);
 | 
			
		||||
		const uniqueBufferKeysHour = await smembers(`chart_unique_buffer_index:${this.name}:hour:${currentHour.getTime()}`);
 | 
			
		||||
		const uniqueBufferKeysDay = await smembers(`chart_unique_buffer_index:${this.name}:day:${currentDay.getTime()}`);
 | 
			
		||||
		if (this.buffer.length === 0 && uniqueBufferKeysHour.length === 0 && uniqueBufferKeysDay.length === 9) {
 | 
			
		||||
			logger.info(`${this.name}: No commits. Write skipped`);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const bakeUniqueBuffer = async (logHour: Log, logDay: Log): Promise<void> => {
 | 
			
		||||
			// この方法だと、100種類のハッシュタグが使われたらbakeごとに100のクエリがredisに送信されることになりそう
 | 
			
		||||
 | 
			
		||||
			// ログ更新
 | 
			
		||||
			await Promise.all([
 | 
			
		||||
				this.repositoryForHour.createQueryBuilder()
 | 
			
		||||
					.update()
 | 
			
		||||
					.set(query)
 | 
			
		||||
					.where('id = :id', { id: logHour.id })
 | 
			
		||||
					.execute(),
 | 
			
		||||
				this.repositoryForDay.createQueryBuilder()
 | 
			
		||||
					.update()
 | 
			
		||||
					.set(query)
 | 
			
		||||
					.where('id = :id', { id: logDay.id })
 | 
			
		||||
					.execute(),
 | 
			
		||||
			]);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// TODO: 前の時間のログがbufferにあった場合のハンドリング
 | 
			
		||||
		// 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。
 | 
			
		||||
		// 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが buffer に追加されたとすると、
 | 
			
		||||
| 
						 | 
				
			
			@ -429,8 +463,10 @@ export default abstract class Chart<T extends Record<string, any>> {
 | 
			
		|||
				Promise.all([
 | 
			
		||||
					this.claimCurrentLog(group, 'hour'),
 | 
			
		||||
					this.claimCurrentLog(group, 'day'),
 | 
			
		||||
				]).then(([logHour, logDay]) =>
 | 
			
		||||
					update(logHour, logDay))));
 | 
			
		||||
				]).then(([logHour, logDay]) => {
 | 
			
		||||
					update(logHour, logDay);
 | 
			
		||||
					bakeUniqueBuffer(logHour, logDay);
 | 
			
		||||
				})));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue