summaryrefslogtreecommitdiff
path: root/src/threads/BackgroundJob.vala
blob: 5d259e7dd2560a6797c0662e02db12e4b8e82584 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

// This callback is executed when an associated BackgroundJob completes.  It is called from within
// the Gtk event loop, *not* the background thread's context.
public delegate void CompletionCallback(BackgroundJob job);

// This callback is executed when an associated BackgroundJob has been cancelled (via its
// Cancellable).  Note that it's *possible* the BackgroundJob performed some or all of its work
// prior to executing this delegate.
public delegate void CancellationCallback(BackgroundJob job);

// This callback is executed by the BackgroundJob when a unit of work is completed, but not the
// entire job.  It is called from within the Gtk event loop, *not* the background thread's
// context.
//
// Note that there does not seem to be any guarantees of order in the Idle queue documentation,
// and this it's possible (and, depending on assigned priorities, likely) that notifications could
// arrive in different orders, and even after the CompletionCallback.  Thus, no guarantee of
// ordering is made here.
//
// NOTE: Would like Value to be nullable, but can't due to this bug:
// https://bugzilla.gnome.org/show_bug.cgi?id=607098
//
// NOTE: There will be a memory leak using NotificationCallbacks due to this bug:
// https://bugzilla.gnome.org/show_bug.cgi?id=571264
//
// NOTE: Because of these two bugs, using an abstract base class rather than Value.  When both are
// fixed (at least the second), may consider going back to Value.

public abstract class NotificationObject {
}

public abstract class InterlockedNotificationObject : NotificationObject {
    private Semaphore semaphore = new Semaphore();
    
    // Only called by BackgroundJob; no need for users or subclasses to use
    public void internal_wait_for_completion() {
        semaphore.wait();
    }
    
    // Only called by BackgroundJob; no need for users or subclasses to use
    public void internal_completed() {
        semaphore.notify();
    }
}

public delegate void NotificationCallback(BackgroundJob job, NotificationObject? user);

// This abstract class represents a unit of work that can be executed within a background thread's
// context.  If specified, the job may be cancellable (which can be checked by execute() and the
// worker thread prior to calling execute()).  The BackgroundJob may also specify a
// CompletionCallback and/or a CancellationCallback to be executed within Gtk's event loop.
// A BackgroundJob may also emit NotificationCallbacks, all of which are also executed within
// Gtk's event loop.
//
// The BackgroundJob may be constructed with a reference to its "owner".  This is not used directly
// by BackgroundJob or Worker, but merely exists to hold a reference to the Object that is receiving
// the various callbacks from BackgroundJob.  Without this, it's possible for the object creating
// BackgroundJobs to be freed before all the callbacks have been received, or even during a callback,
// which is an unstable situation.
public abstract class BackgroundJob {
    public enum JobPriority {
        HIGHEST = 100,
        HIGH = 75,
        NORMAL = 50,
        LOW = 25,
        LOWEST = 0;
        
        // Returns negative if this is higher, zero if equal, positive if this is lower
        public int compare(JobPriority other) {
            return (int) other - (int) this;
        }
        
        public static int compare_func(JobPriority a, JobPriority b) {
            return (int) b - (int) a;
        }
    }
    
    private class NotificationJob {
        public unowned NotificationCallback callback;
        public BackgroundJob background_job;
        public NotificationObject? user;
        
        public NotificationJob(NotificationCallback callback, BackgroundJob background_job,
            NotificationObject? user) {
            this.callback = callback;
            this.background_job = background_job;
            this.user = user;
        }
    }
    
    private static Gee.ArrayList<NotificationJob> notify_queue = new Gee.ArrayList<NotificationJob>();
    
    private Object owner;
    private unowned CompletionCallback callback;
    private Cancellable cancellable;
    private unowned CancellationCallback cancellation;
    private BackgroundJob self = null;
    private AbstractSemaphore semaphore = null;
    
    // The thinking here is that there is exactly one CompletionCallback per job, and the caller
    // probably wants to know that to set off UI and other events in response.  There are several
    // (possibly hundreds or thousands) or notifications, and thus should arrive in a more
    // controlled way (to avoid locking up the UI, for example).  This has ramifications about
    // the order in which completion and notifications arrive (see above note).
    private int completion_priority = Priority.HIGH;
    private int notification_priority = Priority.DEFAULT_IDLE;
    
    public BackgroundJob(Object? owner = null, CompletionCallback? callback = null,
        Cancellable? cancellable = null, CancellationCallback? cancellation = null,
        AbstractSemaphore? completion_semaphore = null) {
        this.owner = owner;
        this.callback = callback;
        this.cancellable = cancellable;
        this.cancellation = cancellation;
        this.semaphore = completion_semaphore;
    }
    
    public abstract void execute();
    
    public virtual JobPriority get_priority() {
        return JobPriority.NORMAL;
    }
    
    // For the CompareFunc delegate, according to JobPriority.
    public static int priority_compare_func(BackgroundJob a, BackgroundJob b) {
        return a.get_priority().compare(b.get_priority());
    }
    
    // For the Comparator delegate, according to JobPriority.
    public static int64 priority_comparator(void *a, void *b) {
        return priority_compare_func((BackgroundJob) a, (BackgroundJob) b);
    }
    
    // This method is not thread-safe.  Best to set priority before the job is enqueued.
    public void set_completion_priority(int priority) {
        completion_priority = priority;
    }
    
    // This method is not thread-safe.  Best to set priority before the job is enqueued.
    public void set_notification_priority(int priority) {
        notification_priority = priority;
    }
    
    // This method is thread-safe, but only waits if a completion semaphore has been set, otherwise
    // exits immediately.  Note that blocking for a semaphore does NOT spin the event loop, so a
    // thread relying on it to continue should not use this.
    public void wait_for_completion() {
        if (semaphore != null)
            semaphore.wait();
    }
    
    public Cancellable? get_cancellable() {
        return cancellable;
    }
    
    public bool is_cancelled() {
        return (cancellable != null) ? cancellable.is_cancelled() : false;
    }
    
    public void cancel() {
        if (cancellable != null)
            cancellable.cancel();
    }
    
    // This should only be called by Workers.  Beware to all who fail to heed.
    public void internal_notify_completion() {
        if (semaphore != null)
            semaphore.notify();
        
        if (callback == null && cancellation == null)
            return;
        
        if (is_cancelled() && cancellation == null)
            return;
        
        // Because Idle doesn't maintain a ref count of the job, and it's going to be dropped by
        // the worker thread soon, need to maintain a ref until the completion callback is made
        self = this;
        
        Idle.add_full(completion_priority, on_notify_completion);
    }
    
    private bool on_notify_completion() {
        // it's still possible the caller cancelled this operation during or after the execute()
        // method was called ... since the completion work can be costly for a job that was
        // already cancelled, and the caller might've dropped all references to the job by now,
        // only notify completion in this context if not cancelled
        if (is_cancelled()) {
            if (cancellation != null)
                cancellation(this);
        } else {
            if (callback != null)
                callback(this);
        }
        
        // drop the ref so this object can be freed ... must not touch "this" after this point
        self = null;
        
        return false;
    }
    
    // This call may be executed by the child class during execute() to inform of a unit of
    // work being completed
    protected void notify(NotificationCallback callback, NotificationObject? user) {
        lock (notify_queue) {
            notify_queue.add(new NotificationJob(callback, this, user));
        }
        
        Idle.add_full(notification_priority, on_notification_ready);
        
        // If an interlocked notification, block until the main thread completes the notification
        // callback
        InterlockedNotificationObject? interlocked = user as InterlockedNotificationObject;
        if (interlocked != null)
            interlocked.internal_wait_for_completion();
    }
    
    private bool on_notification_ready() {
        // this is called once for every notification added, so there should always be something
        // waiting for us
        NotificationJob? notification_job = null;
        lock (notify_queue) {
            if (notify_queue.size > 0)
                notification_job = notify_queue.remove_at(0);
        }
        assert(notification_job != null);
        
        notification_job.callback(notification_job.background_job, notification_job.user);
        
        // Release the blocked thread waiting for this notification to complete
        InterlockedNotificationObject? interlocked = notification_job.user as InterlockedNotificationObject;
        if (interlocked != null)
            interlocked.internal_completed();
        
        return false;
    }
}