felangel / felangel/mason

fix: Mason race condition on hooks compilation prevent simultaneous runs

Open
#1,655 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Dart
Stars
1.1k
Forks
113
PR merge metrics
No merged PRs in 30d

Description

**Description**

The Mason hook compilation is prone to race condition issues when multiple executions of the same brick is executed at the same time and the brick still don't have a cached compilation on the environment yet.

**Steps To Reproduce**

Consider the following small project:

`pubspec.yaml`
```yaml
name: mason_concurrency_repro
description: Minimal reproduction of Mason hook compilation race condition
version: 0.1.0
publish_to: none

environment:
sdk: ^3.0.0

dependencies:
mason: ^0.1.0-dev.60
```

`bin/worker.dart`

```dart
// Worker process that creates a MasonGenerator from a bundled brick with hooks.
// When multiple workers run concurrently, the shared hook cache causes errors.

import 'dart:convert';
import 'dart:io';

import 'package:mason/mason.dart';

/// A simple brick with a pre_gen hook.
/// The hook itself is trivial — the bug is in the cache compilation step.
final _bundle = MasonBundle.fromJson({
'files': [
{
'path': 'output.txt',
'data': base64Encode(utf8.encode('Hello {{name}}!')),
'type': 'text',
},
],
'hooks': [
{
'path': 'pre_gen.dart',
'data': base64Encode(utf8.encode('''
import 'package:mason/mason.dart';

Future run(HookContext context) async {
context.logger.info('Hook running for: \${context.vars['name']}');
}
''')),
'type': 'text',
},
{
'path': 'pubspec.yaml',
'data': base64Encode(utf8.encode('''
name: hooks
publish_to: none

environment:
sdk: ^3.0.0

dependencies:
mason: ^0.1.0-dev.60
''')),
'type': 'text',
},
],
'name': 'repro_brick',
'description': 'A brick to reproduce the concurrency issue',
'version': '0.1.0',
'environment': {'mason': '^0.1.0'},
'vars': {
'name': {
'type': 'string',
'description': 'A name',
'default': 'World',
'prompt': 'Name?',
},
},
});

void main(List args) async {
final index = args.isNotEmpty ? args[0] : '0';
final dir = Directory.systemTemp.createTempSync('mason_repro_$index');

try {
// This is the critical call — MasonGenerator.fromBundle compiles hooks
// into a shared cache. Multiple concurrent calls race on this cache.
final generator = await MasonGenerator.fromBundle(_bundle);
final target = DirectoryGeneratorTarget(dir);
var vars = {'name': 'worker_$index'};

await generator.hooks.preGen(
vars: vars,
onVarsChanged: (v) => vars = v,
workingDirectory: dir.path,
);
await generator.generate(
target,
vars: vars,
fileConflictResolution: FileConflictResolution.overwrite,
);

stderr.writeln('Worker $index: success');
exit(0);
} catch (e) {
stderr.writeln('Worker $index: FAILED - $e');
exit(1);
} finally {
try {
dir.deleteSync(recursive: true);
} catch (_) {}
}
}
```

`bin/repo.dart`

```dart
// Minimal reproduction of Mason hook compilation race condition.
//
// This script demonstrates that when multiple concurrent processes call
// MasonGenerator.fromBundle() on a brick with hooks, the shared hook cache
// causes intermittent "No such file or directory" errors.
//
// This simulates what happens when `melos exec -c 4 -- very_good test` runs
// across multiple packages: each process independently calls
// MasonGenerator.fromBundle() which compiles hooks into a shared cache dir.
//
// See: https://github.com/VeryGoodOpenSource/very_good_cli/issues/947

import 'dart:async';
import 'dart:io';

void main() async {
const workerCount = 10;
const rounds = 5;

print('Mason hook compilation race condition reproduction');
print('=' * 60);
print('Workers per round: $workerCount');
print('Rounds: $rounds');
print('');

// Find the mason cache directory
final home = Platform.environment['HOME'] ?? '';
final masonCache = Directory('$home/.mason-cache');

var totalFailures = 0;

for (var round = 1; round <= rounds; round++) {
print('--- Round $round ---');

// Delete the Mason cache to force all workers to compile hooks
// simultaneously. This simulates the first run or CI clean build.
if (masonCache.existsSync()) {
try {
masonCache.deleteSync(recursive: true);
print(' Cleared Mason cache.');
} catch (e) {
print(' Could not clear cache: $e');
}
}

// Spawn workers as separate processes (like Melos would).
final futures = >[];
for (var i = 0; i < workerCount; i++) {
futures.add(
Process.run(
'dart',
['run', 'bin/worker.dart', '$i'],
),
);
}

final results = await Future.wait(futures);
var roundFailures = 0;
for (var i = 0; i < results.length; i++) {
final result = results[i];
if (result.exitCode != 0) {
roundFailures++;
final stderr = result.stderr.toString().trim();
final lastLine = stderr.split('\n').last;
print(' Worker $i: FAILED (exit ${result.exitCode}) - $lastLine');
}
}

if (roundFailures == 0) {
print(' All $workerCount workers succeeded.');
} else {
print(' $roundFailures/$workerCount workers failed.');
}
totalFailures += roundFailures;
}

print('');
print('=' * 60);
if (totalFailures > 0) {
print('REPRODUCED: $totalFailures total failures across $rounds rounds.');
} else {
print('No failures detected in this run.');
print('The race condition is intermittent — try increasing workerCount');
print('or running on CI where filesystem contention is higher.');
}

exit(totalFailures > 0 ? 1 : 0);
}
```

Running `dart bin/repro.dart` will surface the issues, example:

```
--- Round 5 ---
Cleared Mason cache.
Worker 0: FAILED (exit 1) - Error: "/Users/erickzanardo/.mason-cache/bundled/repro_brick_0.1.0_5c50347778b7dc27b74043aec3a43b0d25ebe05b/hooks/build/hooks/pre_gen/pre_gen_941b41827775cf780a9e35a7f1340748e973bc58.dart" file not found.
Worker 1: FAILED (exit 1) - Bad state: Generating kernel failed!
Worker 2: FAILED (exit 1) - Error: "/Users/erickzanardo/.mason-cache/bundled/repro_brick_0.1.0_5c50347778b7dc27b74043aec3a43b0d25ebe05b/hooks/build/hooks/pre_gen/pre_gen_941b41827775cf780a9e35a7f1340748e973bc58.dart" file not found.
Worker 4: FAILED (exit 1) - Error: "/Users/erickzanardo/.mason-cache/bundled/repro_brick_0.1.0_5c50347778b7dc27b74043aec3a43b0d25ebe05b/hooks/build/hooks/pre_gen/pre_gen_941b41827775cf780a9e35a7f1340748e973bc58.dart" file not found.
Worker 5: FAILED (exit 1) - Bad state: Generating kernel failed!
Worker 6: FAILED (exit 1) - Error: "/Users/erickzanardo/.mason-cache/bundled/repro_brick_0.1.0_5c50347778b7dc27b74043aec3a43b0d25ebe05b/hooks/build/hooks/pre_gen/pre_gen_12d2aceaa3e32c16a996c1ebeb7682cc414dba6a.dart" file not found.
Worker 7: FAILED (exit 1) - Error: "/Users/erickzanardo/.mason-cache/bundled/repro_brick_0.1.0_5c50347778b7dc27b74043aec3a43b0d25ebe05b/hooks/build/hooks/pre_gen/pre_gen_941b41827775cf780a9e35a7f1340748e973bc58.dart" file not found.
Worker 8: FAILED (exit 1) - Bad state: Generating kernel failed!
```

**Expected Behavior**

Mason should be able to handle multiple execution at the same time.

**Additional Context**

If we comment this section on the `repro.dart`:

```dart
if (masonCache.existsSync()) {
try {
masonCache.deleteSync(recursive: true);
print(' Cleared Mason cache.');
} catch (e) {
print(' Could not clear cache: $e');
}
}
```

Only the first round will fail, the other rounds will pass nicely because the hook compilation will be cached in the mason cache folder

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.