Skip to content

Redesign file persistence pipeline to eliminate race conditions #14

@meanmail

Description

@meanmail

Overview

The plugin has a race condition between asynchronous file content persistence and YAML serialization. Current workaround uses blocking waitForPersistingTasks() which can freeze the EDT. The persistence pipeline needs architectural redesign to be fully non-blocking while maintaining data consistency.

Problem

Current Architecture

  1. File contents are persisted asynchronously on pooled threads (executeOnPooledThread at LearningObjectsStorageManager.kt:62)
  2. YAML serialization happens on EDT and requires all file contents to be persisted
  3. Current solution: waitForPersistingTasks() (lines 140-152) blocks on Future.get() until all persistence completes

Issues with Current Approach

fun waitForPersistingTasks() {
    persistingTasks.forEach { it.get() } // Blocking wait!
}
  • Blocks the calling thread (often EDT)
  • Can cause UI freezes during course save operations
  • No timeout handling
  • No cancellation support

Diagnostic Wrapper

Lines 58-61 track content changes during persistence, indicating awareness of the race condition, but the fix is to just block and wait.

Key Files

  • intellij-plugin/hs-core/src/org/hyperskill/academy/learning/storage/LearningObjectsStorageManager.kt
  • intellij-plugin/hs-core/src/org/hyperskill/academy/learning/yaml/YamlFormatSynchronizer.kt
  • intellij-plugin/hs-core/src/org/hyperskill/academy/learning/framework/impl/FrameworkLessonManagerImpl.kt

What Makes This Hard

  1. Concurrency complexity: Coordinating async file persistence with EDT-bound YAML serialization
  2. State synchronization: File content can change during persistence operation
  3. Multiple storage backends: YAML, SQLite, InMemory - each with different threading characteristics
  4. EDT constraints: Cannot perform slow operations on EDT, but YAML writing currently requires synchronized state
  5. Cancellation: Must handle project disposal during persistence
  6. Consistency guarantees: Must ensure YAML always represents completely persisted state
  7. Performance: Solution must not regress performance compared to async persistence

Requirements

Architecture Design

  • Design a fully non-blocking persistence pipeline
  • Implement proper async coordination between persistence and serialization
  • Consider using:
    • CompletableFuture instead of raw Future for better composition
    • ReadAction.nonBlocking() / WriteAction.nonBlocking() for VFS operations
    • Message bus listeners to trigger YAML save after persistence completes
    • Coroutines for async flow control (if appropriate for the platform version)

Implementation

  • Remove all blocking Future.get() calls
  • Ensure no EDT violations
  • Handle all edge cases:
    • File content changes during persistence
    • Project disposal mid-persistence
    • Multiple concurrent persistence requests
    • Storage backend failures
  • Add proper cancellation support
  • Maintain data consistency across all storage backends

Testing

  • Add comprehensive tests for concurrent scenarios:
    • Rapid file modifications + frequent saves
    • Project close during persistence
    • Storage backend failures
  • Performance test: persistence should not regress
  • EDT test: no blocking operations on EDT

Success Criteria

  • Zero EDT violations (verify with EDT violation detection enabled)
  • No blocking waits anywhere in persistence pipeline
  • All concurrent test scenarios pass
  • Performance equal to or better than current implementation
  • Clean shutdown even during active persistence

Estimated Time

6-8 hours (complex async coordination, multiple storage backends, comprehensive testing)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions