Skip to content

Commit

Permalink
fix: Prevent Chokidar events processing issues (#2325)
Browse files Browse the repository at this point in the history
  • Loading branch information
taratatach authored Oct 5, 2023
2 parents 73ecd82 + 5b845f2 commit 2a8f345
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 123 deletions.
23 changes: 23 additions & 0 deletions .github/actions/setup-dnsmasq/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Setup cozy-stack
author: Erwan Guyader
description: Setup dnsmasq for .localhost domains on macOS
runs:
using: composite
steps:
- name: Install dnsmasq
shell: bash
run: brew install dnsmasq

- name: Add address entry to point .localhost to 127.0.0.1
shell: bash
run: echo "address=/.localhost/127.0.0.1" >> "$(brew --prefix)"/etc/dnsmasq.conf

- name: Start dnsmasq
shell: bash
run: sudo brew services start dnsmasq

- name: Create resolver configuration
shell: bash
run: |
sudo mkdir -p /etc/resolver
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/localhost
6 changes: 6 additions & 0 deletions .github/workflows/macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ jobs:
uses: ./.github/actions/setup-cozy-stack
with:
couchdb-url: ${{ steps.setup-couchdb.outputs.couchdb-url }}
- name: Setup DNS resolution for .localhost
uses: ./.github/actions/setup-dnsmasq
- name: Setup local env
env:
COZY_DESKTOP_FS: ${{ matrix.fs }}
Expand Down Expand Up @@ -151,6 +153,8 @@ jobs:
uses: ./.github/actions/setup-cozy-stack
with:
couchdb-url: ${{ steps.setup-couchdb.outputs.couchdb-url }}
- name: Setup DNS resolution for .localhost
uses: ./.github/actions/setup-dnsmasq
- name: Setup local env
env:
COZY_DESKTOP_FS: ${{ matrix.fs }}
Expand Down Expand Up @@ -209,6 +213,8 @@ jobs:
uses: ./.github/actions/setup-cozy-stack
with:
couchdb-url: ${{ steps.setup-couchdb.outputs.couchdb-url }}
- name: Setup DNS resolution for .localhost
uses: ./.github/actions/setup-dnsmasq
- name: Setup local env
env:
COZY_DESKTOP_FS: ${{ matrix.fs }}
Expand Down
4 changes: 2 additions & 2 deletions core/local/chokidar/analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ module.exports = function analysis(
{
pendingChanges,
initialScanParams
} /*: { pendingChanges: LocalChange[], initialScanParams: ?InitialScanParams } */
} /*: { pendingChanges: LocalChange[], initialScanParams: InitialScanParams } */
) /*: LocalChange[] */ {
const changes /*: LocalChange[] */ = analyseEvents(events, pendingChanges)
fixUnsyncedMoves(changes)
sortBeforeSquash(changes)
squashMoves(changes)
sortChanges(changes, initialScanParams != null)
sortChanges(changes, !initialScanParams.done)
return separatePendingChanges(changes, pendingChanges)
}

Expand Down
35 changes: 30 additions & 5 deletions core/local/chokidar/event_buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type FlushCallback<EventType> = (EventType[]) => any
*/
class EventBuffer /*:: <EventType> */ {
/*::
locked: boolean
events: EventType[]
mode: EventBufferMode
timeoutInMs: number
Expand All @@ -38,6 +39,7 @@ class EventBuffer /*:: <EventType> */ {
timeoutInMs /*: number */,
flushed /*: FlushCallback<EventType> */
) {
this.locked = false
this.events = []
this.mode = 'idle'
this.timeoutInMs = timeoutInMs
Expand All @@ -47,6 +49,18 @@ class EventBuffer /*:: <EventType> */ {
autoBind(this)
}

lock() {
if (this.locked) {
throw new Error('lock unavailable')
}

this.locked = true
}

unlock() {
this.locked = false
}

push(event /*: EventType */) /*: void */ {
this.events.push(event)
this.shiftTimeout()
Expand All @@ -72,11 +86,22 @@ class EventBuffer /*:: <EventType> */ {
}

async flush() {
this.clearTimeout()
if (this.events.length > 0) {
const flushedEvents = this.events
this.events = []
return this.flushed(flushedEvents)
try {
this.lock()
} catch (err) {
this.shiftTimeout()
return
}

try {
this.clearTimeout()
if (this.events.length > 0) {
const flushedEvents = this.events
this.events = []
await this.flushed(flushedEvents)
}
} finally {
this.unlock()
}
}

Expand Down
8 changes: 5 additions & 3 deletions core/local/chokidar/initial_scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export type InitialScanParams = {
paths: string[],
emptyDirRetryCount: number,
flushed: boolean,
resolve: () => void
done: boolean,
resolve?: () => void
}
export type InitialScanOpts = {
Expand Down Expand Up @@ -68,9 +69,10 @@ const step = async (
rawEvents /*: ChokidarEvent[] */,
{ buffer, initialScanParams, pouch } /*: InitialScanOpts */
) /*: Promise<Array<ChokidarEvent>> */ => {
// We mark the initial scan as flushed as soon as possible to avoid
// concurrent initial scan processings from later flushes.
// We mark the initial scan as flushed as soon as possible so latter events
// are not marked as part of the initial scan.
initialScanParams.flushed = true

let events = rawEvents

events
Expand Down
2 changes: 1 addition & 1 deletion core/local/chokidar/prepare_events.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type { Pouch } from '../../pouch'
type PrepareEventsOpts = {
+checksum: (string) => Promise<string>,
initialScanParams: ?InitialScanParams,
initialScanParams: InitialScanParams,
pouch: Pouch,
syncPath: string
}
Expand Down
69 changes: 40 additions & 29 deletions core/local/chokidar/watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class LocalWatcher {
prep: Prep
pouch: Pouch
events: EventEmitter
initialScanParams: ?InitialScanParams
initialScanParams: InitialScanParams
checksumer: Checksumer
watcher: any // chokidar
buffer: LocalEventBuffer<ChokidarEvent>
Expand All @@ -93,6 +93,16 @@ class LocalWatcher {
this.checksumer = checksumer.init()
this.pendingChanges = []

// To detect which files&folders have been removed since the last run of
// cozy-desktop, we keep all the paths seen by chokidar during its
// initial scan in @paths to compare them with pouchdb database.
this.initialScanParams = {
paths: [],
emptyDirRetryCount: 3,
flushed: false,
done: false
}

// XXX: this.onFlush must be bound before being passed to LocalEventBuffer
autoBind(this)

Expand All @@ -115,6 +125,8 @@ class LocalWatcher {
start() {
log.debug('Starting...')

this.resetInitialScanParams()

this.watcher = chokidar.watch('.', {
// Let paths in events be relative to this base path
cwd: this.syncPath,
Expand All @@ -140,6 +152,8 @@ class LocalWatcher {
})

const started = new Promise(resolve => {
this.initialScanParams.resolve = resolve

for (let eventType of [
'add',
'addDir',
Expand All @@ -150,8 +164,7 @@ class LocalWatcher {
this.watcher.on(
eventType,
(path /*: ?string */, stats /*: ?fs.Stats */) => {
const isInitialScan =
this.initialScanParams && !this.initialScanParams.flushed
const isInitialScan = !this.initialScanParams.flushed
log.chokidar.debug({ path, stats, isInitialScan }, eventType)
const newEvent = chokidarEvent.build(eventType, path, stats)
if (newEvent.type !== eventType) {
Expand All @@ -166,16 +179,6 @@ class LocalWatcher {
)
}

// To detect which files&folders have been removed since the last run of
// cozy-desktop, we keep all the paths seen by chokidar during its
// initial scan in @paths to compare them with pouchdb database.
this.initialScanParams = {
paths: [],
emptyDirRetryCount: 3,
resolve,
flushed: false
}

this.watcher
.on('ready', () => this.buffer.switchMode('timeout'))
.on('raw', async (event, path, details) => {
Expand Down Expand Up @@ -219,20 +222,14 @@ class LocalWatcher {
this.events.emit('local-start')

let events = rawEvents.filter(hasPath) // @TODO handle root dir events
// We need to destructure `this` otherwise Flow won't detect that
// `this.initialScanParams` is not null even within the conditional block.
const { buffer, pouch, initialScanParams } = this
if (initialScanParams != null && !initialScanParams.flushed) {
events = await initialScan.step(events, {
initialScanParams,
buffer,
pouch
})

if (!this.initialScanParams.flushed) {
events = await initialScan.step(events, this)
}

if (events.length === 0) {
this.events.emit('local-end')
if (this.initialScanParams != null) this.initialScanParams.resolve()
this.endInitialScan()
return
}

Expand Down Expand Up @@ -263,18 +260,16 @@ class LocalWatcher {
release()
this.events.emit('local-end')
}
if (this.initialScanParams != null) {
this.initialScanParams.resolve()
this.initialScanParams = null
}

this.endInitialScan()
}

async stop(force /*: ?bool */ = false) {
log.debug('Stopping watcher...')

if (!this.watcher) return

if (force) {
if (force || !this.initialScanParams.flushed) {
// Drop buffered events
this.buffer.clear()
} else {
Expand All @@ -294,7 +289,7 @@ class LocalWatcher {
// Stop underlying Chokidar watcher
await this.watcher.close()
this.watcher = null
// Stop accepting new events
// Flush buffer and stop flushes loop
this.buffer.switchMode('idle')

if (!force) {
Expand All @@ -305,6 +300,22 @@ class LocalWatcher {
}
}

resetInitialScanParams() {
this.initialScanParams = {
paths: [],
emptyDirRetryCount: 3,
flushed: false,
done: false
}
}

endInitialScan() {
if (this.initialScanParams.resolve) {
this.initialScanParams.done = true
this.initialScanParams.resolve()
}
}

/* Helpers */
async checksum(filePath /*: string */) /*: Promise<string> */ {
const absPath = path.join(this.syncPath, filePath)
Expand Down
2 changes: 2 additions & 0 deletions test/scenarios/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ async function runLocalChokidarWithCaptures(
helpers.local.syncDir.abspath,
eventsFile
)
// XXX: Run initial scan
await helpers.local.scan()
}

const inodeChanges = await runActions(
Expand Down
11 changes: 10 additions & 1 deletion test/support/helpers/pouch.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ module.exports = {

async cleanDatabase() {
if (this.pouch && this.pouch.db) {
await this.pouch.db.destroy()
let retry = 0
do {
try {
await this.pouch.db.destroy()
break
} catch (err) {
retry++
await Promise.delay(500 * 2 ** retry)
}
} while (retry < 3)
}
this.pouch = null
},
Expand Down
Loading

0 comments on commit 2a8f345

Please sign in to comment.