ReactiveX / RxGo

Reactive Extensions for the Go language.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Connecting Observable

dayaftereh opened this issue · comments

I'm not sure if the following behavior an issue or i'm on the wrong track. I want to have a producer observable from a single channel with the ability of rxgo.WithPublishStrategy() to broadcast the items to child observables. I'm creating the child observables by using Observable#Observe channel from the producer observable with rxgo.FromChannel, but when the child Observable is canceled via the context from context.WithCancel() the parent observable stops emitting items.

using: github.com/reactivex/rxgo/v2 v2.5.0

To Reproduce
The following test allows to reproduce the behavior:

import "github.com/reactivex/rxgo/v2"

func TestRxGo(t *testing.T) {
	producerCtx, cancelProducer := context.WithCancel(context.Background())
	// create a infinity producer with context for cancel
	producer := rxgo.Create(
		[]rxgo.Producer{func(ctx context.Context, next chan<- rxgo.Item) {
			log.Print("create producer")
			defer func() {
				log.Print("producer done")
			}()

			i := 0
			for {
				timeout := time.After(time.Second * 1)
				select {
				case <-ctx.Done():
					return
				case <-timeout:
					next <- rxgo.Of(i)
					log.Printf("producer fire %d", i)
				}
				i = i + 1
			}
		}},
		rxgo.WithPublishStrategy(),
		rxgo.WithContext(producerCtx),
		rxgo.WithBackPressureStrategy(rxgo.Drop),
	)

	// connect to the producer
	producer.Connect(producerCtx)

	// create a child observable to receive items from producer
	append := func(index int, parent context.Context) rxgo.Observable {
		observable := rxgo.FromChannel(
			producer.Observe(),
			rxgo.WithBackPressureStrategy(rxgo.Drop),
			rxgo.WithContext(parent),
		)
		observable.DoOnNext(func(i interface{}) {
			log.Printf("Observable %d: %v", index, i)
		})

		return observable
	}

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)

	// create the first observable with own context for cancel
	observable1Ctx, cancelObservable1 := context.WithCancel(context.Background())
	append(1, observable1Ctx)

	// create the second observable with own context for cancel
	observable2Ctx, cancelObservable2 := context.WithCancel(context.Background())
	append(2, observable2Ctx)

	go func() {
		log.Printf("sleep 5")
		time.Sleep(time.Second * 5)

		log.Print("cancel 1 observable")
		cancelObservable1()

		log.Printf("sleep 5")
		time.Sleep(time.Second * 5)

		log.Print("cancel 2 observable")
		cancelObservable2()
	}()

	log.Print("wait for done")
	<-observable2Ctx.Done()
	log.Print("all observable canceled")

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)

	log.Printf("cancel the producer")
	cancelProducer()

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)
}

The console output is:

=== RUN   TestRxGo
2021/05/05 14:57:32 sleep 5
2021/05/05 14:57:32 create producer
2021/05/05 14:57:33 producer fire 0
2021/05/05 14:57:34 producer fire 1
2021/05/05 14:57:35 producer fire 2
2021/05/05 14:57:36 producer fire 3
2021/05/05 14:57:37 wait for done
2021/05/05 14:57:37 sleep 5
2021/05/05 14:57:37 producer fire 4
2021/05/05 14:57:37 Observable 1: 4
2021/05/05 14:57:37 Observable 2: 4
2021/05/05 14:57:38 producer fire 5
2021/05/05 14:57:38 Observable 2: 5
2021/05/05 14:57:38 Observable 1: 5
2021/05/05 14:57:39 producer fire 6
2021/05/05 14:57:39 Observable 2: 6
2021/05/05 14:57:39 Observable 1: 6
2021/05/05 14:57:40 producer fire 7
2021/05/05 14:57:40 Observable 2: 7
2021/05/05 14:57:40 Observable 1: 7
2021/05/05 14:57:41 producer fire 8
2021/05/05 14:57:41 Observable 2: 8
2021/05/05 14:57:41 Observable 1: 8
2021/05/05 14:57:42 cancel 1 observable
2021/05/05 14:57:42 sleep 5
2021/05/05 14:57:42 producer fire 9

##--> Missing Observable 2
##--> Missing producer fire

2021/05/05 14:57:47 cancel 2 observable
2021/05/05 14:57:47 all observable canceled
2021/05/05 14:57:47 sleep 5

##--> Missing producer fire

2021/05/05 14:57:52 cancel the producer

##--> Missing producer done

2021/05/05 14:57:52 sleep 5
--- PASS: TestRxGo (25.04s)
PASS

Expected behavior

Actually i what that the producer observable still emits items without blocking and creating child observables dynamically.

Using Observable.ForEach to forward the items from the producer i'm close to a working example, but with a ugly workaround using a boolflag, because Observable.ForEach is unable to stop emitting item from the producer.

Here is the test:

func TestRxGoWithForEach(t *testing.T) {
	producerCtx, cancelProducer := context.WithCancel(context.Background())
	// create a infinity producer with context for cancel
	producer := rxgo.Create(
		[]rxgo.Producer{func(ctx context.Context, next chan<- rxgo.Item) {
			log.Print("create producer")
			defer func() {
				log.Print("producer done")
			}()

			i := 0
			for {
				timeout := time.After(time.Second * 1)
				select {
				case <-ctx.Done():
					return
				case <-timeout:
					next <- rxgo.Of(i)
					log.Printf("producer fire %d", i)
				}
				i = i + 1
			}
		}},
		rxgo.WithPublishStrategy(),
		rxgo.WithContext(producerCtx),
		rxgo.WithBackPressureStrategy(rxgo.Drop),
	)

	// connect to the producer
	producer.Connect(producerCtx)

	// create a child observable to receive items from producer
	append := func(index int, parent context.Context) rxgo.Observable {

		observableCtx, cancel := context.WithCancel(context.Background())

		observable := rxgo.Create(
			[]rxgo.Producer{func(ctx context.Context, next chan<- rxgo.Item) {
				log.Printf("create observable %d", index)
				defer func() {
					log.Printf("observable done %d", index)
				}()

				done := false

				wait := producer.ForEach(
					func(i interface{}) {
						if !done {
							next <- rxgo.Of(i)
						} else {
							log.Printf("ForEach observable %d", index)
						}
					},
					func(err error) {
						if !done {
							next <- rxgo.Error(err)
						}
					},
					func() {
						cancel()
					},
					rxgo.WithContext(ctx),
				)

				select {
				case <-observableCtx.Done():
				case <-wait:
				}

				done = true
			}},
			rxgo.WithPublishStrategy(),
			rxgo.WithContext(observableCtx),
			rxgo.WithBackPressureStrategy(rxgo.Drop),
		)

		_, abort := observable.Connect(context.Background())

		go func() {
			<-parent.Done()
			abort()
			cancel()
		}()

		observable.DoOnNext(func(i interface{}) {
			log.Printf("Observable %d: %v", index, i)
		})

		return observable
	}

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)

	// create the first observable with own context for cancel
	observable1Ctx, cancelObservable1 := context.WithCancel(context.Background())
	append(1, observable1Ctx)

	// create the second observable with own context for cancel
	observable2Ctx, cancelObservable2 := context.WithCancel(context.Background())
	append(2, observable2Ctx)

	go func() {
		log.Printf("sleep 5")
		time.Sleep(time.Second * 5)

		log.Print("cancel 1 observable")
		cancelObservable1()

		log.Printf("sleep 5")
		time.Sleep(time.Second * 5)

		log.Print("cancel 2 observable")
		cancelObservable2()
	}()

	log.Print("wait for done")
	<-observable2Ctx.Done()
	log.Print("all observable canceled")

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)

	log.Printf("cancel the producer")
	cancelProducer()

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)
}

and the console output:

=== RUN   TestRxGoWithForEach
2021/05/06 08:45:40 sleep 5
2021/05/06 08:45:40 create producer
2021/05/06 08:45:41 producer fire 0
2021/05/06 08:45:42 producer fire 1
2021/05/06 08:45:43 producer fire 2
2021/05/06 08:45:44 producer fire 3
2021/05/06 08:45:45 create observable 1
2021/05/06 08:45:45 create observable 2
2021/05/06 08:45:45 wait for done
2021/05/06 08:45:45 sleep 5
2021/05/06 08:45:45 producer fire 4
2021/05/06 08:45:45 Observable 2: 4
2021/05/06 08:45:45 Observable 1: 4
2021/05/06 08:45:46 producer fire 5
2021/05/06 08:45:46 Observable 2: 5
2021/05/06 08:45:46 Observable 1: 5
2021/05/06 08:45:47 producer fire 6
2021/05/06 08:45:47 Observable 2: 6
2021/05/06 08:45:47 Observable 1: 6
2021/05/06 08:45:48 producer fire 7
2021/05/06 08:45:48 Observable 2: 7
2021/05/06 08:45:48 Observable 1: 7
2021/05/06 08:45:49 producer fire 8
2021/05/06 08:45:49 Observable 1: 8
2021/05/06 08:45:49 Observable 2: 8
2021/05/06 08:45:50 cancel 1 observable
2021/05/06 08:45:50 sleep 5
2021/05/06 08:45:50 observable done 1
2021/05/06 08:45:50 producer fire 9
2021/05/06 08:45:50 ForEach observable 1
2021/05/06 08:45:50 Observable 2: 9
2021/05/06 08:45:51 producer fire 10
2021/05/06 08:45:51 Observable 2: 10
2021/05/06 08:45:51 ForEach observable 1
2021/05/06 08:45:52 producer fire 11
2021/05/06 08:45:52 Observable 2: 11
2021/05/06 08:45:52 ForEach observable 1
2021/05/06 08:45:53 producer fire 12
2021/05/06 08:45:53 Observable 2: 12
2021/05/06 08:45:53 ForEach observable 1
2021/05/06 08:45:54 producer fire 13
2021/05/06 08:45:54 Observable 2: 13
2021/05/06 08:45:54 ForEach observable 1
2021/05/06 08:45:55 cancel 2 observable
2021/05/06 08:45:55 all observable canceled
2021/05/06 08:45:55 sleep 5
2021/05/06 08:45:55 observable done 2
2021/05/06 08:45:55 producer fire 14
2021/05/06 08:45:55 ForEach observable 2
2021/05/06 08:45:55 ForEach observable 1
2021/05/06 08:45:56 producer fire 15
2021/05/06 08:45:56 ForEach observable 2
2021/05/06 08:45:56 ForEach observable 1
2021/05/06 08:45:57 producer fire 16
2021/05/06 08:45:57 ForEach observable 2
2021/05/06 08:45:57 ForEach observable 1
2021/05/06 08:45:58 producer fire 17
2021/05/06 08:45:58 ForEach observable 2
2021/05/06 08:45:58 ForEach observable 1
2021/05/06 08:45:59 producer fire 18
2021/05/06 08:45:59 ForEach observable 2
2021/05/06 08:45:59 ForEach observable 1
2021/05/06 08:46:00 cancel the producer
2021/05/06 08:46:00 sleep 5
2021/05/06 08:46:00 producer done
--- PASS: TestRxGoWithForEach (25.05s)
PASS

I guess I know why that blocks

func (i *channelIterable) produce(ctx context.Context) {
	defer func() {
		i.mutex.RLock()
		for _, subscriber := range i.subscribers {
			close(subscriber)
		}
		i.mutex.RUnlock()
	}()

	for {
		select {
		case <-ctx.Done():
			return
		case item, ok := <-i.next:
			if !ok {
				return
			}
			i.mutex.RLock()
			for _, subscriber := range i.subscribers {
				subscriber <- item
			}
			i.mutex.RUnlock()
		}
	}
}

The subscriber channel is not listened to when the subscription context goes away. In your example (Observable 1 and Observable 2). So...It gets stuck in this for loop. @dayaftereh - when both adding and removing subscribers are also taken into account, then subscription leakage happens because closing one subscription context leads to pipeline block :(

@dayaftereh - I made some changes in the PR linked to allow unsubscribe. For testing purposes, I changed the producer part in your example to a channel based producer. Here is the modified test

func TestRxGo(t *testing.T) {
	producerCtx, cancelProducer := context.WithCancel(context.Background())
	// create a infinity producer with context for cancel
	itemCh := make(chan Item)
	go func() {
		defer func() {
			log.Print("producer done")
			close(itemCh)
		}()
		i := 0
		for {
			select {
			case <-producerCtx.Done():
				return
			case <-time.After(1 * time.Second):
				select {
				case <-producerCtx.Done():
					return
				case itemCh <- Of(i):
					log.Printf("producer fire %d", i)
				}
				i++
			}
		}
	}()

	producer := FromChannel(itemCh,
		WithPublishStrategy(),
		WithContext(producerCtx),
		WithBackPressureStrategy(Drop))

	// connect to the producer
	producer.Connect(producerCtx)

	// create a child observable to receive items from producer
	append := func(index int, parent context.Context) {
		producer.DoOnNext(func(i interface{}) {
			log.Printf("Observable %d: %v", index, i)
		}, WithContext(parent))
	}

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)

	// create the first observable with own context for cancel
	observable1Ctx, cancelObservable1 := context.WithCancel(context.Background())
	append(1, observable1Ctx)

	// create the second observable with own context for cancel
	observable2Ctx, cancelObservable2 := context.WithCancel(context.Background())
	append(2, observable2Ctx)

	go func() {
		log.Printf("sleep 5")
		time.Sleep(time.Second * 5)

		log.Print("cancel 1 observable")
		cancelObservable1()

		log.Printf("sleep 5")
		time.Sleep(time.Second * 5)

		log.Print("cancel 2 observable")
		cancelObservable2()
	}()

	log.Print("wait for done")
	<-observable2Ctx.Done()
	log.Print("all observable canceled")

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)

	log.Printf("cancel the producer")
	cancelProducer()

	log.Printf("sleep 5")
	time.Sleep(time.Second * 5)
}

And..........Here is the output

=== RUN   TestRxGo
2021/11/04 13:34:22 sleep 5
2021/11/04 13:34:23 producer fire 0
2021/11/04 13:34:24 producer fire 1
2021/11/04 13:34:25 producer fire 2
2021/11/04 13:34:26 producer fire 3
2021/11/04 13:34:27 wait for done
2021/11/04 13:34:27 sleep 5
2021/11/04 13:34:27 producer fire 4
2021/11/04 13:34:27 Observable 2: 4
2021/11/04 13:34:27 Observable 1: 4
2021/11/04 13:34:28 producer fire 5
2021/11/04 13:34:28 Observable 2: 5
2021/11/04 13:34:28 Observable 1: 5
2021/11/04 13:34:29 producer fire 6
2021/11/04 13:34:29 Observable 2: 6
2021/11/04 13:34:29 Observable 1: 6
2021/11/04 13:34:30 producer fire 7
2021/11/04 13:34:30 Observable 1: 7
2021/11/04 13:34:30 Observable 2: 7
2021/11/04 13:34:31 producer fire 8
2021/11/04 13:34:31 Observable 2: 8
2021/11/04 13:34:31 Observable 1: 8
2021/11/04 13:34:32 cancel 1 observable
2021/11/04 13:34:32 sleep 5
2021/11/04 13:34:32 producer fire 9
2021/11/04 13:34:32 Observable 2: 9
2021/11/04 13:34:33 producer fire 10
2021/11/04 13:34:33 Observable 2: 10
2021/11/04 13:34:34 producer fire 11
2021/11/04 13:34:34 Observable 2: 11
2021/11/04 13:34:35 producer fire 12
2021/11/04 13:34:35 Observable 2: 12
2021/11/04 13:34:36 producer fire 13
2021/11/04 13:34:36 Observable 2: 13
2021/11/04 13:34:37 cancel 2 observable
2021/11/04 13:34:37 all observable canceled
2021/11/04 13:34:37 sleep 5
2021/11/04 13:34:37 producer fire 14
2021/11/04 13:34:38 producer fire 15
2021/11/04 13:34:39 producer fire 16
2021/11/04 13:34:40 producer fire 17
2021/11/04 13:34:41 producer fire 18
2021/11/04 13:34:42 cancel the producer
2021/11/04 13:34:42 sleep 5
2021/11/04 13:34:42 producer done
--- PASS: TestRxGo (25.01s)
PASS

Perfect.