fix: RwLock race condition during group controller instantiation (#6860)

* chore: code cleanup

* fix: view editor used during initialization

* fix: quick and dirty hack job

* test: add test

* chore: don't create separate mut var

* chore: docs

* fix: uninitialized group controllers

* chore: remove group

* chore: fix test

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Richard Shiue 2024-11-28 17:26:13 +08:00 committed by GitHub
parent 4c6f6f14f3
commit 510d8357ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 114 additions and 35 deletions

View File

@ -293,7 +293,6 @@ class DatabaseTabBarController {
OnViewChildViewChanged? onViewChildViewChanged;
Future<void> dispose() async {
await viewListener.stop();
await controller.dispose();
await Future.wait([viewListener.stop(), controller.dispose()]);
}
}

View File

@ -179,9 +179,7 @@ class _DesktopBoardPageState extends State<DesktopBoardPage> {
_focusScope.dispose();
_boardBloc.close();
_boardActionsCubit.close();
_didCreateRow
..removeListener(_handleDidCreateRow)
..dispose();
_didCreateRow.dispose();
super.dispose();
}
@ -189,7 +187,7 @@ class _DesktopBoardPageState extends State<DesktopBoardPage> {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<BoardBloc>.value(
BlocProvider.value(
value: _boardBloc,
),
BlocProvider.value(

View File

@ -70,6 +70,14 @@ impl Drop for DatabaseViewEditor {
}
impl DatabaseViewEditor {
/// Create a new Database View Editor.
///
/// After creating the editor, you must call [DatabaseViewEditor::initialize] to properly initialize it.
/// This initialization step will load essential data, such as group information.
///
/// Avoid calling any methods of [DatabaseViewOperation] before the editor is fully initialized,
/// as some actions may rely on the current editor state. Failing to follow this order could result
/// in unexpected behavior, including potential deadlocks.
pub async fn new(
database_id: String,
view_id: String,
@ -127,6 +135,16 @@ impl DatabaseViewEditor {
})
}
/// Initialize the editor after creating it
/// You should call [DatabaseViewEditor::initialize] after creating the editor
pub async fn initialize(&self) -> FlowyResult<()> {
if let Some(group) = self.group_controller.write().await.as_mut() {
group.load_group_data().await?;
}
Ok(())
}
pub async fn insert_row(&self, row: Option<Arc<Row>>, index: u32, row_order: &RowOrder) {
let mut row_orders = self.row_orders.write().await;
if row_orders.len() >= index as usize {
@ -975,7 +993,7 @@ impl DatabaseViewEditor {
if let Some(field) = self.delegate.get_field(field_id).await {
tracing::trace!("create new group controller");
let new_group_controller = new_group_controller(
let mut new_group_controller = new_group_controller(
self.view_id.clone(),
self.delegate.clone(),
self.filter_controller.clone(),
@ -983,7 +1001,9 @@ impl DatabaseViewEditor {
)
.await?;
if let Some(controller) = &new_group_controller {
if let Some(controller) = &mut new_group_controller {
(*controller).load_group_data().await?;
let new_groups = controller
.get_all_groups()
.into_iter()
@ -1113,13 +1133,22 @@ impl DatabaseViewEditor {
}
// initialize the group controller if the current layout support grouping
*self.group_controller.write().await = new_group_controller(
let new_group_controller = match new_group_controller(
self.view_id.clone(),
self.delegate.clone(),
self.filter_controller.clone(),
None,
)
.await?;
.await?
{
Some(mut controller) => {
controller.load_group_data().await?;
Some(controller)
},
None => None,
};
*self.group_controller.write().await = new_group_controller;
let payload = DatabaseLayoutMetaPB {
view_id: self.view_id.clone(),

View File

@ -27,14 +27,13 @@ pub async fn new_group_controller(
let controller_delegate = GroupControllerDelegateImpl {
delegate: delegate.clone(),
filter_controller: filter_controller.clone(),
filter_controller,
};
let grouping_field = match grouping_field {
Some(field) => Some(field),
None => {
let group_setting = controller_delegate.get_group_setting(&view_id).await;
let fields = delegate.get_fields(&view_id, None).await;
group_setting

View File

@ -61,8 +61,10 @@ impl DatabaseViews {
return Ok(editor.clone());
}
let mut editor_map = self.view_editors.write().await;
let database_id = self.database.read().await.get_database_id();
// Acquire the write lock to insert the new editor. We need to acquire the lock first to avoid
// initializing the editor multiple times.
let mut editor_map = self.view_editors.write().await;
let editor = Arc::new(
DatabaseViewEditor::new(
database_id,
@ -73,6 +75,9 @@ impl DatabaseViews {
.await?,
);
editor_map.insert(view_id.to_owned(), editor.clone());
drop(editor_map);
editor.initialize().await?;
Ok(editor)
}

View File

@ -98,6 +98,7 @@ pub trait GroupCustomize: Send + Sync {
/// while a `URL` group controller will be a `DefaultGroupController`.
#[async_trait]
pub trait GroupController: Send + Sync {
async fn load_group_data(&mut self) -> FlowyResult<()>;
/// Returns the id of field that is being used to group the rows
fn get_grouping_field_id(&self) -> &str;

View File

@ -54,20 +54,12 @@ where
{
pub async fn new(
grouping_field: &Field,
mut configuration: GroupControllerContext<C>,
context: GroupControllerContext<C>,
delegate: Arc<dyn GroupControllerDelegate>,
) -> FlowyResult<Self> {
let field_type = FieldType::from(grouping_field.field_type);
let type_option = grouping_field
.get_type_option::<T>(&field_type)
.unwrap_or_else(|| T::from(default_type_option_data_from_type(field_type)));
let generated_groups = G::build(grouping_field, &configuration, &type_option).await;
let _ = configuration.init_groups(generated_groups)?;
Ok(Self {
grouping_field_id: grouping_field.id.clone(),
context: configuration,
context,
group_builder_phantom: PhantomData,
cell_parser_phantom: PhantomData,
delegate,
@ -163,6 +155,30 @@ where
G: GroupsBuilder<Context = GroupControllerContext<C>, GroupTypeOption = T>,
Self: GroupCustomize<GroupTypeOption = T>,
{
async fn load_group_data(&mut self) -> FlowyResult<()> {
let grouping_field = self
.delegate
.get_field(&self.grouping_field_id)
.await
.ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?;
let field_type = FieldType::from(grouping_field.field_type);
let type_option = grouping_field
.get_type_option::<T>(&field_type)
.unwrap_or_else(|| T::from(default_type_option_data_from_type(field_type)));
let generated_groups = G::build(&grouping_field, &self.context, &type_option).await;
let _ = self.context.init_groups(generated_groups)?;
let row_details = self.delegate.get_all_rows(&self.context.view_id).await;
let rows = row_details
.iter()
.map(|row| row.as_ref())
.collect::<Vec<_>>();
self.fill_groups(rows.as_slice(), &grouping_field)?;
Ok(())
}
fn get_grouping_field_id(&self) -> &str {
&self.grouping_field_id
}

View File

@ -219,7 +219,7 @@ impl GroupCustomize for DateGroupController {
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, _)) => {
_ => {
let date = DateTime::parse_from_str(group_id, GROUP_ID_DATE_FORMAT).unwrap();
let cell = insert_date_cell(date.timestamp(), None, Some(false), field);
cells.insert(field.id.clone(), cell);

View File

@ -21,6 +21,7 @@ use crate::services::group::{
/// means all rows will be grouped in the same group.
///
pub struct DefaultGroupController {
pub view_id: String,
pub field_id: String,
pub group: GroupData,
pub delegate: Arc<dyn GroupControllerDelegate>,
@ -29,9 +30,10 @@ pub struct DefaultGroupController {
const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController";
impl DefaultGroupController {
pub fn new(field: &Field, delegate: Arc<dyn GroupControllerDelegate>) -> Self {
pub fn new(view_id: &str, field: &Field, delegate: Arc<dyn GroupControllerDelegate>) -> Self {
let group = GroupData::new(DEFAULT_GROUP_CONTROLLER.to_owned(), field.id.clone(), true);
Self {
view_id: view_id.to_owned(),
field_id: field.id.clone(),
group,
delegate,
@ -41,6 +43,19 @@ impl DefaultGroupController {
#[async_trait]
impl GroupController for DefaultGroupController {
async fn load_group_data(&mut self) -> FlowyResult<()> {
let row_details = self.delegate.get_all_rows(&self.view_id).await;
let rows = row_details
.iter()
.map(|row| row.as_ref())
.collect::<Vec<_>>();
rows.iter().for_each(|row| {
self.group.add_row((*row).clone());
});
Ok(())
}
fn get_grouping_field_id(&self) -> &str {
&self.field_id
}

View File

@ -114,6 +114,7 @@ impl GroupCustomize for MultiSelectGroupController {
{
// Remove the option if the group is found
new_type_option.options.remove(option_index);
self.context.delete_group(group_id)?;
Ok(Some(new_type_option.into()))
} else {
Ok(None)

View File

@ -116,6 +116,7 @@ impl GroupCustomize for SingleSelectGroupController {
{
// Remove the option if the group is found
new_type_option.options.remove(option_index);
self.context.delete_group(group_id)?;
Ok(Some(new_type_option.into()))
} else {
Ok(None)

View File

@ -63,7 +63,7 @@ pub type UpdatedCells = HashMap<String, Cell>;
fields(grouping_field_id=%grouping_field.id, grouping_field_type)
err
)]
pub async fn make_group_controller<D>(
pub(crate) async fn make_group_controller<D>(
view_id: &str,
grouping_field: Field,
delegate: D,
@ -74,7 +74,7 @@ where
let grouping_field_type = FieldType::from(grouping_field.field_type);
tracing::Span::current().record("grouping_field", &grouping_field_type.default_name());
let mut group_controller: Box<dyn GroupController>;
let group_controller: Box<dyn GroupController>;
let delegate = Arc::new(delegate);
match grouping_field_type {
@ -135,20 +135,13 @@ where
},
_ => {
group_controller = Box::new(DefaultGroupController::new(
view_id,
&grouping_field,
delegate.clone(),
));
},
}
// Separates the rows into different groups
let row_details = delegate.get_all_rows(view_id).await;
let rows = row_details
.iter()
.map(|row| row.as_ref())
.collect::<Vec<_>>();
group_controller.fill_groups(rows.as_slice(), &grouping_field)?;
#[cfg(feature = "verbose_log")]
{
for group in group_controller.get_all_groups() {

View File

@ -78,3 +78,25 @@ async fn group_move_url_group_row_test() {
test.assert_group_row_count(1, 3).await;
test.assert_group_row_count(2, 1).await;
}
// Create a URL field in the default board and then set it as the grouping field.
#[tokio::test]
async fn set_group_by_url_field_test() {
let test = DatabaseGroupTest::new().await;
let url_field = test.get_url_field().await;
// group by URL field
test
.editor
.set_group_by_field(&test.view_id, &url_field.id, vec![])
.await
.unwrap();
// assert number of groups
test.assert_group_count(3).await;
// close the database view
test.editor.close_view(&test.view_id).await;
test.assert_group_count(3).await;
}