@@ -66,6 +66,11 @@ class account_history_plugin_impl
66
66
flat_set<account_id_type> _tracked_accounts;
67
67
bool _partial_operations = false ;
68
68
primary_index< simple_index< operation_history_object > >* _oho_index;
69
+ uint32_t _max_ops_per_account = -1 ;
70
+ private:
71
+ /* * add one history record, then check and remove the earliest history record */
72
+ void add_account_history ( const account_id_type account_id, const operation_history_id_type op_id );
73
+
69
74
};
70
75
71
76
account_history_plugin_impl::~account_history_plugin_impl ()
@@ -89,95 +94,146 @@ void account_history_plugin_impl::update_account_histories( const signed_block&
89
94
} ) );
90
95
};
91
96
92
- if ( _partial_operations)
97
+ if ( !o_op. valid () || ( _max_ops_per_account == 0 && _partial_operations ) )
93
98
{
94
- if ( !o_op.valid () )
95
- {
96
- _oho_index->use_next_id ();
97
- continue ;
98
- }
99
+ // Note: the 2nd and 3rd checks above are for better performance, when the db is not clean,
100
+ // they will break consistency of account_stats.total_ops and removed_ops and most_recent_op
101
+ _oho_index->use_next_id ();
102
+ continue ;
99
103
}
100
- else
101
- {
104
+ else if ( !_partial_operations )
102
105
// add to the operation history index
103
106
oho = create_oho ();
104
107
105
- if ( !o_op.valid () )
106
- {
107
- ilog ( " removing failed operation with ID: ${id}" , (" id" , oho->id ) );
108
- db.remove ( *oho );
109
- continue ;
110
- }
111
- }
112
-
113
108
const operation_history_object& op = *o_op;
114
109
115
110
// get the set of accounts this operation applies to
116
111
flat_set<account_id_type> impacted;
117
112
vector<authority> other;
118
- operation_get_required_authorities ( op.op , impacted, impacted, other );
113
+ operation_get_required_authorities ( op.op , impacted, impacted, other ); // fee_payer is added here
119
114
120
115
if ( op.op .which () == operation::tag< account_create_operation >::value )
121
- {
122
- if (!oho.valid ()) { oho = create_oho (); }
123
- impacted.insert ( oho->result .get <object_id_type>() );
124
- }
116
+ impacted.insert ( op.result .get <object_id_type>() );
125
117
else
126
118
graphene::app::operation_get_impacted_accounts ( op.op , impacted );
127
119
128
120
for ( auto & a : other )
129
121
for ( auto & item : a.account_auths )
130
122
impacted.insert ( item.first );
131
123
124
+ // be here, either _max_ops_per_account > 0, or _partial_operations == false, or both
125
+ // if _partial_operations == false, oho should have been created above
126
+ // so the only case should be checked here is:
127
+ // whether need to create oho if _max_ops_per_account > 0 and _partial_operations == true
128
+
132
129
// for each operation this account applies to that is in the config link it into the history
133
- if ( _tracked_accounts.size () == 0 )
130
+ if ( _tracked_accounts.size () == 0 ) // tracking all accounts
134
131
{
132
+ // if tracking all accounts, when impacted is not empty (although it will always be),
133
+ // still need to create oho if _max_ops_per_account > 0 and _partial_operations == true
134
+ // so always need to create oho if not done
135
135
if (!impacted.empty () && !oho.valid ()) { oho = create_oho (); }
136
- for ( auto & account_id : impacted )
136
+
137
+ if ( _max_ops_per_account > 0 )
137
138
{
138
- // we don't do index_account_keys here anymore, because
139
- // that indexing now happens in observers' post_evaluate()
140
-
141
- // add history
142
- const auto & stats_obj = account_id (db).statistics (db);
143
- const auto & ath = db.create <account_transaction_history_object>( [&]( account_transaction_history_object& obj ){
144
- obj.operation_id = oho->id ;
145
- obj.account = account_id;
146
- obj.sequence = stats_obj.total_ops +1 ;
147
- obj.next = stats_obj.most_recent_op ;
148
- });
149
- db.modify ( stats_obj, [&]( account_statistics_object& obj ){
150
- obj.most_recent_op = ath.id ;
151
- obj.total_ops = ath.sequence ;
152
- });
139
+ // Note: the check above is for better performance, when the db is not clean,
140
+ // it breaks consistency of account_stats.total_ops and removed_ops and most_recent_op,
141
+ // but it ensures it's safe to remove old entries in add_account_history(...)
142
+ for ( auto & account_id : impacted )
143
+ {
144
+ // we don't do index_account_keys here anymore, because
145
+ // that indexing now happens in observers' post_evaluate()
146
+
147
+ // add history
148
+ add_account_history ( account_id, oho->id );
149
+ }
153
150
}
154
151
}
155
- else
152
+ else // tracking a subset of accounts
156
153
{
157
- for ( auto account_id : _tracked_accounts )
154
+ // whether need to create oho if _max_ops_per_account > 0 and _partial_operations == true ?
155
+ // the answer: only need to create oho if a tracked account is impacted and need to save history
156
+
157
+ if ( _max_ops_per_account > 0 )
158
158
{
159
- if ( impacted.find ( account_id ) != impacted.end () )
159
+ // Note: the check above is for better performance, when the db is not clean,
160
+ // it breaks consistency of account_stats.total_ops and removed_ops and most_recent_op,
161
+ // but it ensures it's safe to remove old entries in add_account_history(...)
162
+ for ( auto account_id : _tracked_accounts )
160
163
{
161
- if (!oho.valid ()) { oho = create_oho (); }
162
- // add history
163
- const auto & stats_obj = account_id (db).statistics (db);
164
- const auto & ath = db.create <account_transaction_history_object>( [&]( account_transaction_history_object& obj ){
165
- obj.operation_id = oho->id ;
166
- obj.account = account_id;
167
- obj.sequence = stats_obj.total_ops +1 ;
168
- obj.next = stats_obj.most_recent_op ;
169
- });
170
- db.modify ( stats_obj, [&]( account_statistics_object& obj ){
171
- obj.most_recent_op = ath.id ;
172
- obj.total_ops = ath.sequence ;
173
- });
164
+ if ( impacted.find ( account_id ) != impacted.end () )
165
+ {
166
+ if (!oho.valid ()) { oho = create_oho (); }
167
+ // add history
168
+ add_account_history ( account_id, oho->id );
169
+ }
174
170
}
175
171
}
176
172
}
177
173
if (_partial_operations && ! oho.valid ())
178
174
_oho_index->use_next_id ();
179
175
}
180
176
}
177
+
178
+ void account_history_plugin_impl::add_account_history ( const account_id_type account_id, const operation_history_id_type op_id )
179
+ {
180
+ graphene::chain::database& db = database ();
181
+ const auto & stats_obj = account_id (db).statistics (db);
182
+ // add new entry
183
+ const auto & ath = db.create <account_transaction_history_object>( [&]( account_transaction_history_object& obj ){
184
+ obj.operation_id = op_id;
185
+ obj.account = account_id;
186
+ obj.sequence = stats_obj.total_ops + 1 ;
187
+ obj.next = stats_obj.most_recent_op ;
188
+ });
189
+ db.modify ( stats_obj, [&]( account_statistics_object& obj ){
190
+ obj.most_recent_op = ath.id ;
191
+ obj.total_ops = ath.sequence ;
192
+ });
193
+ // remove the earliest account history entry if too many
194
+ // _max_ops_per_account is guaranteed to be non-zero outside
195
+ if ( stats_obj.total_ops - stats_obj.removed_ops > _max_ops_per_account )
196
+ {
197
+ // look for the earliest entry
198
+ const auto & his_idx = db.get_index_type <account_transaction_history_index>();
199
+ const auto & by_seq_idx = his_idx.indices ().get <by_seq>();
200
+ auto itr = by_seq_idx.lower_bound ( boost::make_tuple ( account_id, 0 ) );
201
+ // make sure don't remove the one just added
202
+ if ( itr != by_seq_idx.end () && itr->account == account_id && itr->id != ath.id )
203
+ {
204
+ // if found, remove the entry, and adjust account stats object
205
+ const auto remove_op_id = itr->operation_id ;
206
+ const auto itr_remove = itr;
207
+ ++itr;
208
+ db.remove ( *itr_remove );
209
+ db.modify ( stats_obj, [&]( account_statistics_object& obj ){
210
+ obj.removed_ops = obj.removed_ops + 1 ;
211
+ });
212
+ // modify previous node's next pointer
213
+ // this should be always true, but just have a check here
214
+ if ( itr != by_seq_idx.end () && itr->account == account_id )
215
+ {
216
+ db.modify ( *itr, [&]( account_transaction_history_object& obj ){
217
+ obj.next = account_transaction_history_id_type ();
218
+ });
219
+ }
220
+ // else need to modify the head pointer, but it shouldn't be true
221
+
222
+ // remove the operation history entry (1.11.x) if configured and no reference left
223
+ if ( _partial_operations )
224
+ {
225
+ // check for references
226
+ const auto & by_opid_idx = his_idx.indices ().get <by_opid>();
227
+ if ( by_opid_idx.find ( remove_op_id ) == by_opid_idx.end () )
228
+ {
229
+ // if no reference, remove
230
+ db.remove ( remove_op_id (db) );
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+
181
237
} // end namespace detail
182
238
183
239
@@ -207,6 +263,7 @@ void account_history_plugin::plugin_set_program_options(
207
263
cli.add_options ()
208
264
(" track-account" , boost::program_options::value<std::vector<std::string>>()->composing ()->multitoken (), " Account ID to track history for (may specify multiple times)" )
209
265
(" partial-operations" , boost::program_options::value<bool >(), " Keep only those operations in memory that are related to account history tracking" )
266
+ (" max-ops-per-account" , boost::program_options::value<uint32_t >(), " Maximum number of operations per account will be kept in memory" )
210
267
;
211
268
cfg.add (cli);
212
269
}
@@ -221,6 +278,9 @@ void account_history_plugin::plugin_initialize(const boost::program_options::var
221
278
if (options.count (" partial-operations" )) {
222
279
my->_partial_operations = options[" partial-operations" ].as <bool >();
223
280
}
281
+ if (options.count (" max-ops-per-account" )) {
282
+ my->_max_ops_per_account = options[" max-ops-per-account" ].as <uint32_t >();
283
+ }
224
284
}
225
285
226
286
void account_history_plugin::plugin_startup ()
0 commit comments