Projet

Général

Profil

Paste
Télécharger (21,8 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / feeds / includes / FeedsSource.inc @ 2c8c2b87

1
<?php
2

    
3
/**
4
 * @file
5
 * Definition of FeedsSourceInterface and FeedsSource class.
6
 */
7

    
8
/**
9
 * Distinguish exceptions occuring when handling locks.
10
 */
11
class FeedsLockException extends Exception {}
12

    
13
/**
14
 * Denote a import or clearing stage. Used for multi page processing.
15
 */
16
define('FEEDS_START', 'start_time');
17
define('FEEDS_FETCH', 'fetch');
18
define('FEEDS_PARSE', 'parse');
19
define('FEEDS_PROCESS', 'process');
20
define('FEEDS_PROCESS_CLEAR', 'process_clear');
21

    
22
/**
23
 * Declares an interface for a class that defines default values and form
24
 * descriptions for a FeedSource.
25
 */
26
interface FeedsSourceInterface {
27

    
28
  /**
29
   * Crutch: for ease of use, we implement FeedsSourceInterface for every
30
   * plugin, but then we need to have a handle which plugin actually implements
31
   * a source.
32
   *
33
   * @see FeedsPlugin class.
34
   *
35
   * @return
36
   *   TRUE if a plugin handles source specific configuration, FALSE otherwise.
37
   */
38
  public function hasSourceConfig();
39

    
40
  /**
41
   * Return an associative array of default values.
42
   */
43
  public function sourceDefaults();
44

    
45
  /**
46
   * Return a Form API form array that defines a form configuring values. Keys
47
   * correspond to the keys of the return value of sourceDefaults().
48
   */
49
  public function sourceForm($source_config);
50

    
51
  /**
52
   * Validate user entered values submitted by sourceForm().
53
   */
54
  public function sourceFormValidate(&$source_config);
55

    
56
  /**
57
   * A source is being saved.
58
   */
59
  public function sourceSave(FeedsSource $source);
60

    
61
  /**
62
   * A source is being deleted.
63
   */
64
  public function sourceDelete(FeedsSource $source);
65
}
66

    
67
/**
68
 * Status of an import or clearing operation on a source.
69
 */
70
class FeedsState {
71
  /**
72
   * Floating point number denoting the progress made. 0.0 meaning no progress
73
   * 1.0 = FEEDS_BATCH_COMPLETE meaning finished.
74
   */
75
  public $progress;
76

    
77
  /**
78
   * Used as a pointer to store where left off. Must be serializable.
79
   */
80
  public $pointer;
81

    
82
  /**
83
   * Natural numbers denoting more details about the progress being made.
84
   */
85
  public $total;
86
  public $created;
87
  public $updated;
88
  public $deleted;
89
  public $skipped;
90
  public $failed;
91

    
92
  /**
93
   * Constructor, initialize variables.
94
   */
95
  public function __construct() {
96
    $this->progress = FEEDS_BATCH_COMPLETE;
97
    $this->total =
98
    $this->created =
99
    $this->updated =
100
    $this->deleted =
101
    $this->skipped =
102
    $this->failed = 0;
103
  }
104

    
105
  /**
106
   * Safely report progress.
107
   *
108
   * When $total == $progress, the state of the task tracked by this state is
109
   * regarded to be complete.
110
   *
111
   * Handles the following cases gracefully:
112
   *
113
   * - $total is 0
114
   * - $progress is larger than $total
115
   * - $progress approximates $total so that $finished rounds to 1.0
116
   *
117
   * @param $total
118
   *   A natural number that is the total to be worked off.
119
   * @param $progress
120
   *   A natural number that is the progress made on $total.
121
   */
122
  public function progress($total, $progress) {
123
    if ($progress > $total) {
124
      $this->progress = FEEDS_BATCH_COMPLETE;
125
    }
126
    elseif ($total) {
127
      $this->progress = $progress / $total;
128
      if ($this->progress == FEEDS_BATCH_COMPLETE && $total != $progress) {
129
        $this->progress = 0.99;
130
      }
131
    }
132
    else {
133
      $this->progress = FEEDS_BATCH_COMPLETE;
134
    }
135
  }
136
}
137

    
138
/**
139
 * This class encapsulates a source of a feed. It stores where the feed can be
140
 * found and how to import it.
141
 *
142
 * Information on how to import a feed is encapsulated in a FeedsImporter object
143
 * which is identified by the common id of the FeedsSource and the
144
 * FeedsImporter. More than one FeedsSource can use the same FeedsImporter
145
 * therefore a FeedsImporter never holds a pointer to a FeedsSource object, nor
146
 * does it hold any other information for a particular FeedsSource object.
147
 *
148
 * Classes extending FeedsPlugin can implement a sourceForm to expose
149
 * configuration for a FeedsSource object. This is for instance how FeedsFetcher
150
 * exposes a text field for a feed URL or how FeedsCSVParser exposes a select
151
 * field for choosing between colon or semicolon delimiters.
152
 *
153
 * It is important that a FeedsPlugin does not directly hold information about
154
 * a source but leave all storage up to FeedsSource. An instance of a
155
 * FeedsPlugin class only exists once per FeedsImporter configuration, while an
156
 * instance of a FeedsSource class exists once per feed_nid to be imported.
157
 *
158
 * As with FeedsImporter, the idea with FeedsSource is that it can be used
159
 * without actually saving the object to the database.
160
 */
161
class FeedsSource extends FeedsConfigurable {
162

    
163
  // Contains the node id of the feed this source info object is attached to.
164
  // Equals 0 if not attached to any node - i. e. if used on a
165
  // standalone import form within Feeds or by other API users.
166
  protected $feed_nid;
167

    
168
  // The FeedsImporter object that this source is expected to be used with.
169
  protected $importer;
170

    
171
  // A FeedsSourceState object holding the current import/clearing state of this
172
  // source.
173
  protected $state;
174

    
175
  // Fetcher result, used to cache fetcher result when batching.
176
  protected $fetcher_result;
177

    
178
  // Timestamp when this source was imported the last time.
179
  protected $imported;
180

    
181
  /**
182
   * Instantiate a unique object per class/id/feed_nid. Don't use
183
   * directly, use feeds_source() instead.
184
   */
185
  public static function instance($importer_id, $feed_nid) {
186
    $class = variable_get('feeds_source_class', 'FeedsSource');
187
    static $instances = array();
188
    if (!isset($instances[$class][$importer_id][$feed_nid])) {
189
      $instances[$class][$importer_id][$feed_nid] = new $class($importer_id, $feed_nid);
190
    }
191
    return $instances[$class][$importer_id][$feed_nid];
192
  }
193

    
194
  /**
195
   * Constructor.
196
   */
197
  protected function __construct($importer_id, $feed_nid) {
198
    $this->feed_nid = $feed_nid;
199
    $this->importer = feeds_importer($importer_id);
200
    parent::__construct($importer_id);
201
    $this->load();
202
  }
203

    
204
  /**
205
   * Returns the FeedsImporter object that this source is expected to be used with.
206
   */
207
  public function importer() {
208
    return $this->importer;
209
  }
210

    
211
  /**
212
   * Preview = fetch and parse a feed.
213
   *
214
   * @return
215
   *   FeedsParserResult object.
216
   *
217
   * @throws
218
   *   Throws Exception if an error occurs when fetching or parsing.
219
   */
220
  public function preview() {
221
    $result = $this->importer->fetcher->fetch($this);
222
    $result = $this->importer->parser->parse($this, $result);
223
    module_invoke_all('feeds_after_parse', $this, $result);
224
    return $result;
225
  }
226

    
227
  /**
228
   * Start importing a source.
229
   *
230
   * This method starts an import job. Depending on the configuration of the
231
   * importer of this source, a Batch API job or a background job with Job
232
   * Scheduler will be created.
233
   *
234
   * @throws Exception
235
   *   If processing in background is enabled, the first batch chunk of the
236
   *   import will be executed on the current page request. This means that this
237
   *   method may throw the same exceptions as FeedsSource::import().
238
   */
239
  public function startImport() {
240
    module_invoke_all('feeds_before_import', $this);
241
    $config = $this->importer->getConfig();
242
    if ($config['process_in_background']) {
243
      $this->startBackgroundJob('import');
244
    }
245
    else {
246
      $this->startBatchAPIJob(t('Importing'), 'import');
247
    }
248
  }
249

    
250
  /**
251
   * Start deleting all imported items of a source.
252
   *
253
   * This method starts a clear job. Depending on the configuration of the
254
   * importer of this source, a Batch API job or a background job with Job
255
   * Scheduler will be created.
256
   *
257
   * @throws Exception
258
   *   If processing in background is enabled, the first batch chunk of the
259
   *   clear task will be executed on the current page request. This means that
260
   *   this method may throw the same exceptions as FeedsSource::clear().
261
   */
262
  public function startClear() {
263
    $config = $this->importer->getConfig();
264
    if ($config['process_in_background']) {
265
      $this->startBackgroundJob('clear');
266
    }
267
    else {
268
      $this->startBatchAPIJob(t('Deleting'), 'clear');
269
    }
270
  }
271

    
272
  /**
273
   * Schedule all periodic tasks for this source.
274
   */
275
  public function schedule() {
276
    $this->scheduleImport();
277
  }
278

    
279
  /**
280
   * Schedule periodic or background import tasks.
281
   */
282
  public function scheduleImport() {
283
    // Check whether any fetcher is overriding the import period.
284
    $period = $this->importer->config['import_period'];
285
    $fetcher_period = $this->importer->fetcher->importPeriod($this);
286
    if (is_numeric($fetcher_period)) {
287
      $period = $fetcher_period;
288
    }
289
    $period = $this->progressImporting() === FEEDS_BATCH_COMPLETE ? $period : 0;
290
    $job = array(
291
      'type' => $this->id,
292
      'id' => $this->feed_nid,
293
      // Schedule as soon as possible if a batch is active.
294
      'period' => $period,
295
      'periodic' => TRUE,
296
    );
297
    if ($period != FEEDS_SCHEDULE_NEVER) {
298
      JobScheduler::get('feeds_source_import')->set($job);
299
    }
300
    else {
301
      JobScheduler::get('feeds_source_import')->remove($job);
302
    }
303
  }
304

    
305
  /**
306
   * Schedule background clearing tasks.
307
   */
308
  public function scheduleClear() {
309
    $job = array(
310
      'type' => $this->id,
311
      'id' => $this->feed_nid,
312
      'period' => 0,
313
      'periodic' => TRUE,
314
    );
315
    // Remove job if batch is complete.
316
    if ($this->progressClearing() === FEEDS_BATCH_COMPLETE) {
317
      JobScheduler::get('feeds_source_clear')->remove($job);
318
    }
319
    // Schedule as soon as possible if batch is not complete.
320
    else {
321
      JobScheduler::get('feeds_source_clear')->set($job);
322
    }
323
  }
324

    
325
  /**
326
   * Import a source: execute fetching, parsing and processing stage.
327
   *
328
   * This method only executes the current batch chunk, then returns. If you are
329
   * looking to import an entire source, use FeedsSource::startImport() instead.
330
   *
331
   * @return
332
   *   FEEDS_BATCH_COMPLETE if the import process finished. A decimal between
333
   *   0.0 and 0.9 periodic if import is still in progress.
334
   *
335
   * @throws
336
   *   Throws Exception if an error occurs when importing.
337
   */
338
  public function import() {
339
    $this->acquireLock();
340
    try {
341
      // If fetcher result is empty, we are starting a new import, log.
342
      if (empty($this->fetcher_result)) {
343
        $this->state[FEEDS_START] = time();
344
      }
345

    
346
      // Fetch.
347
      if (empty($this->fetcher_result) || FEEDS_BATCH_COMPLETE == $this->progressParsing()) {
348
        $this->fetcher_result = $this->importer->fetcher->fetch($this);
349
        // Clean the parser's state, we are parsing an entirely new file.
350
        unset($this->state[FEEDS_PARSE]);
351
      }
352

    
353
      // Parse.
354
      $parser_result = $this->importer->parser->parse($this, $this->fetcher_result);
355
      module_invoke_all('feeds_after_parse', $this, $parser_result);
356

    
357
      // Process.
358
      $this->importer->processor->process($this, $parser_result);
359
    }
360
    catch (Exception $e) {
361
      // Do nothing.
362
    }
363
    $this->releaseLock();
364

    
365
    // Clean up.
366
    $result = $this->progressImporting();
367
    if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
368
      $this->imported = time();
369
      $this->log('import', 'Imported in !s s', array('!s' => $this->imported - $this->state[FEEDS_START]), WATCHDOG_INFO);
370
      module_invoke_all('feeds_after_import', $this);
371
      unset($this->fetcher_result, $this->state);
372
    }
373
    $this->save();
374
    if (isset($e)) {
375
      throw $e;
376
    }
377
    return $result;
378
  }
379

    
380
  /**
381
   * Remove all items from a feed.
382
   *
383
   * This method only executes the current batch chunk, then returns. If you are
384
   * looking to delete all items of a source, use FeedsSource::startClear()
385
   * instead.
386
   *
387
   * @return
388
   *   FEEDS_BATCH_COMPLETE if the clearing process finished. A decimal between
389
   *   0.0 and 0.9 periodic if clearing is still in progress.
390
   *
391
   * @throws
392
   *   Throws Exception if an error occurs when clearing.
393
   */
394
  public function clear() {
395
    $this->acquireLock();
396
    try {
397
      $this->importer->fetcher->clear($this);
398
      $this->importer->parser->clear($this);
399
      $this->importer->processor->clear($this);
400
    }
401
    catch (Exception $e) {
402
      // Do nothing.
403
    }
404
    $this->releaseLock();
405

    
406
    // Clean up.
407
    $result = $this->progressClearing();
408
    if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
409
      module_invoke_all('feeds_after_clear', $this);
410
      unset($this->state);
411
    }
412
    $this->save();
413
    if (isset($e)) {
414
      throw $e;
415
    }
416
    return $result;
417
  }
418

    
419
  /**
420
   * Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
421
   */
422
  public function progressParsing() {
423
    return $this->state(FEEDS_PARSE)->progress;
424
  }
425

    
426
  /**
427
   * Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
428
   */
429
  public function progressImporting() {
430
    $fetcher = $this->state(FEEDS_FETCH);
431
    $parser = $this->state(FEEDS_PARSE);
432
    if ($fetcher->progress == FEEDS_BATCH_COMPLETE && $parser->progress == FEEDS_BATCH_COMPLETE) {
433
      return FEEDS_BATCH_COMPLETE;
434
    }
435
    // Fetching envelops parsing.
436
    // @todo: this assumes all fetchers neatly use total. May not be the case.
437
    $fetcher_fraction = $fetcher->total ? 1.0 / $fetcher->total : 1.0;
438
    $parser_progress = $parser->progress * $fetcher_fraction;
439
    $result = $fetcher->progress - $fetcher_fraction + $parser_progress;
440
    if ($result == FEEDS_BATCH_COMPLETE) {
441
      return 0.99;
442
    }
443
    return $result;
444
  }
445

    
446
  /**
447
   * Report progress on clearing.
448
   */
449
  public function progressClearing() {
450
    return $this->state(FEEDS_PROCESS_CLEAR)->progress;
451
  }
452

    
453
  /**
454
   * Return a state object for a given stage. Lazy instantiates new states.
455
   *
456
   * @todo Rename getConfigFor() accordingly to config().
457
   *
458
   * @param $stage
459
   *   One of FEEDS_FETCH, FEEDS_PARSE, FEEDS_PROCESS or FEEDS_PROCESS_CLEAR.
460
   *
461
   * @return
462
   *   The FeedsState object for the given stage.
463
   */
464
  public function state($stage) {
465
    if (!is_array($this->state)) {
466
      $this->state = array();
467
    }
468
    if (!isset($this->state[$stage])) {
469
      $this->state[$stage] = new FeedsState();
470
    }
471
    return $this->state[$stage];
472
  }
473

    
474
  /**
475
   * Count items imported by this source.
476
   */
477
  public function itemCount() {
478
    return $this->importer->processor->itemCount($this);
479
  }
480

    
481
  /**
482
   * Save configuration.
483
   */
484
  public function save() {
485
    // Alert implementers of FeedsSourceInterface to the fact that we're saving.
486
    foreach ($this->importer->plugin_types as $type) {
487
      $this->importer->$type->sourceSave($this);
488
    }
489
    $config = $this->getConfig();
490

    
491
    // Store the source property of the fetcher in a separate column so that we
492
    // can do fast lookups on it.
493
    $source = '';
494
    if (isset($config[get_class($this->importer->fetcher)]['source'])) {
495
      $source = $config[get_class($this->importer->fetcher)]['source'];
496
    }
497
    $object = array(
498
      'id' => $this->id,
499
      'feed_nid' => $this->feed_nid,
500
      'imported' => $this->imported,
501
      'config' => $config,
502
      'source' => $source,
503
      'state' => isset($this->state) ? $this->state : FALSE,
504
      'fetcher_result' => isset($this->fetcher_result) ? $this->fetcher_result : FALSE,
505
    );
506
    if (db_query_range("SELECT 1 FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", 0, 1, array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchField()) {
507
      drupal_write_record('feeds_source', $object, array('id', 'feed_nid'));
508
    }
509
    else {
510
      drupal_write_record('feeds_source', $object);
511
    }
512
  }
513

    
514
  /**
515
   * Load configuration and unpack.
516
   *
517
   * @todo Patch CTools to move constants from export.inc to ctools.module.
518
   */
519
  public function load() {
520
    if ($record = db_query("SELECT imported, config, state, fetcher_result FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchObject()) {
521
      // While FeedsSource cannot be exported, we still use CTool's export.inc
522
      // export definitions.
523
      ctools_include('export');
524
      $this->export_type = EXPORT_IN_DATABASE;
525
      $this->imported = $record->imported;
526
      $this->config = unserialize($record->config);
527
      if (!empty($record->state)) {
528
        $this->state = unserialize($record->state);
529
      }
530
      if (!is_array($this->state)) {
531
        $this->state = array();
532
      }
533
      if (!empty($record->fetcher_result)) {
534
        $this->fetcher_result = unserialize($record->fetcher_result);
535
      }
536
    }
537
  }
538

    
539
  /**
540
   * Delete configuration. Removes configuration information
541
   * from database, does not delete configuration itself.
542
   */
543
  public function delete() {
544
    // Alert implementers of FeedsSourceInterface to the fact that we're
545
    // deleting.
546
    foreach ($this->importer->plugin_types as $type) {
547
      $this->importer->$type->sourceDelete($this);
548
    }
549
    db_delete('feeds_source')
550
      ->condition('id', $this->id)
551
      ->condition('feed_nid', $this->feed_nid)
552
      ->execute();
553
    // Remove from schedule.
554
    $job = array(
555
      'type' => $this->id,
556
      'id' => $this->feed_nid,
557
    );
558
    JobScheduler::get('feeds_source_import')->remove($job);
559
  }
560

    
561
  /**
562
   * Only return source if configuration is persistent and valid.
563
   *
564
   * @see FeedsConfigurable::existing().
565
   */
566
  public function existing() {
567
    // If there is no feed nid given, there must be no content type specified.
568
    // If there is a feed nid given, there must be a content type specified.
569
    // Ensure that importer is persistent (= defined in code or DB).
570
    // Ensure that source is persistent (= defined in DB).
571
    if ((empty($this->feed_nid) && empty($this->importer->config['content_type'])) ||
572
        (!empty($this->feed_nid) && !empty($this->importer->config['content_type']))) {
573
      $this->importer->existing();
574
      return parent::existing();
575
    }
576
    throw new FeedsNotExistingException(t('Source configuration not valid.'));
577
  }
578

    
579
  /**
580
   * Returns the configuration for a specific client class.
581
   *
582
   * @param FeedsSourceInterface $client
583
   *   An object that is an implementer of FeedsSourceInterface.
584
   *
585
   * @return
586
   *   An array stored for $client.
587
   */
588
  public function getConfigFor(FeedsSourceInterface $client) {
589
    $class = get_class($client);
590
    return isset($this->config[$class]) ? $this->config[$class] : $client->sourceDefaults();
591
  }
592

    
593
  /**
594
   * Sets the configuration for a specific client class.
595
   *
596
   * @param FeedsSourceInterface $client
597
   *   An object that is an implementer of FeedsSourceInterface.
598
   * @param $config
599
   *   The configuration for $client.
600
   *
601
   * @return
602
   *   An array stored for $client.
603
   */
604
  public function setConfigFor(FeedsSourceInterface $client, $config) {
605
    $this->config[get_class($client)] = $config;
606
  }
607

    
608
  /**
609
   * Return defaults for feed configuration.
610
   */
611
  public function configDefaults() {
612
    // Collect information from plugins.
613
    $defaults = array();
614
    foreach ($this->importer->plugin_types as $type) {
615
      if ($this->importer->$type->hasSourceConfig()) {
616
        $defaults[get_class($this->importer->$type)] = $this->importer->$type->sourceDefaults();
617
      }
618
    }
619
    return $defaults;
620
  }
621

    
622
  /**
623
   * Override parent::configForm().
624
   */
625
  public function configForm(&$form_state) {
626
    // Collect information from plugins.
627
    $form = array();
628
    foreach ($this->importer->plugin_types as $type) {
629
      if ($this->importer->$type->hasSourceConfig()) {
630
        $class = get_class($this->importer->$type);
631
        $config = isset($this->config[$class]) ? $this->config[$class] : array();
632
        $form[$class] = $this->importer->$type->sourceForm($config);
633
        $form[$class]['#tree'] = TRUE;
634
      }
635
    }
636
    return $form;
637
  }
638

    
639
  /**
640
   * Override parent::configFormValidate().
641
   */
642
  public function configFormValidate(&$values) {
643
    foreach ($this->importer->plugin_types as $type) {
644
      $class = get_class($this->importer->$type);
645
      if (isset($values[$class]) && $this->importer->$type->hasSourceConfig()) {
646
        $this->importer->$type->sourceFormValidate($values[$class]);
647
      }
648
    }
649
  }
650

    
651
  /**
652
   * Writes to feeds log.
653
   */
654
  public function log($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE) {
655
    feeds_log($this->id, $this->feed_nid, $type, $message, $variables, $severity);
656
  }
657

    
658
  /**
659
   * Background job helper. Starts a background job using Job Scheduler.
660
   *
661
   * Execute the first batch chunk of a background job on the current page load,
662
   * moves the rest of the job processing to a cron powered background job.
663
   *
664
   * Executing the first batch chunk is important, otherwise, when a user
665
   * submits a source for import or clearing, we will leave her without any
666
   * visual indicators of an ongoing job.
667
   *
668
   * @see FeedsSource::startImport().
669
   * @see FeedsSource::startClear().
670
   *
671
   * @param $method
672
   *   Method to execute on importer; one of 'import' or 'clear'.
673
   *
674
   * @throws Exception $e
675
   */
676
  protected function startBackgroundJob($method) {
677
    if (FEEDS_BATCH_COMPLETE != $this->$method()) {
678
      $job = array(
679
        'type' => $this->id,
680
        'id' => $this->feed_nid,
681
        'period' => 0,
682
        'periodic' => FALSE,
683
      );
684
      JobScheduler::get("feeds_source_{$method}")->set($job);
685
    }
686
  }
687

    
688
  /**
689
   * Batch API helper. Starts a Batch API job.
690
   *
691
   * @see FeedsSource::startImport().
692
   * @see FeedsSource::startClear().
693
   * @see feeds_batch()
694
   *
695
   * @param $title
696
   *   Title to show to user when executing batch.
697
   * @param $method
698
   *   Method to execute on importer; one of 'import' or 'clear'.
699
   */
700
  protected function startBatchAPIJob($title, $method) {
701
    $batch = array(
702
      'title' => $title,
703
      'operations' => array(
704
        array('feeds_batch', array($method, $this->id, $this->feed_nid)),
705
      ),
706
      'progress_message' => '',
707
    );
708
    batch_set($batch);
709
  }
710

    
711
  /**
712
   * Acquires a lock for this source.
713
   *
714
   * @throws FeedsLockException
715
   *   If a lock for the requested job could not be acquired.
716
   */
717
  protected function acquireLock() {
718
    if (!lock_acquire("feeds_source_{$this->id}_{$this->feed_nid}", 60.0)) {
719
      throw new FeedsLockException(t('Cannot acquire lock for source @id / @feed_nid.', array('@id' => $this->id, '@feed_nid' => $this->feed_nid)));
720
    }
721
  }
722

    
723
  /**
724
   * Releases a lock for this source.
725
   */
726
  protected function releaseLock() {
727
    lock_release("feeds_source_{$this->id}_{$this->feed_nid}");
728
  }
729
}