Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/policy/cloud/component_cloud_policy_updater.h" | 5 #include "chrome/browser/policy/cloud/component_cloud_policy_updater.h" |
| 6 | 6 |
| 7 #include <string> | 7 #include <string> |
| 8 | 8 |
| 9 #include "base/bind.h" | 9 #include "base/bind.h" |
| 10 #include "base/location.h" | 10 #include "base/bind_helpers.h" |
| 11 #include "base/logging.h" | 11 #include "base/logging.h" |
| 12 #include "base/sequenced_task_runner.h" | 12 #include "base/sequenced_task_runner.h" |
| 13 #include "base/stl_util.h" | 13 #include "base/strings/string_number_conversions.h" |
| 14 #include "chrome/browser/policy/cloud/component_cloud_policy_store.h" | 14 #include "chrome/browser/policy/cloud/component_cloud_policy_store.h" |
| 15 #include "chrome/browser/policy/cloud/proto/chrome_extension_policy.pb.h" | 15 #include "chrome/browser/policy/cloud/proto/chrome_extension_policy.pb.h" |
| 16 #include "chrome/browser/policy/cloud/proto/device_management_backend.pb.h" | 16 #include "chrome/browser/policy/cloud/proto/device_management_backend.pb.h" |
| 17 #include "googleurl/src/gurl.h" | 17 #include "chrome/browser/policy/policy_service.h" |
| 18 #include "net/base/backoff_entry.h" | |
| 19 #include "net/base/load_flags.h" | |
| 20 #include "net/base/net_errors.h" | |
| 21 #include "net/url_request/url_fetcher.h" | |
| 22 #include "net/url_request/url_fetcher_delegate.h" | |
| 23 #include "net/url_request/url_request_context_getter.h" | 18 #include "net/url_request/url_request_context_getter.h" |
| 24 #include "net/url_request/url_request_status.h" | |
| 25 | 19 |
| 26 namespace em = enterprise_management; | 20 namespace em = enterprise_management; |
| 27 | 21 |
| 28 namespace policy { | 22 namespace policy { |
| 29 | 23 |
| 30 namespace { | 24 namespace { |
| 31 | 25 |
| 32 // The maximum size of the serialized policy protobuf. | 26 // The maximum size of the serialized policy protobuf. |
| 33 const size_t kPolicyProtoMaxSize = 16 * 1024; | 27 const size_t kPolicyProtoMaxSize = 16 * 1024; |
| 34 | 28 |
| 35 // The maximum size of the downloaded policy data. | 29 // The maximum size of the downloaded policy data. |
| 36 const int64 kPolicyDataMaxSize = 5 * 1024 * 1024; | 30 const int64 kPolicyDataMaxSize = 5 * 1024 * 1024; |
| 37 | 31 |
| 38 // Policies for exponential backoff of failed requests. There are 3 policies, | |
| 39 // corresponding to the 3 RetrySchedule enum values below. | |
| 40 | |
| 41 // For temporary errors (HTTP 500, RST, etc). | |
| 42 const net::BackoffEntry::Policy kRetrySoonPolicy = { | |
| 43 // Number of initial errors to ignore before starting to back off. | |
| 44 0, | |
| 45 | |
| 46 // Initial delay in ms: 60 seconds. | |
| 47 1000 * 60, | |
| 48 | |
| 49 // Factor by which the waiting time is multiplied. | |
| 50 2, | |
| 51 | |
| 52 // Fuzzing percentage; this spreads delays randomly between 80% and 100% | |
| 53 // of the calculated time. | |
| 54 0.20, | |
| 55 | |
| 56 // Maximum delay in ms: 12 hours. | |
| 57 1000 * 60 * 60 * 12, | |
| 58 | |
| 59 // When to discard an entry: never. | |
| 60 -1, | |
| 61 | |
| 62 // |always_use_initial_delay|; false means that the initial delay is | |
| 63 // applied after the first error, and starts backing off from there. | |
| 64 false, | |
| 65 }; | |
| 66 | |
| 67 // For other errors (request failed, server errors). | |
| 68 const net::BackoffEntry::Policy kRetryLaterPolicy = { | |
| 69 // Number of initial errors to ignore before starting to back off. | |
| 70 0, | |
| 71 | |
| 72 // Initial delay in ms: 1 hour. | |
| 73 1000 * 60 * 60, | |
| 74 | |
| 75 // Factor by which the waiting time is multiplied. | |
| 76 2, | |
| 77 | |
| 78 // Fuzzing percentage; this spreads delays randomly between 80% and 100% | |
| 79 // of the calculated time. | |
| 80 0.20, | |
| 81 | |
| 82 // Maximum delay in ms: 12 hours. | |
| 83 1000 * 60 * 60 * 12, | |
| 84 | |
| 85 // When to discard an entry: never. | |
| 86 -1, | |
| 87 | |
| 88 // |always_use_initial_delay|; false means that the initial delay is | |
| 89 // applied after the first error, and starts backing off from there. | |
| 90 false, | |
| 91 }; | |
| 92 | |
| 93 // When the data fails validation (maybe because the policy URL and the data | |
| 94 // served at that URL are out of sync). This essentially retries every 12 hours, | |
| 95 // with some random jitter. | |
| 96 const net::BackoffEntry::Policy kRetryMuchLaterPolicy = { | |
| 97 // Number of initial errors to ignore before starting to back off. | |
| 98 0, | |
| 99 | |
| 100 // Initial delay in ms: 12 hours. | |
| 101 1000 * 60 * 60 * 12, | |
| 102 | |
| 103 // Factor by which the waiting time is multiplied. | |
| 104 2, | |
| 105 | |
| 106 // Fuzzing percentage; this spreads delays randomly between 80% and 100% | |
| 107 // of the calculated time. | |
| 108 0.20, | |
| 109 | |
| 110 // Maximum delay in ms: 12 hours. | |
| 111 1000 * 60 * 60 * 12, | |
| 112 | |
| 113 // When to discard an entry: never. | |
| 114 -1, | |
| 115 | |
| 116 // |always_use_initial_delay|; false means that the initial delay is | |
| 117 // applied after the first error, and starts backing off from there. | |
| 118 false, | |
| 119 }; | |
| 120 | |
| 121 // Maximum number of retries for requests that aren't likely to get a | |
| 122 // different response (e.g. HTTP 4xx replies). | |
| 123 const int kMaxLimitedRetries = 3; | |
| 124 | |
| 125 } // namespace | 32 } // namespace |
| 126 | 33 |
| 127 // Each FetchJob contains the data about a particular component, and handles | |
| 128 // the downloading of its corresponding data. These objects are owned by the | |
| 129 // updater, and the updater always outlives FetchJobs. | |
| 130 // A FetchJob can be scheduled for a retry later, but its data never changes. | |
| 131 // If the ExternalPolicyData for a particular component changes then a new | |
| 132 // FetchJob is created, and the previous one is discarded. | |
| 133 class ComponentCloudPolicyUpdater::FetchJob | |
| 134 : public base::SupportsWeakPtr<FetchJob>, | |
| 135 public net::URLFetcherDelegate { | |
| 136 public: | |
| 137 FetchJob(ComponentCloudPolicyUpdater* updater, | |
| 138 const PolicyNamespace& ns, | |
| 139 const std::string& serialized_response, | |
| 140 const em::ExternalPolicyData& data); | |
| 141 virtual ~FetchJob(); | |
| 142 | |
| 143 const PolicyNamespace& policy_namespace() const { return ns_; } | |
| 144 | |
| 145 // Returns true if |other| equals |data_|. | |
| 146 bool ParamsEquals(const em::ExternalPolicyData& other); | |
| 147 | |
| 148 void StartJob(); | |
| 149 | |
| 150 // URLFetcherDelegate implementation: | |
| 151 virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; | |
| 152 virtual void OnURLFetchDownloadProgress(const net::URLFetcher* source, | |
| 153 int64 current, | |
| 154 int64 total) OVERRIDE; | |
| 155 | |
| 156 private: | |
| 157 void OnSucceeded(); | |
| 158 void OnFailed(net::BackoffEntry* backoff_entry); | |
| 159 void Schedule(); | |
| 160 | |
| 161 // Always valid as long as |this| is alive. | |
| 162 ComponentCloudPolicyUpdater* updater_; | |
| 163 | |
| 164 const PolicyNamespace ns_; | |
| 165 const std::string serialized_response_; | |
| 166 const em::ExternalPolicyData data_; | |
| 167 | |
| 168 // If |fetcher_| exists then |this| is the current job, and must call either | |
| 169 // OnSucceeded or OnFailed. | |
| 170 scoped_ptr<net::URLFetcher> fetcher_; | |
| 171 | |
| 172 // Some errors should trigger a limited number of retries, even with backoff. | |
| 173 // This counts the number of such retries, to stop retrying once the limit | |
| 174 // is reached. | |
| 175 int limited_retries_count_; | |
| 176 | |
| 177 // Various delays to retry a failed download, depending on the failure reason. | |
| 178 net::BackoffEntry retry_soon_entry_; | |
| 179 net::BackoffEntry retry_later_entry_; | |
| 180 net::BackoffEntry retry_much_later_entry_; | |
| 181 | |
| 182 DISALLOW_COPY_AND_ASSIGN(FetchJob); | |
| 183 }; | |
| 184 | |
| 185 ComponentCloudPolicyUpdater::FetchJob::FetchJob( | |
| 186 ComponentCloudPolicyUpdater* updater, | |
| 187 const PolicyNamespace& ns, | |
| 188 const std::string& serialized_response, | |
| 189 const em::ExternalPolicyData& data) | |
| 190 : updater_(updater), | |
| 191 ns_(ns), | |
| 192 serialized_response_(serialized_response), | |
| 193 data_(data), | |
| 194 limited_retries_count_(0), | |
| 195 retry_soon_entry_(&kRetrySoonPolicy), | |
| 196 retry_later_entry_(&kRetryLaterPolicy), | |
| 197 retry_much_later_entry_(&kRetryMuchLaterPolicy) {} | |
| 198 | |
| 199 ComponentCloudPolicyUpdater::FetchJob::~FetchJob() { | |
| 200 if (fetcher_) { | |
| 201 fetcher_.reset(); | |
| 202 // This is the current job; inform the updater that it was cancelled. | |
| 203 updater_->OnJobFailed(this); | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 bool ComponentCloudPolicyUpdater::FetchJob::ParamsEquals( | |
| 208 const em::ExternalPolicyData& other) { | |
| 209 return data_.download_url() == other.download_url() && | |
| 210 data_.secure_hash() == other.secure_hash() && | |
| 211 data_.download_auth_method() == other.download_auth_method(); | |
| 212 } | |
| 213 | |
| 214 void ComponentCloudPolicyUpdater::FetchJob::StartJob() { | |
| 215 fetcher_.reset(net::URLFetcher::Create( | |
| 216 0, GURL(data_.download_url()), net::URLFetcher::GET, this)); | |
| 217 fetcher_->SetRequestContext(updater_->request_context_); | |
| 218 fetcher_->SetLoadFlags(net::LOAD_BYPASS_CACHE | | |
| 219 net::LOAD_DISABLE_CACHE | | |
| 220 net::LOAD_DO_NOT_SAVE_COOKIES | | |
| 221 net::LOAD_IS_DOWNLOAD | | |
| 222 net::LOAD_DO_NOT_SEND_COOKIES | | |
| 223 net::LOAD_DO_NOT_SEND_AUTH_DATA); | |
| 224 fetcher_->SetAutomaticallyRetryOnNetworkChanges(3); | |
| 225 fetcher_->Start(); | |
| 226 } | |
| 227 | |
| 228 void ComponentCloudPolicyUpdater::FetchJob::OnURLFetchComplete( | |
| 229 const net::URLFetcher* source) { | |
| 230 DCHECK(source == fetcher_.get()); | |
| 231 | |
| 232 const net::URLRequestStatus status = source->GetStatus(); | |
| 233 if (status.status() != net::URLRequestStatus::SUCCESS) { | |
| 234 if (status.error() == net::ERR_CONNECTION_RESET || | |
| 235 status.error() == net::ERR_TEMPORARILY_THROTTLED) { | |
| 236 // The connection was interrupted; try again soon. | |
| 237 OnFailed(&retry_soon_entry_); | |
| 238 return; | |
| 239 } else { | |
| 240 // Other network error; try again later. | |
| 241 OnFailed(&retry_later_entry_); | |
| 242 return; | |
| 243 } | |
| 244 } else { | |
| 245 // Status is success; inspect the HTTP response code. | |
| 246 if (source->GetResponseCode() >= 500) { | |
| 247 // Problem at the server; try again soon. | |
| 248 OnFailed(&retry_soon_entry_); | |
| 249 return; | |
| 250 } else if (source->GetResponseCode() >= 400) { | |
| 251 // Client error; this is unlikely to go away. Retry later, and give up | |
| 252 // retrying after 3 attempts. | |
| 253 OnFailed(limited_retries_count_ < kMaxLimitedRetries ? &retry_later_entry_ | |
| 254 : NULL); | |
| 255 limited_retries_count_++; | |
| 256 return; | |
| 257 } else if (source->GetResponseCode() != 200) { | |
| 258 // Other HTTP failure; try again later. | |
| 259 OnFailed(&retry_later_entry_); | |
| 260 return; | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 std::string data; | |
| 265 if (!source->GetResponseAsString(&data) || | |
| 266 static_cast<int64>(data.size()) > kPolicyDataMaxSize || | |
| 267 !updater_->store_->Store( | |
| 268 ns_, serialized_response_, data_.secure_hash(), data)) { | |
| 269 // Failed to retrieve |data|, or it exceeds the size limit, or it failed | |
| 270 // validation. This may be a temporary error at the download URL. | |
| 271 OnFailed(&retry_much_later_entry_); | |
| 272 return; | |
| 273 } | |
| 274 | |
| 275 OnSucceeded(); | |
| 276 } | |
| 277 | |
| 278 void ComponentCloudPolicyUpdater::FetchJob::OnURLFetchDownloadProgress( | |
| 279 const net::URLFetcher* source, | |
| 280 int64 current, | |
| 281 int64 total) { | |
| 282 DCHECK(source == fetcher_.get()); | |
| 283 // Reject the data if it exceeds the size limit. The content length is in | |
| 284 // |total|, and it may be -1 when not known. | |
| 285 if (current > kPolicyDataMaxSize || total > kPolicyDataMaxSize) | |
| 286 OnFailed(&retry_much_later_entry_); | |
| 287 } | |
| 288 | |
| 289 void ComponentCloudPolicyUpdater::FetchJob::OnSucceeded() { | |
| 290 fetcher_.reset(); | |
| 291 updater_->OnJobSucceeded(this); | |
| 292 } | |
| 293 | |
| 294 void ComponentCloudPolicyUpdater::FetchJob::OnFailed(net::BackoffEntry* entry) { | |
| 295 fetcher_.reset(); | |
| 296 | |
| 297 if (entry) { | |
| 298 entry->InformOfRequest(false); | |
| 299 | |
| 300 // If new ExternalPolicyData for this component is fetched then this job | |
| 301 // will be deleted, and the retry task is invalidated. A new job using the | |
| 302 // new data will be scheduled immediately in that case. | |
| 303 updater_->task_runner_->PostDelayedTask( | |
| 304 FROM_HERE, | |
| 305 base::Bind(&FetchJob::Schedule, AsWeakPtr()), | |
| 306 entry->GetTimeUntilRelease()); | |
| 307 } | |
| 308 | |
| 309 updater_->OnJobFailed(this); | |
| 310 } | |
| 311 | |
| 312 void ComponentCloudPolicyUpdater::FetchJob::Schedule() { | |
| 313 updater_->ScheduleJob(this); | |
| 314 } | |
| 315 | |
| 316 ComponentCloudPolicyUpdater::ComponentCloudPolicyUpdater( | 34 ComponentCloudPolicyUpdater::ComponentCloudPolicyUpdater( |
| 317 scoped_refptr<base::SequencedTaskRunner> task_runner, | 35 scoped_refptr<base::SequencedTaskRunner> task_runner, |
| 318 scoped_refptr<net::URLRequestContextGetter> request_context, | 36 scoped_refptr<net::URLRequestContextGetter> request_context, |
| 319 ComponentCloudPolicyStore* store) | 37 ComponentCloudPolicyStore* store) |
| 320 : task_runner_(task_runner), | 38 : store_(store), |
| 321 request_context_(request_context), | 39 external_policy_data_updater_(task_runner, request_context, 1) { |
|
Joao da Silva
2013/04/08 14:49:40
Come to think of it, I guess 2 or 3 parallel downl
bartfab (slow)
2013/04/08 15:32:12
Done. Updated the tests accordingly as well.
| |
| 322 store_(store), | |
| 323 shutting_down_(false) { | |
| 324 DCHECK(task_runner_->RunsTasksOnCurrentThread()); | |
| 325 } | |
| 326 | |
| 327 ComponentCloudPolicyUpdater::~ComponentCloudPolicyUpdater() { | |
| 328 DCHECK(CalledOnValidThread()); | |
| 329 shutting_down_ = true; | |
| 330 STLDeleteValues(&fetch_jobs_); | |
| 331 } | 40 } |
| 332 | 41 |
| 333 void ComponentCloudPolicyUpdater::UpdateExternalPolicy( | 42 void ComponentCloudPolicyUpdater::UpdateExternalPolicy( |
| 334 scoped_ptr<em::PolicyFetchResponse> response) { | 43 scoped_ptr<em::PolicyFetchResponse> response) { |
| 335 DCHECK(CalledOnValidThread()); | |
| 336 | 44 |
|
Joao da Silva
2013/04/08 14:49:40
nit: remove this newline
bartfab (slow)
2013/04/08 15:32:12
Done.
| |
| 337 // Keep a serialized copy of |response|, to cache it later. | 45 // Keep a serialized copy of |response|, to cache it later. |
| 338 // The policy is also rejected if it exceeds the maximum size. | 46 // The policy is also rejected if it exceeds the maximum size. |
| 339 std::string serialized_response; | 47 std::string serialized_response; |
| 340 if (!response->SerializeToString(&serialized_response) || | 48 if (!response->SerializeToString(&serialized_response) || |
| 341 serialized_response.size() > kPolicyProtoMaxSize) { | 49 serialized_response.size() > kPolicyProtoMaxSize) { |
| 342 return; | 50 return; |
| 343 } | 51 } |
| 344 | 52 |
| 345 // Validate the policy before doing anything else. | 53 // Validate the policy before doing anything else. |
| 346 PolicyNamespace ns; | 54 PolicyNamespace ns; |
| 347 em::ExternalPolicyData data; | 55 em::ExternalPolicyData data; |
| 348 if (!store_->ValidatePolicy(response.Pass(), &ns, &data)) { | 56 if (!store_->ValidatePolicy(response.Pass(), &ns, &data)) { |
| 349 LOG(ERROR) << "Failed to validate component policy fetched from DMServer"; | 57 LOG(ERROR) << "Failed to validate component policy fetched from DMServer"; |
| 350 return; | 58 return; |
| 351 } | 59 } |
| 352 | 60 |
| 353 // Maybe the data for this hash has already been downloaded and cached. | 61 // Maybe the data for this hash has already been downloaded and cached. |
| 354 if (data.has_secure_hash() && | 62 const std::string cached_hash = store_->GetCachedHash(ns); |
|
Joao da Silva
2013/04/08 14:49:40
This can be a const ref
bartfab (slow)
2013/04/08 15:32:12
Done.
| |
| 355 data.secure_hash() == store_->GetCachedHash(ns)) { | 63 if (!cached_hash.empty() && data.secure_hash() == cached_hash) { |
| 356 return; | 64 return; |
| 357 } | 65 } |
| 358 | 66 |
| 359 // TODO(joaodasilva): implement the other two auth methods. | 67 // TODO(joaodasilva): implement the other two auth methods. |
| 360 if (data.download_auth_method() != em::ExternalPolicyData::NONE) | 68 if (data.download_auth_method() != em::ExternalPolicyData::NONE) |
| 361 return; | 69 return; |
| 362 | 70 |
| 363 // Check for an existing job for this component. | 71 // Encode |ns| into a string |key|. |
| 364 FetchJob* job = fetch_jobs_[ns]; | 72 const std::string domain = base::IntToString(ns.domain); |
| 365 if (job) { | 73 const std::string key = |
| 366 // Check if this data has already been seen. | 74 base::IntToString(domain.size()) + ":" + domain + ":" + ns.component_id; |
| 367 if (job->ParamsEquals(data)) | |
| 368 return; | |
| 369 | 75 |
| 370 // The existing job is obsolete, cancel it. If |job| is in the job queue | 76 if (data.download_url().empty() || !data.has_secure_hash()) { |
| 371 // then its WeakPtr will be invalided and skipped in the next StartNextJob. | 77 // If there is no policy for this component or the policy has been removed, |
| 372 // If |job| is the current job then it will immediately call OnJobFailed. | 78 // cancel any existing request to fetch policy for this component. |
| 373 delete job; | 79 external_policy_data_updater_.CancelExternalDataFetch(key); |
| 374 fetch_jobs_.erase(ns); | |
| 375 } | |
| 376 | 80 |
| 377 if (data.download_url().empty()) { | 81 // Delete any existing policy for this component. |
| 378 // There is no policy for this component, or the policy has been removed. | |
| 379 store_->Delete(ns); | 82 store_->Delete(ns); |
| 380 } else { | 83 } else { |
| 381 // Start a new job with the new or updated data. | 84 // Make a request to fetch policy for this component. If another fetch |
| 382 job = new FetchJob(this, ns, serialized_response, data); | 85 // request is already pending for the component, it will be canceled. |
| 383 fetch_jobs_[ns] = job; | 86 external_policy_data_updater_.FetchExternalData( |
| 384 ScheduleJob(job); | 87 key, |
| 88 ExternalPolicyDataUpdater::Request(data.download_url(), | |
| 89 data.secure_hash(), | |
| 90 kPolicyDataMaxSize), | |
| 91 base::Bind(&ComponentCloudPolicyStore::Store, base::Unretained(store_), | |
| 92 ns, | |
| 93 serialized_response, | |
| 94 data.secure_hash())); | |
|
Joao da Silva
2013/04/08 14:49:40
Super cool that this works :-)
bartfab (slow)
2013/04/08 15:32:12
That's why I fixed the other CL to use a "const st
| |
| 385 } | 95 } |
| 386 } | 96 } |
| 387 | 97 |
| 388 void ComponentCloudPolicyUpdater::ScheduleJob(FetchJob* job) { | |
| 389 job_queue_.push(job->AsWeakPtr()); | |
| 390 // The job at the front of the queue is always the current job. If |job| is | |
| 391 // at the front then start it immediately. An invalid job is never at the | |
| 392 // front; as soon as it becomes invalidated it will call OnJobFailed() and | |
| 393 // flush the queue. | |
| 394 if (job == job_queue_.front().get()) | |
| 395 StartNextJob(); | |
| 396 } | |
| 397 | |
| 398 void ComponentCloudPolicyUpdater::StartNextJob() { | |
| 399 // Some of the jobs may have been invalidated, and have to be skipped. | |
| 400 while (!job_queue_.empty() && !job_queue_.front()) | |
| 401 job_queue_.pop(); | |
| 402 | |
| 403 // A started job will always call OnJobSucceeded or OnJobFailed. | |
| 404 if (!job_queue_.empty() && !shutting_down_) | |
| 405 job_queue_.front()->StartJob(); | |
| 406 } | |
| 407 | |
| 408 void ComponentCloudPolicyUpdater::OnJobSucceeded(FetchJob* job) { | |
| 409 DCHECK(fetch_jobs_[job->policy_namespace()] == job); | |
| 410 DCHECK(!job_queue_.empty() && job_queue_.front() == job); | |
| 411 fetch_jobs_.erase(job->policy_namespace()); | |
| 412 delete job; | |
| 413 job_queue_.pop(); | |
| 414 StartNextJob(); | |
| 415 } | |
| 416 | |
| 417 void ComponentCloudPolicyUpdater::OnJobFailed(FetchJob* job) { | |
| 418 DCHECK(fetch_jobs_[job->policy_namespace()] == job); | |
| 419 DCHECK(!job_queue_.empty() && job_queue_.front() == job); | |
| 420 // The job isn't deleted when it fails because a retry attempt may have been | |
| 421 // scheduled. It's also kept so that UpdateExternalPolicy() can see the | |
| 422 // current data, and avoid a new fetch if the data hasn't changed. | |
| 423 job_queue_.pop(); | |
| 424 StartNextJob(); | |
| 425 } | |
| 426 | |
| 427 } // namespace policy | 98 } // namespace policy |
| OLD | NEW |