]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/dird/ua_prune.c
Make new prune algo to work with backup
[bacula/bacula] / bacula / src / dird / ua_prune.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2002-2009 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation and included
11    in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of Kern Sibbald.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28 /*
29  *
30  *   Bacula Director -- User Agent Database prune Command
31  *      Applies retention periods
32  *
33  *     Kern Sibbald, February MMII
34  *
35  */
36
37 #include "bacula.h"
38 #include "dird.h"
39
40 /* Imported functions */
41
42 /* Forward referenced functions */
43 static bool grow_del_list(struct del_ctx *del);
44
45 /*
46  * Called here to count entries to be deleted
47  */
48 int del_count_handler(void *ctx, int num_fields, char **row)
49 {
50    struct s_count_ctx *cnt = (struct s_count_ctx *)ctx;
51
52    if (row[0]) {
53       cnt->count = str_to_int64(row[0]);
54    } else {
55       cnt->count = 0;
56    }
57    return 0;
58 }
59
60
61 /*
62  * Called here to make in memory list of JobIds to be
63  *  deleted and the associated PurgedFiles flag.
64  *  The in memory list will then be transversed
65  *  to issue the SQL DELETE commands.  Note, the list
66  *  is allowed to get to MAX_DEL_LIST_LEN to limit the
67  *  maximum malloc'ed memory.
68  */
69 int job_delete_handler(void *ctx, int num_fields, char **row)
70 {
71    struct del_ctx *del = (struct del_ctx *)ctx;
72
73    if (!grow_del_list(del)) {
74       return 1;
75    }
76    del->JobId[del->num_ids] = (JobId_t)str_to_int64(row[0]);
77    Dmsg2(0, "job_delete_handler row=%d val=%d\n", del->num_ids, del->JobId[del->num_ids]);
78    del->PurgedFiles[del->num_ids++] = (char)str_to_int64(row[1]);
79    return 0;
80 }
81
82 int file_delete_handler(void *ctx, int num_fields, char **row)
83 {
84    struct del_ctx *del = (struct del_ctx *)ctx;
85
86    if (!grow_del_list(del)) {
87       return 1;
88    }
89    del->JobId[del->num_ids++] = (JobId_t)str_to_int64(row[0]);
90 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
91    return 0;
92 }
93
94 /*
95  *   Prune records from database
96  *
97  *    prune files (from) client=xxx [pool=yyy]
98  *    prune jobs (from) client=xxx [pool=yyy]
99  *    prune volume=xxx
100  *    prune stats
101  */
102 int prunecmd(UAContext *ua, const char *cmd)
103 {
104    DIRRES *dir;
105    CLIENT *client;
106    POOL *pool;
107    POOL_DBR pr;
108    MEDIA_DBR mr;
109    utime_t retention;
110    int kw;
111
112    static const char *keywords[] = {
113       NT_("Files"),
114       NT_("Jobs"),
115       NT_("Volume"),
116       NT_("Stats"),
117       NULL};
118
119    if (!open_client_db(ua)) {
120       return false;
121    }
122
123    /* First search args */
124    kw = find_arg_keyword(ua, keywords);
125    if (kw < 0 || kw > 3) {
126       /* no args, so ask user */
127       kw = do_keyword_prompt(ua, _("Choose item to prune"), keywords);
128    }
129
130    switch (kw) {
131    case 0:  /* prune files */
132       client = get_client_resource(ua);
133       if (find_arg_with_value(ua, "pool") >= 0) {
134          pool = get_pool_resource(ua);
135       } else {
136          pool = NULL;
137       }
138       /* Pool File Retention takes precedence over client File Retention */
139       if (pool && pool->FileRetention > 0) {
140          if (!confirm_retention(ua, &pool->FileRetention, "File")) {
141             return false;
142          }
143       } else if (!client || !confirm_retention(ua, &client->FileRetention, "File")) {
144          return false;
145       }
146       prune_files(ua, client, pool);
147       return true;
148    case 1:  /* prune jobs */
149       client = get_client_resource(ua);
150       if (find_arg_with_value(ua, "pool") >= 0) {
151          pool = get_pool_resource(ua);
152       } else {
153          pool = NULL;
154       }
155       /* Pool Job Retention takes precedence over client Job Retention */
156       if (pool && pool->JobRetention > 0) {
157          if (!confirm_retention(ua, &pool->JobRetention, "Job")) {
158             return false;
159          }
160       } else if (!client || !confirm_retention(ua, &client->JobRetention, "Job")) {
161          return false;
162       }
163       /* ****FIXME**** allow user to select JobType */
164       prune_jobs(ua, client, pool, JT_BACKUP);
165       return 1;
166    case 2:  /* prune volume */
167       if (!select_pool_and_media_dbr(ua, &pr, &mr)) {
168          return false;
169       }
170       if (mr.Enabled == 2) {
171          ua->error_msg(_("Cannot prune Volume \"%s\" because it is archived.\n"),
172             mr.VolumeName);
173          return false;
174       }
175       if (!confirm_retention(ua, &mr.VolRetention, "Volume")) {
176          return false;
177       }
178       prune_volume(ua, &mr);
179       return true;
180    case 3:  /* prune stats */
181       dir = (DIRRES *)GetNextRes(R_DIRECTOR, NULL);
182       if (!dir->stats_retention) {
183          return false;
184       }
185       retention = dir->stats_retention;
186       if (!confirm_retention(ua, &retention, "Statistics")) {
187          return false;
188       }
189       prune_stats(ua, retention);
190       return true;
191    default:
192       break;
193    }
194
195    return true;
196 }
197
198 /* Prune Job stat records from the database. 
199  *
200  */
201 int prune_stats(UAContext *ua, utime_t retention)
202 {
203    char ed1[50];
204    POOL_MEM query(PM_MESSAGE);
205    utime_t now = (utime_t)time(NULL);
206
207    db_lock(ua->db);
208    Mmsg(query, "DELETE FROM JobHisto WHERE JobTDate < %s", 
209         edit_int64(now - retention, ed1));
210    db_sql_query(ua->db, query.c_str(), NULL, NULL);
211    db_unlock(ua->db);
212
213    ua->info_msg(_("Pruned Jobs from JobHisto catalog.\n"));
214
215    return true;
216 }
217
218 /*
219  * Prune File records from the database. For any Job which
220  * is older than the retention period, we unconditionally delete
221  * all File records for that Job.  This is simple enough that no
222  * temporary tables are needed. We simply make an in memory list of
223  * the JobIds meeting the prune conditions, then delete all File records
224  * pointing to each of those JobIds.
225  *
226  * This routine assumes you want the pruning to be done. All checking
227  *  must be done before calling this routine.
228  *
229  * Note: pool can possibly be NULL.
230  */
231 int prune_files(UAContext *ua, CLIENT *client, POOL *pool)
232 {
233    struct del_ctx del;
234    struct s_count_ctx cnt;
235    POOL_MEM query(PM_MESSAGE);
236    utime_t now, period;
237    CLIENT_DBR cr;
238    char ed1[50], ed2[50];
239
240    db_lock(ua->db);
241    memset(&cr, 0, sizeof(cr));
242    memset(&del, 0, sizeof(del));
243    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
244    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
245       db_unlock(ua->db);
246       return 0;
247    }
248
249    if (pool && pool->FileRetention > 0) {
250       period = pool->FileRetention;
251    } else {
252       period = client->FileRetention;
253    }
254    now = (utime_t)time(NULL);
255
256 //   edit_utime(now-period, ed1, sizeof(ed1));
257 //   Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s secs.\n"), ed1);
258    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs.\n"));
259    /* Select Jobs -- for counting */ 
260    edit_int64(now - period, ed1);
261    Mmsg(query, count_select_job, ed1, edit_int64(cr.ClientId, ed2));
262    Dmsg3(050, "select now=%u period=%u sql=%s\n", (uint32_t)now, 
263                (uint32_t)period, query.c_str());
264    cnt.count = 0;
265    if (!db_sql_query(ua->db, query.c_str(), del_count_handler, (void *)&cnt)) {
266       ua->error_msg("%s", db_strerror(ua->db));
267       Dmsg0(050, "Count failed\n");
268       goto bail_out;
269    }
270
271    if (cnt.count == 0) {
272       if (ua->verbose) {
273          ua->warning_msg(_("No Files found to prune.\n"));
274       }
275       goto bail_out;
276    }
277
278    if (cnt.count < MAX_DEL_LIST_LEN) {
279       del.max_ids = cnt.count + 1;
280    } else {
281       del.max_ids = MAX_DEL_LIST_LEN;
282    }
283    del.tot_ids = 0;
284
285    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
286
287    /* Now process same set but making a delete list */
288    Mmsg(query, select_job, edit_int64(now - period, ed1), 
289         edit_int64(cr.ClientId, ed2));
290    db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)&del);
291
292    purge_files_from_job_list(ua, del);
293
294    edit_uint64_with_commas(del.num_del, ed1);
295    ua->info_msg(_("Pruned Files from %s Jobs for client %s from catalog.\n"),
296       ed1, client->name());
297
298 bail_out:
299    db_unlock(ua->db);
300    if (del.JobId) {
301       free(del.JobId);
302    }
303    return 1;
304 }
305
306
307 static void drop_temp_tables(UAContext *ua)
308 {
309    int i;
310    for (i=0; drop_deltabs[i]; i++) {
311       db_sql_query(ua->db, drop_deltabs[i], NULL, (void *)NULL);
312    }
313 }
314
315 static bool create_temp_tables(UAContext *ua)
316 {
317    /* Create temp tables and indicies */
318    if (!db_sql_query(ua->db, create_deltabs[db_type], NULL, (void *)NULL)) {
319       ua->error_msg("%s", db_strerror(ua->db));
320       Dmsg0(050, "create DelTables table failed\n");
321       return false;
322    }
323    if (!db_sql_query(ua->db, create_delindex, NULL, (void *)NULL)) {
324        ua->error_msg("%s", db_strerror(ua->db));
325        Dmsg0(050, "create DelInx1 index failed\n");
326        return false;
327    }
328    return true;
329 }
330
331 static bool grow_del_list(struct del_ctx *del)
332 {
333    if (del->num_ids == MAX_DEL_LIST_LEN) {
334       return false;
335    }
336
337    if (del->num_ids == del->max_ids) {
338       del->max_ids = (del->max_ids * 3) / 2;
339       del->JobId = (JobId_t *)brealloc(del->JobId, sizeof(JobId_t) *
340          del->max_ids);
341       del->PurgedFiles = (char *)brealloc(del->PurgedFiles, del->max_ids);
342    }
343    return true;
344 }
345
346 struct verify_ctx {
347    DBId_t ClientId;                   /* Id of client */
348    DBId_t FileSetId;                  /* Id of FileSet */ 
349 };
350
351 /* row: Job.Name, FileSet, Client.Name, FileSetId, ClientId */
352 static int job_select_handler(void *ctx, int num_fields, char **row)
353 {
354    alist *lst = (alist *)ctx;
355    struct verify_ctx *res;
356
357    if (num_fields != 5) {
358       return 1;
359    }
360
361    /* If this job doesn't exist anymore in the configuration, delete it */
362    if (GetResWithName(R_JOB, row[0]) == NULL) {
363       return 1;
364    }
365
366    /* If this fileset doesn't exist anymore in the configuration, delete it */
367    if (GetResWithName(R_FILESET, row[1]) == NULL) {
368       return 1;
369    }
370
371    /* If this client doesn't exist anymore in the configuration, delete it */
372    if (GetResWithName(R_CLIENT, row[2]) == NULL) {
373       return 1;
374    }
375
376    res = (struct verify_ctx*) malloc(sizeof(struct verify_ctx));
377    res->FileSetId = str_to_int64(row[3]);
378    res->ClientId = str_to_int64(row[4]);
379    lst->append(res);
380
381 // Dmsg2(150, "row=%d val=%d\n", del->num_ids-1, del->JobId[del->num_ids-1]);
382    return 0;
383 }
384
385 static void dump_del(struct del_ctx *del)
386 {
387    POOL_MEM query;
388    for (int i=0; del->num_ids; ) {
389       pm_strcpy(query, "");
390       for (int j=0; j<1000 && del->num_ids>0; j++) {
391          del->num_ids--;
392          if (del->JobId[i] == 0 || ua->jcr->JobId == del->JobId[i]) {
393             Dmsg2(0, "skip JobId[%d]=%d\n", i, (int)del->JobId[i]);
394             i++;
395             continue;
396          }
397          if (*query.c_str() != 0) {
398             pm_strcat(query, ",");
399          }
400          pm_strcat(query, edit_int64(del->JobId[i++], ed1));
401          del->num_del++;
402       }
403       Dmsg1(0, "num_ids=%d\n", del->num_ids);
404       Dmsg1(0, "purge_jobs_from_catalog %s\n", query.c_str());
405    }
406 }
407
408 /*
409  * Pruning Jobs is a bit more complicated than purging Files
410  * because we delete Job records only if there is a more current
411  * backup of the FileSet. Otherwise, we keep the Job record.
412  * In other words, we never delete the only Job record that
413  * contains a current backup of a FileSet. This prevents the
414  * Volume from being recycled and destroying a current backup.
415  *
416  * For Verify Jobs, we do not delete the last InitCatalog.
417  *
418  * For Restore Jobs there are no restrictions.
419  */
420 int prune_jobs(UAContext *ua, CLIENT *client, POOL *pool, int JobType)
421 {
422    struct del_ctx del;
423    POOL_MEM query(PM_MESSAGE);
424    utime_t now, period;
425    CLIENT_DBR cr;
426    char ed1[50], ed2[50];
427    alist *jobids_check=NULL;
428    struct verify_ctx *elt;
429    db_list_ctx jobids;
430    JCR *jcr;
431
432    db_lock(ua->db);
433    memset(&cr, 0, sizeof(cr));
434
435    bstrncpy(cr.Name, client->name(), sizeof(cr.Name));
436    if (!db_create_client_record(ua->jcr, ua->db, &cr)) {
437       db_unlock(ua->db);
438       return 0;
439    }
440
441    if (pool && pool->JobRetention > 0) {
442       period = pool->JobRetention;
443    } else {
444       period = client->JobRetention;
445    }
446    now = (utime_t)time(NULL);
447
448    /* Drop any previous temporary tables still there */
449    drop_temp_tables(ua);
450
451    /* Create temp tables and indicies */
452    if (!create_temp_tables(ua)) {
453       goto bail_out;
454    }
455
456    edit_utime(period, ed1, sizeof(ed1));
457    Jmsg(ua->jcr, M_INFO, 0, _("Begin pruning Jobs older than %s.\n"), ed1);
458
459    edit_int64(now - period, ed1); /* Jobs older than ed1 are good candidates */
460    edit_int64(cr.ClientId, ed2);
461
462    memset(&del, 0, sizeof(del));
463    del.max_ids = 100;
464    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
465    del.PurgedFiles = (char *)malloc(del.max_ids);
466
467    /* Prune garbage jobs (JobStatus not successful) */
468    Mmsg(query, 
469    "SELECT JobId, PurgedFiles FROM Job "
470     "WHERE ( JobFiles=0 "
471          "OR JobStatus NOT IN ('T', 'W') "
472           ") "
473       "AND JobTDate < %s "
474       "AND ClientId = %s ",
475         ed1, ed2);
476    
477    Dmsg1(150, "Query=%s\n", query.c_str());
478    if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
479       ua->error_msg("%s", db_strerror(ua->db));
480    }
481
482    /* Prune Admin, Restore, Copy and Migration jobs */
483    Mmsg(query, 
484    "SELECT JobId, PurgedFiles FROM Job "
485     "WHERE Type IN ('D', 'R', 'c', 'm') "
486       "AND JobTDate < %s "
487       "AND ClientId = %s ",
488         ed1, ed2);
489    
490    Dmsg1(150, "Query=%s\n", query.c_str());
491    if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
492       ua->error_msg("%s", db_strerror(ua->db));
493    }
494
495    /*
496     * Select all files that are older than the JobRetention period
497     *  and stuff them into the "DeletionCandidates" table.
498     */
499    Mmsg(query, insert_delcand, (char)JobType, ed1, 
500         edit_int64(cr.ClientId, ed2));
501    if (!db_sql_query(ua->db, query.c_str(), NULL, (void *)NULL)) {
502       if (ua->verbose) {
503          ua->error_msg("%s", db_strerror(ua->db));
504       }
505       Dmsg0(050, "insert delcand failed\n");
506       goto bail_out;
507    }
508
509    /* Now, for the selection, we discard some of them in order to be always
510     * able to restore files. (ie, last full, last diff, last incr)
511     */
512    jobids_check = New(alist(10, owned_by_alist));
513    Mmsg(query, 
514         "SELECT DISTINCT Job.Name, FileSet, Client.Name, Job.FileSetId, Job.ClientId "
515           "FROM DelCandidates "
516                "JOIN Job USING (JobId) "
517                "JOIN Client USING (ClientId) "
518                "JOIN FileSet ON (Job.FileSetId = FileSet.FileSetId) "
519       );
520
521    if (!db_sql_query(ua->db, query.c_str(), job_select_handler, jobids_check)) {
522       ua->error_msg("%s", db_strerror(ua->db));
523    }
524
525    /* For all jobs of this client, we exclude current jobs used for restore or
526     * accurate. This will prevent to prune the last full backup used for
527     * current backup & restore
528     */
529    jcr = ua->jcr;
530    jcr->jr.JobLevel = L_INCREMENTAL; /* To find useful jobs, we do like an incremental */
531
532    foreach_alist(elt, jobids_check) {
533       jcr->jr.ClientId = elt->ClientId;
534       jcr->jr.FileSetId = elt->FileSetId;
535       db_accurate_get_jobids(jcr, ua->db, &jcr->jr, &jobids);
536    }
537
538    if (jobids.count > 0) {
539       Dmsg1(0, "jobids to exclude before basejobs = %s\n", jobids.list);
540       /* We also need to exclude all basejobs used */
541       db_get_used_base_jobids(jcr, ua->db, jobids.list, &jobids);
542
543       Mmsg(query, "DELETE FROM DelCandidates WHERE JobId IN (%s)", jobids.list);
544       if (!db_sql_query(ua->db, query.c_str(), NULL, NULL)) {
545          ua->error_msg("%s", db_strerror(ua->db));
546       }
547       Dmsg1(0, "jobids to exclude = %s\n", jobids.list);
548    }
549
550    Mmsg(query, "SELECT DelCandidates.JobId,DelCandidates.PurgedFiles FROM DelCandidates");
551    Dmsg1(150, "Query=%s\n", query.c_str());
552    if (!db_sql_query(ua->db, query.c_str(), job_delete_handler, (void *)&del)) {
553       ua->error_msg("%s", db_strerror(ua->db));
554    }
555
556    dump_del(&del);
557 //   purge_job_list_from_catalog(ua, del);
558
559    if (del.num_del > 0) {
560       ua->info_msg(_("Pruned %d %s for client %s from catalog.\n"), del.num_del,
561          del.num_del==1?_("Job"):_("Jobs"), client->name());
562     } else if (ua->verbose) {
563        ua->info_msg(_("No Jobs found to prune.\n"));
564     }
565
566 bail_out:
567    drop_temp_tables(ua);
568    db_unlock(ua->db);
569    if (del.JobId) {
570       free(del.JobId);
571    }
572    if (del.PurgedFiles) {
573       free(del.PurgedFiles);
574    }
575    if (jobids_check) {
576       delete jobids_check;
577    }
578    return 1;
579 }
580
581 /*
582  * Prune a given Volume
583  */
584 bool prune_volume(UAContext *ua, MEDIA_DBR *mr)
585 {
586    POOL_MEM query(PM_MESSAGE);
587    struct del_ctx del;
588    bool ok = false;
589    int count;
590
591    if (mr->Enabled == 2) {
592       return false;                   /* Cannot prune archived volumes */
593    }
594
595    memset(&del, 0, sizeof(del));
596    del.max_ids = 10000;
597    del.JobId = (JobId_t *)malloc(sizeof(JobId_t) * del.max_ids);
598
599    db_lock(ua->db);
600
601    /* Prune only Volumes with status "Full", or "Used" */
602    if (strcmp(mr->VolStatus, "Full")   == 0 ||
603        strcmp(mr->VolStatus, "Used")   == 0) {
604       Dmsg2(050, "get prune list MediaId=%d Volume %s\n", (int)mr->MediaId, mr->VolumeName);
605       count = get_prune_list_for_volume(ua, mr, &del);
606       Dmsg1(050, "Num pruned = %d\n", count);
607       if (count != 0) {
608          purge_job_list_from_catalog(ua, del);
609       }
610       ok = is_volume_purged(ua, mr);
611    }
612
613    db_unlock(ua->db);
614    if (del.JobId) {
615       free(del.JobId);
616    }
617    return ok;
618 }
619
620 /*
621  * Get prune list for a volume
622  */
623 int get_prune_list_for_volume(UAContext *ua, MEDIA_DBR *mr, del_ctx *del)
624 {
625    POOL_MEM query(PM_MESSAGE);
626    int count = 0;
627    utime_t now, period;
628    char ed1[50], ed2[50];
629
630    if (mr->Enabled == 2) {
631       return 0;                    /* cannot prune Archived volumes */
632    }
633
634    /*
635     * Now add to the  list of JobIds for Jobs written to this Volume
636     */
637    edit_int64(mr->MediaId, ed1); 
638    period = mr->VolRetention;
639    now = (utime_t)time(NULL);
640    edit_int64(now-period, ed2);
641    Mmsg(query, sel_JobMedia, ed1, ed2);
642    Dmsg3(250, "Now=%d period=%d now-period=%s\n", (int)now, (int)period,
643       ed2);
644
645    Dmsg1(050, "Query=%s\n", query.c_str());
646    if (!db_sql_query(ua->db, query.c_str(), file_delete_handler, (void *)del)) {
647       if (ua->verbose) {
648          ua->error_msg("%s", db_strerror(ua->db));
649       }
650       Dmsg0(050, "Count failed\n");
651       goto bail_out;
652    }
653    count = exclude_running_jobs_from_list(del);
654    
655 bail_out:
656    return count;
657 }
658
659 /*
660  * We have a list of jobs to prune or purge. If any of them is
661  *   currently running, we set its JobId to zero which effectively
662  *   excludes it.
663  *
664  * Returns the number of jobs that can be prunned or purged.
665  *
666  */
667 int exclude_running_jobs_from_list(del_ctx *prune_list)
668 {
669    int count = 0;
670    JCR *jcr;
671    bool skip;
672    int i;          
673
674    /* Do not prune any job currently running */
675    for (i=0; i < prune_list->num_ids; i++) {
676       skip = false;
677       foreach_jcr(jcr) {
678          if (jcr->JobId == prune_list->JobId[i]) {
679             Dmsg2(050, "skip running job JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
680             prune_list->JobId[i] = 0;
681             skip = true;
682             break;
683          }
684       }
685       endeach_jcr(jcr);
686       if (skip) {
687          continue;  /* don't increment count */
688       }
689       Dmsg2(050, "accept JobId[%d]=%d\n", i, (int)prune_list->JobId[i]);
690       count++;
691    }
692    return count;
693 }