summaryrefslogtreecommitdiff
path: root/worker.js
blob: 1f1263eed590c1d8b0420492bd335f2c2ba57501 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/node
/* jshint esnext:true */
"use strict";

var amqp = require("amqplib"),
    Seq = require("sequelize"),
    sleep = require("sleep-promise"),
    hash = require("object-hash");

var RABBITMQ_URI = process.env.RABBITMQ_URI,
    DATABASE_URI = process.env.DATABASE_URI;

(async () => {
    let seq, model, rabbit, ch;

    while (true) {
        try {
            seq = new Seq(DATABASE_URI),
            rabbit = await amqp.connect(RABBITMQ_URI),
            ch = await rabbit.createChannel();
            await ch.assertQueue("crunch", {durable: true});
            break;
        } catch (err) {
            console.error(err);
            await sleep(5000);
        }
    }
    model = require("../orm/model")(seq, Seq);

    let queue = [],
        timer = undefined;

    await seq.sync();

    // no batching for cruncher, not worth it
    // we'll read a million tables and commit just one record
    // once per hour
    await ch.prefetch(1);

    ch.consume("crunch", async (msg) => {
        let dim = JSON.parse(msg.content),
            filter = dim.filter, dimension, filter_hash,
            action;
        console.log("crunching dimension", dim);

        // we don't want to store duplicate filters
        // but we can't reliably compare JSON in SQL either
        // so we hash the object and lookup/store a sha1
        filter_hash = hash(dim.filter, { unorderedArrays: true });
        try {
            dimension = (await model.StatsDimensions.findOrCreate({
                where: {
                    dimension_on: dim.dimension_on,
                    filter_hash: filter_hash
                }
            }))[0];
        } catch (err) {
            console.error(err);
            await ch.nack(msg, false, false);
            return;
        }

        // not implemented -> error
        switch (dimension.dimension_on) {
            case "hero":
                action = calculateHeroStats(dimension.id, filter);
                break;
            default:
                action = async () => { throw "dimension_on not implemented " + dimension.dimension_on; };
        }

        try {
            await action;
            console.log("acking");
            await ch.ack(msg);
        } catch (err) {
            console.error(err);
            await ch.nack(msg, false, false);  // nack and do not requeue
        }
    }, { noAck: false });

    async function calculateHeroStats(dimension_id, filter) {
        let win_rate, pick_rate, gold_per_min;

        // in literals: q -> column name, e -> function or string
        let q = (qry) => seq.dialect.QueryGenerator.quote(qry),
            e = (qry) => seq.dialect.QueryGenerator.escape(qry);

        // TODO (workaround) ignore old API data where participant.gold was null
        filter["$participant.gold$"] = { $ne: null };
        console.log(filter);

        // alternative for win rate
        //[ seq.literal(`${e(seq.fn("sum", seq.cast(seq.col("participant.winner"), "int") ))} / ${e(seq.fn("count", seq.col("participant.id")))}`), "win_rate" ]

        // calculate stats
        let total_participants = await model.Participant.count({
            where: filter,
            include: [ {
                model: model.Roster,
                attributes: [],
                include: [ {
                    model: model.Match,
                    attributes: []
                } ]
            }, {
                model: model.Heros,
                attributes: []
            } ]
        });

        // TODO (workaround) filter unmapped actor<->hero
        filter["$hero.id$"] = { $ne: null };
        let data = await model.Participant.findAll({
            where: filter,
            attributes: [
                // meta
                [ seq.col("hero.name"), "hero_name" ],
                [ seq.col("hero.id"), "hero_id" ],

                // stats, see `hero_stats` table
                [ seq.literal(`${q(seq.fn("count", seq.col("participant.id") ))} / ${total_participants}`), "pick_rate" ],
                [ seq.fn("avg", seq.cast(seq.col("participant.winner"), "int") ), "win_rate" ],
                [ seq.fn("sum", seq.literal(`${q("participant.gold")} / ${q("roster.match.duration")}`)), "gold_per_min" ],
                [ seq.fn("avg", seq.literal(`60.0 * ${q("participant.minion_kills")} / ${q("roster.match.duration")}`)), "cs_per_min" ]
            ],
            group: q("hero.name"),
            include: [ {
                model: model.Roster,
                attributes: [],
                include: [ {
                    model: model.Match,
                    attributes: []
                } ]
            }, {
                model: model.Heros,
                attributes: []
            } ]
        });
        console.log("hero stats", filter, data.map((d) => d.dataValues));
        // insert hero x dimension x stats
        await seq.transaction({ autocommit: false }, (transaction) =>
            Promise.all(data.map(async (record) => {
                let hero_stat_db = await model.HeroStats.create(record.dataValues);
                await model.HeroDimension.create({
                    hero_id: record.get("hero_id"),
                    dimension_id: dimension_id,
                    stats_id: hero_stat_db.get("id"),
                    computed_on: new Date()
                }, { transaction: transaction });
            }))
        );
    }
})();