1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "mozilla/DebugOnly.h"
8 
9 #include "VacuumManager.h"
10 
11 #include "mozilla/Services.h"
12 #include "mozilla/Preferences.h"
13 #include "nsIObserverService.h"
14 #include "nsIFile.h"
15 #include "nsThreadUtils.h"
16 #include "mozilla/Logging.h"
17 #include "prtime.h"
18 
19 #include "mozStorageConnection.h"
20 #include "mozIStorageStatement.h"
21 #include "mozIStorageAsyncStatement.h"
22 #include "mozIStoragePendingStatement.h"
23 #include "mozIStorageError.h"
24 #include "mozStorageHelper.h"
25 #include "nsXULAppAPI.h"
26 
27 #define OBSERVER_TOPIC_IDLE_DAILY "idle-daily"
28 #define OBSERVER_TOPIC_XPCOM_SHUTDOWN "xpcom-shutdown"
29 
30 // Used to notify begin and end of a heavy IO task.
31 #define OBSERVER_TOPIC_HEAVY_IO "heavy-io-task"
32 #define OBSERVER_DATA_VACUUM_BEGIN u"vacuum-begin"
33 #define OBSERVER_DATA_VACUUM_END u"vacuum-end"
34 
35 // This preferences root will contain last vacuum timestamps (in seconds) for
36 // each database.  The database filename is used as a key.
37 #define PREF_VACUUM_BRANCH "storage.vacuum.last."
38 
39 // Time between subsequent vacuum calls for a certain database.
40 #define VACUUM_INTERVAL_SECONDS 30 * 86400  // 30 days.
41 
42 extern mozilla::LazyLogModule gStorageLog;
43 
44 namespace mozilla {
45 namespace storage {
46 
47 namespace {
48 
49 ////////////////////////////////////////////////////////////////////////////////
50 //// BaseCallback
51 
52 class BaseCallback : public mozIStorageStatementCallback {
53  public:
54   NS_DECL_ISUPPORTS
55   NS_DECL_MOZISTORAGESTATEMENTCALLBACK
BaseCallback()56   BaseCallback() {}
57 
58  protected:
~BaseCallback()59   virtual ~BaseCallback() {}
60 };
61 
62 NS_IMETHODIMP
HandleError(mozIStorageError * aError)63 BaseCallback::HandleError(mozIStorageError *aError) {
64 #ifdef DEBUG
65   int32_t result;
66   nsresult rv = aError->GetResult(&result);
67   NS_ENSURE_SUCCESS(rv, rv);
68   nsAutoCString message;
69   rv = aError->GetMessage(message);
70   NS_ENSURE_SUCCESS(rv, rv);
71 
72   nsAutoCString warnMsg;
73   warnMsg.AppendLiteral("An error occured during async execution: ");
74   warnMsg.AppendInt(result);
75   warnMsg.Append(' ');
76   warnMsg.Append(message);
77   NS_WARNING(warnMsg.get());
78 #endif
79   return NS_OK;
80 }
81 
82 NS_IMETHODIMP
HandleResult(mozIStorageResultSet * aResultSet)83 BaseCallback::HandleResult(mozIStorageResultSet *aResultSet) {
84   // We could get results from PRAGMA statements, but we don't mind them.
85   return NS_OK;
86 }
87 
88 NS_IMETHODIMP
HandleCompletion(uint16_t aReason)89 BaseCallback::HandleCompletion(uint16_t aReason) {
90   // By default BaseCallback will just be silent on completion.
91   return NS_OK;
92 }
93 
94 NS_IMPL_ISUPPORTS(BaseCallback, mozIStorageStatementCallback)
95 
96 ////////////////////////////////////////////////////////////////////////////////
97 //// Vacuumer declaration.
98 
99 class Vacuumer : public BaseCallback {
100  public:
101   NS_DECL_MOZISTORAGESTATEMENTCALLBACK
102 
103   explicit Vacuumer(mozIStorageVacuumParticipant *aParticipant);
104 
105   bool execute();
106   nsresult notifyCompletion(bool aSucceeded);
107 
108  private:
109   nsCOMPtr<mozIStorageVacuumParticipant> mParticipant;
110   nsCString mDBFilename;
111   nsCOMPtr<mozIStorageConnection> mDBConn;
112 };
113 
114 ////////////////////////////////////////////////////////////////////////////////
115 //// Vacuumer implementation.
116 
Vacuumer(mozIStorageVacuumParticipant * aParticipant)117 Vacuumer::Vacuumer(mozIStorageVacuumParticipant *aParticipant)
118     : mParticipant(aParticipant) {}
119 
execute()120 bool Vacuumer::execute() {
121   MOZ_ASSERT(NS_IsMainThread(), "Must be running on the main thread!");
122 
123   // Get the connection and check its validity.
124   nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn));
125   NS_ENSURE_SUCCESS(rv, false);
126   bool ready = false;
127   if (!mDBConn || NS_FAILED(mDBConn->GetConnectionReady(&ready)) || !ready) {
128     NS_WARNING("Unable to get a connection to vacuum database");
129     return false;
130   }
131 
132   // Ask for the expected page size.  Vacuum can change the page size, unless
133   // the database is using WAL journaling.
134   // TODO Bug 634374: figure out a strategy to fix page size with WAL.
135   int32_t expectedPageSize = 0;
136   rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize);
137   if (NS_FAILED(rv) || !Service::pageSizeIsValid(expectedPageSize)) {
138     NS_WARNING("Invalid page size requested for database, will use default ");
139     NS_WARNING(mDBFilename.get());
140     expectedPageSize = Service::getDefaultPageSize();
141   }
142 
143   // Get the database filename.  Last vacuum time is stored under this name
144   // in PREF_VACUUM_BRANCH.
145   nsCOMPtr<nsIFile> databaseFile;
146   mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile));
147   if (!databaseFile) {
148     NS_WARNING("Trying to vacuum a in-memory database!");
149     return false;
150   }
151   nsAutoString databaseFilename;
152   rv = databaseFile->GetLeafName(databaseFilename);
153   NS_ENSURE_SUCCESS(rv, false);
154   mDBFilename = NS_ConvertUTF16toUTF8(databaseFilename);
155   MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty");
156 
157   // Check interval from last vacuum.
158   int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC);
159   int32_t lastVacuum;
160   nsAutoCString prefName(PREF_VACUUM_BRANCH);
161   prefName += mDBFilename;
162   rv = Preferences::GetInt(prefName.get(), &lastVacuum);
163   if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS) {
164     // This database was vacuumed recently, skip it.
165     return false;
166   }
167 
168   // Notify that we are about to start vacuuming.  The participant can opt-out
169   // if it cannot handle a vacuum at this time, and then we'll move to the next
170   // one.
171   bool vacuumGranted = false;
172   rv = mParticipant->OnBeginVacuum(&vacuumGranted);
173   NS_ENSURE_SUCCESS(rv, false);
174   if (!vacuumGranted) {
175     return false;
176   }
177 
178   // Notify a heavy IO task is about to start.
179   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
180   if (os) {
181     rv = os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO,
182                              OBSERVER_DATA_VACUUM_BEGIN);
183     MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to notify");
184   }
185 
186   // Execute the statements separately, since the pragma may conflict with the
187   // vacuum, if they are executed in the same transaction.
188   nsCOMPtr<mozIStorageAsyncStatement> pageSizeStmt;
189   nsAutoCString pageSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR
190                               "PRAGMA page_size = ");
191   pageSizeQuery.AppendInt(expectedPageSize);
192   rv = mDBConn->CreateAsyncStatement(pageSizeQuery,
193                                      getter_AddRefs(pageSizeStmt));
194   NS_ENSURE_SUCCESS(rv, false);
195   RefPtr<BaseCallback> callback = new BaseCallback();
196   nsCOMPtr<mozIStoragePendingStatement> ps;
197   rv = pageSizeStmt->ExecuteAsync(callback, getter_AddRefs(ps));
198   NS_ENSURE_SUCCESS(rv, false);
199 
200   nsCOMPtr<mozIStorageAsyncStatement> stmt;
201   rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING("VACUUM"),
202                                      getter_AddRefs(stmt));
203   NS_ENSURE_SUCCESS(rv, false);
204   rv = stmt->ExecuteAsync(this, getter_AddRefs(ps));
205   NS_ENSURE_SUCCESS(rv, false);
206 
207   return true;
208 }
209 
210 ////////////////////////////////////////////////////////////////////////////////
211 //// mozIStorageStatementCallback
212 
213 NS_IMETHODIMP
HandleError(mozIStorageError * aError)214 Vacuumer::HandleError(mozIStorageError *aError) {
215   int32_t result;
216   nsresult rv;
217   nsAutoCString message;
218 
219 #ifdef DEBUG
220   rv = aError->GetResult(&result);
221   NS_ENSURE_SUCCESS(rv, rv);
222   rv = aError->GetMessage(message);
223   NS_ENSURE_SUCCESS(rv, rv);
224 
225   nsAutoCString warnMsg;
226   warnMsg.AppendLiteral("Unable to vacuum database: ");
227   warnMsg.Append(mDBFilename);
228   warnMsg.AppendLiteral(" - ");
229   warnMsg.AppendInt(result);
230   warnMsg.Append(' ');
231   warnMsg.Append(message);
232   NS_WARNING(warnMsg.get());
233 #endif
234 
235   if (MOZ_LOG_TEST(gStorageLog, LogLevel::Error)) {
236     rv = aError->GetResult(&result);
237     NS_ENSURE_SUCCESS(rv, rv);
238     rv = aError->GetMessage(message);
239     NS_ENSURE_SUCCESS(rv, rv);
240     MOZ_LOG(gStorageLog, LogLevel::Error,
241             ("Vacuum failed with error: %d '%s'. Database was: '%s'", result,
242              message.get(), mDBFilename.get()));
243   }
244   return NS_OK;
245 }
246 
247 NS_IMETHODIMP
HandleResult(mozIStorageResultSet * aResultSet)248 Vacuumer::HandleResult(mozIStorageResultSet *aResultSet) {
249   NS_NOTREACHED("Got a resultset from a vacuum?");
250   return NS_OK;
251 }
252 
253 NS_IMETHODIMP
HandleCompletion(uint16_t aReason)254 Vacuumer::HandleCompletion(uint16_t aReason) {
255   if (aReason == REASON_FINISHED) {
256     // Update last vacuum time.
257     int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC);
258     MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty");
259     nsAutoCString prefName(PREF_VACUUM_BRANCH);
260     prefName += mDBFilename;
261     DebugOnly<nsresult> rv = Preferences::SetInt(prefName.get(), now);
262     MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference");
263   }
264 
265   notifyCompletion(aReason == REASON_FINISHED);
266 
267   return NS_OK;
268 }
269 
notifyCompletion(bool aSucceeded)270 nsresult Vacuumer::notifyCompletion(bool aSucceeded) {
271   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
272   if (os) {
273     os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO,
274                         OBSERVER_DATA_VACUUM_END);
275   }
276 
277   nsresult rv = mParticipant->OnEndVacuum(aSucceeded);
278   NS_ENSURE_SUCCESS(rv, rv);
279 
280   return NS_OK;
281 }
282 
283 }  // namespace
284 
285 ////////////////////////////////////////////////////////////////////////////////
286 //// VacuumManager
287 
288 NS_IMPL_ISUPPORTS(VacuumManager, nsIObserver)
289 
290 VacuumManager *VacuumManager::gVacuumManager = nullptr;
291 
getSingleton()292 already_AddRefed<VacuumManager> VacuumManager::getSingleton() {
293   // Don't allocate it in the child Process.
294   if (!XRE_IsParentProcess()) {
295     return nullptr;
296   }
297 
298   if (!gVacuumManager) {
299     auto manager = MakeRefPtr<VacuumManager>();
300     MOZ_ASSERT(gVacuumManager == manager.get());
301     return manager.forget();
302   }
303   return do_AddRef(gVacuumManager);
304 }
305 
VacuumManager()306 VacuumManager::VacuumManager() : mParticipants("vacuum-participant") {
307   MOZ_ASSERT(!gVacuumManager,
308              "Attempting to create two instances of the service!");
309   gVacuumManager = this;
310 }
311 
~VacuumManager()312 VacuumManager::~VacuumManager() {
313   // Remove the static reference to the service.  Check to make sure its us
314   // in case somebody creates an extra instance of the service.
315   MOZ_ASSERT(gVacuumManager == this,
316              "Deleting a non-singleton instance of the service");
317   if (gVacuumManager == this) {
318     gVacuumManager = nullptr;
319   }
320 }
321 
322 ////////////////////////////////////////////////////////////////////////////////
323 //// nsIObserver
324 
325 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)326 VacuumManager::Observe(nsISupports *aSubject, const char *aTopic,
327                        const char16_t *aData) {
328   if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) {
329     // Try to run vacuum on all registered entries.  Will stop at the first
330     // successful one.
331     nsCOMArray<mozIStorageVacuumParticipant> entries;
332     mParticipants.GetEntries(entries);
333     // If there are more entries than what a month can contain, we could end up
334     // skipping some, since we run daily.  So we use a starting index.
335     static const char *kPrefName = PREF_VACUUM_BRANCH "index";
336     int32_t startIndex = Preferences::GetInt(kPrefName, 0);
337     if (startIndex >= entries.Count()) {
338       startIndex = 0;
339     }
340     int32_t index;
341     for (index = startIndex; index < entries.Count(); ++index) {
342       RefPtr<Vacuumer> vacuum = new Vacuumer(entries[index]);
343       // Only vacuum one database per day.
344       if (vacuum->execute()) {
345         break;
346       }
347     }
348     DebugOnly<nsresult> rv = Preferences::SetInt(kPrefName, index);
349     MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference");
350   }
351 
352   return NS_OK;
353 }
354 
355 }  // namespace storage
356 }  // namespace mozilla
357