~ubuntu-branches/ubuntu/raring/eucalyptus/raring

« back to all changes in this revision

Viewing changes to clc/modules/www/conf/reports/Pruner.groovy

  • Committer: Package Import Robot
  • Author(s): Brian Thomason
  • Date: 2011-11-29 13:17:52 UTC
  • mfrom: (1.2.1 upstream)
  • mto: This revision was merged to the branch mainline in revision 185.
  • Revision ID: package-import@ubuntu.com-20111129131752-rq31al3ntutv2vvl
Tags: upstream-3.0.999beta1
ImportĀ upstreamĀ versionĀ 3.0.999beta1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import java.sql.Timestamp;
 
2
import groovy.sql.*;
 
3
import org.apache.log4j.*;
 
4
 
 
5
/**
 
6
 * This class prunes superfluous data from the records_logs table.
 
7
 *
 
8
 * Its primary method (prune) is invoked every time an instance report is
 
9
 * generated. It can also be invoked from the command line.
 
10
 *
 
11
 * @author twerges
 
12
 */
 
13
class Pruner
 
14
{
 
15
        private static Integer QUERY_LIMIT = 1000000  //HACK to avoid MySQL memory leak; @see pruneRepeatedly
 
16
        private static Logger  LOG = Logger.getLogger( Pruner.class )
 
17
 
 
18
        private final Sql sql
 
19
 
 
20
 
 
21
        public Pruner(Sql sql)
 
22
        {
 
23
                this.sql = sql
 
24
        }
 
25
        
 
26
 
 
27
 
 
28
        /**
 
29
         * Prunes redundant data from the database, by deleting all data except the
 
30
         * earliest and latest n rows for each instance.
 
31
         *
 
32
         * It's necessary to delete superfluous data because of a bug in the code that re-adds
 
33
         * instance data to the log table every 20 seconds. That bug creates a vast amount of
 
34
         * superfluous log data for long-running instances, which slows down report generation.
 
35
         *
 
36
         * This function can be run repeatedly, and will incrementally re-prune the
 
37
         * records_logs table each time.
 
38
         *
 
39
         * This function uses an algoritm which requires only one full table scan and does
 
40
         * not require mysql to sort the results.
 
41
         *
 
42
         * @param redundantRowsDeleteThreshold How many redundant rows an instance must have
 
43
         *   to be pruned.
 
44
         * @param targetRowsNum The number of rows to retain after pruning for each instance,
 
45
         *   at the beginning and at the end (n rows at the beginning, and n at the end).
 
46
         */
 
47
        public void prune(Integer redundantRowsDeleteThreshold=100, Integer targetRowsNum=80,
 
48
                                          Long onlyAfterTimestamp=0, Long onlyBeforeTimestamp=9999999999) {
 
49
 
 
50
                assert redundantRowsDeleteThreshold != null
 
51
                assert targetRowsNum != null
 
52
                assert onlyAfterTimestamp != null
 
53
                assert onlyBeforeTimestamp != null
 
54
                assert onlyAfterTimestamp < onlyBeforeTimestamp
 
55
                assert redundantRowsDeleteThreshold > 0
 
56
                assert targetRowsNum > 0
 
57
 
 
58
                LOG.info("Begin prune")
 
59
 
 
60
                /* Find earliest and latest Nth rows, according to the following algorithm.
 
61
                 * Establish two lists for each instance: one for the earliest nth rows, and one
 
62
                 * for the latest nth rows. Whenever we encounter a row for an instance which
 
63
                 * is earlier than the latest row in the early list, displace the earliest
 
64
                 * row in the early list and add the current row. Likewise (but in reverse)
 
65
                 * with the latest rows. At the end of this procedure, we will have two
 
66
                 * lists for each instance that contain the earliest and latest n rows for
 
67
                 * that instance. Then, delete all rows for the instance with a timestamp
 
68
                 * greater than the latest timestamp in the early list, and less than the
 
69
                 * earliest timestamp in the late list. What remains will be the earlist n
 
70
                 * and latest n rows for an instance; everything in between will be deleted.
 
71
                 *
 
72
                 * This algorithm was chosen for the following reasons: 1) It requires only
 
73
                 * one full table scan of the data; 2) it does not require sorting the data,
 
74
                 * which would cause MySQL to do a slow disk sort; 3) it uses a minmum of heap
 
75
                 * space; 4) it can be run incrementally without storing any data between
 
76
                 * invocations such as last pruning timestamp
 
77
                 */
 
78
 
 
79
                /* Find earliest and latest n rows */
 
80
                String query = """
 
81
                SELECT record_correlation_id, UNIX_TIMESTAMP(record_timestamp) as ts
 
82
                FROM records_logs
 
83
                WHERE record_class = 'VM'
 
84
                AND UNIX_TIMESTAMP(record_timestamp) > ?
 
85
                AND UNIX_TIMESTAMP(record_timestamp) < ?
 
86
                """     
 
87
 
 
88
                /* HACK to avoid memory leak from MySQL driver; add LIMIT clause to SQL */
 
89
                if (QUERY_LIMIT != null && QUERY_LIMIT > 0) { query = query + " LIMIT " + QUERY_LIMIT }
 
90
 
 
91
                def instanceInfoMap = [:]
 
92
 
 
93
                this.sql.eachRow( query, [onlyAfterTimestamp,onlyBeforeTimestamp] ) {
 
94
 
 
95
                        if (! instanceInfoMap.containsKey(it.record_correlation_id)) {
 
96
                                instanceInfoMap[it.record_correlation_id]=new InstanceInfo()
 
97
                                LOG.debug("Found new instance:" + it.record_correlation_id)
 
98
                        }
 
99
                        InstanceInfo info = instanceInfoMap[it.record_correlation_id]
 
100
 
 
101
                        //latestEarlyTs is only set when earlyList has reached n rows
 
102
                        if (info.latestEarlyTs==null) {
 
103
                                info.earlyList.add(it.ts)
 
104
                                if (info.earlyList.size() >= targetRowsNum) {
 
105
                                        info.earlyList.sort()
 
106
                                        info.latestEarlyTs = info.earlyList.last()
 
107
                                }
 
108
                        } else if (it.ts <= info.latestEarlyTs) {
 
109
                                Long ts = it.ts
 
110
                                info.earlyList.add(ts)
 
111
                                info.earlyList.sort()
 
112
                                info.earlyList.remove(info.earlyList.last())
 
113
                                info.latestEarlyTs = info.earlyList.last()
 
114
                        }
 
115
 
 
116
                        //earliestLateTs is only set when lateList has reached n rows
 
117
                        if (info.earliestLateTs==null) {
 
118
                                info.lateList.add(it.ts)
 
119
                                if (info.lateList.size() >= targetRowsNum) {
 
120
                                        info.lateList.sort()
 
121
                                        info.earliestLateTs = info.lateList.first()
 
122
                                }
 
123
                        } else if (it.ts >= info.earliestLateTs) {
 
124
                                Long ts = it.ts
 
125
                                info.lateList.add(ts)
 
126
                                info.lateList.sort()
 
127
                                info.lateList.remove(info.lateList.first())
 
128
                                info.earliestLateTs = info.lateList.first()
 
129
                        }
 
130
 
 
131
                        info.rowCnt = info.rowCnt+1
 
132
 
 
133
                }
 
134
 
 
135
 
 
136
                /* Delete data for each instance which is later than the latest early timestamp,
 
137
                 * but earlier than the earliest late timestamp.
 
138
                 */
 
139
                query = """
 
140
                DELETE FROM records_logs
 
141
                WHERE record_correlation_id = ?
 
142
                AND record_class = 'VM'
 
143
                AND UNIX_TIMESTAMP(record_timestamp) > ?
 
144
                AND UNIX_TIMESTAMP(record_timestamp) < ?
 
145
                """
 
146
                LOG.debug("Begin deleting")
 
147
                Integer redundantRowsCnt
 
148
                instanceInfoMap.each { key, value ->
 
149
                        redundantRowsCnt = value.rowCnt-(targetRowsNum*2)
 
150
                        LOG.debug("INSTANCE id:${key} rowsAboveThreshold:${redundantRowsCnt}")
 
151
                        if (value.rowCnt-(targetRowsNum*2) > redundantRowsDeleteThreshold) {
 
152
                                this.sql.executeUpdate(query, [key, value.latestEarlyTs, value.earliestLateTs])
 
153
                                LOG.debug(String.format("DELETE id:%s %d-%d",
 
154
                                                                                key, value.latestEarlyTs, value.earliestLateTs))
 
155
                        }
 
156
                }
 
157
 
 
158
                LOG.info("End prune")
 
159
        }   //end: prune method
 
160
 
 
161
 
 
162
        /**
 
163
         * HACK to handle memory leak in mysql/groovy.  The MySQL JDBC driver will throw an
 
164
         * OutOfMemory exception sometimes for ResultSets greater than 10M rows or so.
 
165
         * This happens even when you execute the query by itself in a groovy script with no
 
166
         * other code.
 
167
         *
 
168
         * To get around this problem, I added a "LIMIT" clause to the prune() method which
 
169
         * limits resultsets to 4M rows.
 
170
         *
 
171
         * This method calls prune repeatedly, so the entire log will be pruned (4M rows at
 
172
         * a time).
 
173
         */
 
174
        public void pruneRepeatedly()
 
175
        {
 
176
                def res = sql.firstRow("SELECT count(*) AS cnt FROM records_logs")
 
177
                if (QUERY_LIMIT != null) {
 
178
                        for (int i=0; i<res.cnt.intdiv(QUERY_LIMIT); i++) {
 
179
                                LOG.info("Prune iteration ${i}")
 
180
                                prune()  //prune 4M rows
 
181
                        }
 
182
                }
 
183
                LOG.info("Prune iteration final")
 
184
                prune()  //prune remainder of rows less than 4M
 
185
        }
 
186
 
 
187
 
 
188
        /**
 
189
         * Command-line invocation of the prune method
 
190
         */
 
191
        public static void main(String[] args) {
 
192
 
 
193
                /* Read cmd-line args */
 
194
                CliBuilder cli = new CliBuilder(usage:"Pruner.groovy -p mysqlPassword [options]")
 
195
                cli.D(args:1, required:false, argName:"dbName", "Database Name (default eucalyptus_records)")
 
196
                cli.u(args:1, required:false, argName:"username", "username (default eucalyptus)")
 
197
                cli.p(args:1, required:true,  argName:"password", "password for mysql")
 
198
                cli.h(args:1, required:false, argName:"host", "host of mysqld (default localhost)")
 
199
                cli.P(args:1, required:false, argName:"port", "port or mysqld (default 8777)")
 
200
                cli.g(args:0, required:false, argName:"debug", "debugging output (default false)")
 
201
                cli.a(args:1, required:false, argName:"onlyAfterTimestamp", "only prunes after timestamp in secs (default 0)")
 
202
                cli.b(args:1, required:false, argName:"onlyBeforeTimestamp", "only prunes before timestamp in secs (default max timestamp)")
 
203
                cli.t(args:1, required:false, argName:"redundantRowsDeleteThreshold", "only prunes instances more than n redundant rows (default 100)")
 
204
                cli.r(args:1, required:false, argName:"targetRowsNum", "how many rows to preserve at the beginning and end for each instance (default 80)")
 
205
                cli.e(args:0, required:false, argName:"pruneRepeatedly", "repeatedly prunes one section at a time")
 
206
                def options = cli.parse(args)
 
207
                if (!options) System.exit(-1)
 
208
 
 
209
                /* Parse cmd-line args into appropriate types and provide default values */
 
210
                def optsMap = [:]
 
211
                optsMap['D']=options.D ? options.D : "eucalyptus_records"
 
212
                optsMap['u']=options.u ? options.u : "eucalyptus"
 
213
                optsMap['p']=options.p
 
214
                optsMap['g']=options.g
 
215
                optsMap['h']=options.h ? options.h : "localhost"
 
216
                optsMap['P']=options.P ? Long.parseLong(options.P) : 8777l
 
217
                optsMap['a']=options.a ? Long.parseLong(options.a) : 0l
 
218
                optsMap['b']=options.b ? Long.parseLong(options.b) : 9999999999l
 
219
                optsMap['t']=options.t ? Integer.parseInt(options.t) : 100
 
220
                optsMap['r']=options.r ? Integer.parseInt(options.r) : 80
 
221
                optsMap['e']=options.e
 
222
 
 
223
                if (optsMap['g']) {
 
224
                        LOG.setLevel(Level.DEBUG)
 
225
                } else {
 
226
                        LOG.setLevel(Level.OFF)
 
227
                }
 
228
 
 
229
                LOG.debug(String.format("Using db:%s user:%s host:%s port:%d debug:%s " +
 
230
                                                                "after:%d before:%d threshold:%d target:%d i:%b", 
 
231
                                                                optsMap.D, optsMap.u, optsMap.h, optsMap.P, optsMap.g,
 
232
                                                                optsMap.a, optsMap.b, optsMap.t, optsMap.r, optsMap.e))
 
233
 
 
234
 
 
235
                /* Create a mysql connection, then prune */
 
236
                def connStr = "jdbc:mysql://${optsMap['h']}:${optsMap['P']}/${optsMap['D']}"
 
237
                Sql sql = Sql.newInstance(connStr, optsMap['u'],optsMap['p'],'com.mysql.jdbc.Driver')
 
238
 
 
239
                Pruner pruner = new Pruner(sql);
 
240
                if (optsMap['e']) {
 
241
                        pruner.pruneRepeatedly()
 
242
                } else {
 
243
                        pruner.prune(optsMap['t'], optsMap['r'], optsMap['a'], optsMap['b'])
 
244
                }
 
245
 
 
246
        }  //end: main method
 
247
 
 
248
 
 
249
}  //end: Pruner class
 
250
 
 
251
 
 
252
class InstanceInfo {
 
253
        List earlyList = []
 
254
        List lateList = []
 
255
        Long latestEarlyTs = null
 
256
        Long earliestLateTs = null
 
257
        Integer rowCnt=0
 
258
}