Skip to content

Commit 504aa5d

Browse files
committed
Account history: option to prune old data (#292)
1 parent 19f0ec5 commit 504aa5d

File tree

4 files changed

+126
-58
lines changed

4 files changed

+126
-58
lines changed

libraries/app/api.cpp

+4-3
Original file line numberDiff line numberDiff line change
@@ -493,13 +493,14 @@ namespace graphene { namespace app {
493493
const auto& db = *_app.chain_database();
494494
FC_ASSERT(limit <= 100);
495495
vector<operation_history_object> result;
496+
const auto& stats = account(db).statistics(db);
496497
if( start == 0 )
497-
start = account(db).statistics(db).total_ops;
498+
start = stats.total_ops;
498499
else
499-
start = min( account(db).statistics(db).total_ops, start );
500+
start = min( stats.total_ops, start );
500501

501502

502-
if( start >= stop && start > 0 && limit > 0 )
503+
if( start >= stop && start > stats.removed_ops && limit > 0 )
503504
{
504505
const auto& hist_idx = db.get_index_type<account_transaction_history_index>();
505506
const auto& by_seq_idx = hist_idx.indices().get<by_seq>();

libraries/chain/include/graphene/chain/account_object.hpp

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ namespace graphene { namespace chain {
5050
* Keep the most recent operation as a root pointer to a linked list of the transaction history.
5151
*/
5252
account_transaction_history_id_type most_recent_op;
53+
/** Total operations related to this account. */
5354
uint32_t total_ops = 0;
55+
/** Total operations related to this account that has been removed from the database. */
56+
uint32_t removed_ops = 0;
5457

5558
/**
5659
* When calculating votes it is necessary to know how much is stored in orders (and thus unavailable for
@@ -386,7 +389,7 @@ FC_REFLECT_DERIVED( graphene::chain::account_statistics_object,
386389
(graphene::chain::object),
387390
(owner)
388391
(most_recent_op)
389-
(total_ops)
392+
(total_ops)(removed_ops)
390393
(total_core_in_orders)
391394
(lifetime_fees_paid)
392395
(pending_fees)(pending_vested_fees)

libraries/chain/include/graphene/chain/operation_history_object.hpp

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ namespace graphene { namespace chain {
102102
struct by_id;
103103
struct by_seq;
104104
struct by_op;
105+
struct by_opid;
105106
typedef multi_index_container<
106107
account_transaction_history_object,
107108
indexed_by<
@@ -117,6 +118,9 @@ typedef multi_index_container<
117118
member< account_transaction_history_object, account_id_type, &account_transaction_history_object::account>,
118119
member< account_transaction_history_object, operation_history_id_type, &account_transaction_history_object::operation_id>
119120
>
121+
>,
122+
ordered_non_unique< tag<by_opid>,
123+
member< account_transaction_history_object, operation_history_id_type, &account_transaction_history_object::operation_id>
120124
>
121125
>
122126
> account_transaction_history_multi_index_type;

libraries/plugins/account_history/account_history_plugin.cpp

+114-54
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ class account_history_plugin_impl
6666
flat_set<account_id_type> _tracked_accounts;
6767
bool _partial_operations = false;
6868
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+
6974
};
7075

7176
account_history_plugin_impl::~account_history_plugin_impl()
@@ -89,95 +94,146 @@ void account_history_plugin_impl::update_account_histories( const signed_block&
8994
} ) );
9095
};
9196

92-
if (_partial_operations)
97+
if( !o_op.valid() || ( _max_ops_per_account == 0 && _partial_operations ) )
9398
{
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;
99103
}
100-
else
101-
{
104+
else if( !_partial_operations )
102105
// add to the operation history index
103106
oho = create_oho();
104107

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-
113108
const operation_history_object& op = *o_op;
114109

115110
// get the set of accounts this operation applies to
116111
flat_set<account_id_type> impacted;
117112
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
119114

120115
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>() );
125117
else
126118
graphene::app::operation_get_impacted_accounts( op.op, impacted );
127119

128120
for( auto& a : other )
129121
for( auto& item : a.account_auths )
130122
impacted.insert( item.first );
131123

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+
132129
// 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
134131
{
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
135135
if (!impacted.empty() && !oho.valid()) { oho = create_oho(); }
136-
for( auto& account_id : impacted )
136+
137+
if( _max_ops_per_account > 0 )
137138
{
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+
}
153150
}
154151
}
155-
else
152+
else // tracking a subset of accounts
156153
{
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 )
158158
{
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 )
160163
{
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+
}
174170
}
175171
}
176172
}
177173
if (_partial_operations && ! oho.valid())
178174
_oho_index->use_next_id();
179175
}
180176
}
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+
181237
} // end namespace detail
182238

183239

@@ -207,6 +263,7 @@ void account_history_plugin::plugin_set_program_options(
207263
cli.add_options()
208264
("track-account", boost::program_options::value<std::vector<std::string>>()->composing()->multitoken(), "Account ID to track history for (may specify multiple times)")
209265
("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")
210267
;
211268
cfg.add(cli);
212269
}
@@ -221,6 +278,9 @@ void account_history_plugin::plugin_initialize(const boost::program_options::var
221278
if (options.count("partial-operations")) {
222279
my->_partial_operations = options["partial-operations"].as<bool>();
223280
}
281+
if (options.count("max-ops-per-account")) {
282+
my->_max_ops_per_account = options["max-ops-per-account"].as<uint32_t>();
283+
}
224284
}
225285

226286
void account_history_plugin::plugin_startup()

0 commit comments

Comments
 (0)