Repository: knative/pkg (go)

Issue:
Move LastTransitionTime grooming to reconcile post-process
<!--
Pro-tip: You can leave this block commented, and it still works!

Select the appropriate areas for your issue:

/area API
/area monitoring
/area networking
/area test-and-release

Classify what kind of issue this is:

/kind question
/kind bug
/kind cleanup
/kind doc
/kind feature
/kind good-first-issue

-->

## Expected Behavior
When updating conditions, we want to ensure that LastTransitionTime does not change if a condition is set but otherwise keeps the same value. Currently we use the ConditionManager to do so on each modification of a condition.

## Actual Behavior
Some implementers might reset the conditions or switch between conditions within the same reconciliation. On write-back this can cause churn of the LastTransitionTime field which may result in unneeded watch events / resource versions.

Example: https://github.com/knative/serving/blob/baace2803b2ce4ff17078db8410ad884c2a2a6f5/pkg/reconciler/autoscaling/hpa/hpa.go#L64

In the case where this reconciler is Inactive we get here: https://github.com/knative/serving/blob/baace2803b2ce4ff17078db8410ad884c2a2a6f5/pkg/reconciler/autoscaling/hpa/hpa.go#L106

We'll keep flipping from Active to Inactive. The user will just see Inactive with a marching timestamp.

## Proposed Solution
In the post-process step for reconciliation, compare the original resource to the written one and preserve the old condition if nothing has changed (exempting timestamps for equality). This will make it so implementers don't need to worry about changing the resource more than once with unintended side-effects.

At the moment its expected that all writes to conditions go through the ConditionManager, but this will also make it so that direct changes to the condition struct won't have unintended side effects either (in practice I don't think people are doing this, but could potentially make future implementations more readable)


Pull request description:
Groom conditions LastTransitionTime in postprocess
Fixes #1394

* This fixes behavior if a condition is flip/flopped in a single reconciliation
* All changed conditions share the same time

Patch:
diff --git a/codegen/cmd/injection-gen/generators/reconciler_reconciler.go b/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
index 647b82fd0..8b4d0c733 100644
--- a/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
+++ b/codegen/cmd/injection-gen/generators/reconciler_reconciler.go
@@ -320,7 +320,7 @@ func (r *reconcilerImpl) Reconcile(ctx {{.contextContext|raw}}, key string) erro
 		reconcileEvent = r.reconciler.ReconcileKind(ctx, resource)
 
 		{{if .isKRShaped}}
-		reconciler.PostProcessReconcile(ctx, resource)
+		reconciler.PostProcessReconcile(ctx, resource, original)
 		{{end}}
 	} else if fin, ok := r.reconciler.(Finalizer); ok {
 		// Append the target method to the logger.
diff --git a/go.mod b/go.mod
index e23d408b6..1dc12a5e5 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,7 @@ require (
 	github.com/prometheus/common v0.9.1
 	github.com/spf13/pflag v1.0.5
 	github.com/tsenart/vegeta v12.7.1-0.20190725001342-b5f4fca92137+incompatible
-	go.opencensus.io v0.22.3
+	go.opencensus.io v0.22.4
 	go.uber.org/multierr v1.5.0
 	go.uber.org/zap v1.14.1
 	golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
diff --git a/go.sum b/go.sum
index 7e85df170..3bf17f53d 100644
--- a/go.sum
+++ b/go.sum
@@ -898,6 +898,8 @@ go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
diff --git a/injection/README.md b/injection/README.md
index d30b090bc..e23ff32f5 100644
--- a/injection/README.md
+++ b/injection/README.md
@@ -471,7 +471,7 @@ reconciler.PreProcessReconcile(ctx, resource)
 
 reconcileEvent = r.reconciler.ReconcileKind(ctx, resource)
 
-reconciler.PostProcessReconcile(ctx, resource)
+reconciler.PostProcessReconcile(ctx, resource, oldResource)
 ```
 
 #### Stubs
diff --git a/reconciler/reconcile_common.go b/reconciler/reconcile_common.go
index c9f8b5cb8..a807edfb6 100644
--- a/reconciler/reconcile_common.go
+++ b/reconciler/reconcile_common.go
@@ -18,6 +18,10 @@ package reconciler
 
 import (
 	"context"
+	"reflect"
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"knative.dev/pkg/apis"
 	duckv1 "knative.dev/pkg/apis/duck/v1"
@@ -50,7 +54,7 @@ func PreProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
 }
 
 // PostProcessReconcile contains logic to apply after reconciliation of a resource.
-func PostProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
+func PostProcessReconcile(ctx context.Context, resource, oldResource duckv1.KRShaped) {
 	logger := logging.FromContext(ctx)
 	status := resource.GetStatus()
 	mgr := resource.GetConditionSet().Manage(status)
@@ -64,4 +68,26 @@ func PostProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
 	} else if rc.Reason == failedGenerationBump {
 		logger.Warn("A reconciler observed a new generation without updating the resource status")
 	}
+
+	groomConditionsTransitionTime(resource, oldResource)
+}
+
+// groomConditionsTransitionTime ensures that the LastTransitionTime only advances for resources
+// where the condition has changed during reconciliation. This also ensures that all advanced
+// conditions share the same timestamp.
+func groomConditionsTransitionTime(resource, oldResource duckv1.KRShaped) {
+	now := apis.VolatileTime{Inner: metav1.NewTime(time.Now())}
+	sts := resource.GetStatus()
+	for i := range sts.Conditions {
+		cond := &sts.Conditions[i]
+
+		if oldCond := oldResource.GetStatus().GetCondition(cond.Type); oldCond != nil {
+			cond.LastTransitionTime = oldCond.LastTransitionTime
+			if reflect.DeepEqual(cond, oldCond) {
+				continue
+			}
+		}
+
+		cond.LastTransitionTime = now
+	}
 }
diff --git a/vendor/go.opencensus.io/Makefile b/vendor/go.opencensus.io/Makefile
index 457866cb1..b3ce3df30 100644
--- a/vendor/go.opencensus.io/Makefile
+++ b/vendor/go.opencensus.io/Makefile
@@ -8,7 +8,7 @@ ALL_PKGS := $(shell go list $(sort $(dir $(ALL_SRC))))
 GOTEST_OPT?=-v -race -timeout 30s
 GOTEST_OPT_WITH_COVERAGE = $(GOTEST_OPT) -coverprofile=coverage.txt -covermode=atomic
 GOTEST=go test
-GOFMT=gofmt
+GOIMPORTS=goimports
 GOLINT=golint
 GOVET=go vet
 EMBEDMD=embedmd
@@ -17,14 +17,14 @@ TRACE_ID_LINT_EXCEPTION="type name will be used as trace.TraceID by other packag
 TRACE_OPTION_LINT_EXCEPTION="type name will be used as trace.TraceOptions by other packages"
 README_FILES := $(shell find . -name '*README.md' | sort | tr '\n' ' ')
 
-.DEFAULT_GOAL := fmt-lint-vet-embedmd-test
+.DEFAULT_GOAL := imports-lint-vet-embedmd-test
 
-.PHONY: fmt-lint-vet-embedmd-test
-fmt-lint-vet-embedmd-test: fmt lint vet embedmd test
+.PHONY: imports-lint-vet-embedmd-test
+imports-lint-vet-embedmd-test: imports lint vet embedmd test
 
 # TODO enable test-with-coverage in tavis
 .PHONY: travis-ci
-travis-ci: fmt lint vet embedmd test test-386
+travis-ci: imports lint vet embedmd test test-386
 
 all-pkgs:
 	@echo $(ALL_PKGS) | tr ' ' '\n' | sort
@@ -44,15 +44,15 @@ test-386:
 test-with-coverage:
 	$(GOTEST) $(GOTEST_OPT_WITH_COVERAGE) $(ALL_PKGS)
 
-.PHONY: fmt
-fmt:
-	@FMTOUT=`$(GOFMT) -s -l $(ALL_SRC) 2>&1`; \
-	if [ "$$FMTOUT" ]; then \
-		echo "$(GOFMT) FAILED => gofmt the following files:\n"; \
-		echo "$$FMTOUT\n"; \
+.PHONY: imports
+imports:
+	@IMPORTSOUT=`$(GOIMPORTS) -l $(ALL_SRC) 2>&1`; \
+	if [ "$$IMPORTSOUT" ]; then \
+		echo "$(GOIMPORTS) FAILED => goimports the following files:\n"; \
+		echo "$$IMPORTSOUT\n"; \
 		exit 1; \
 	else \
-	    echo "Fmt finished successfully"; \
+	    echo "Imports finished successfully"; \
 	fi
 
 .PHONY: lint
@@ -91,6 +91,7 @@ embedmd:
 
 .PHONY: install-tools
 install-tools:
-	go get -u golang.org/x/tools/cmd/cover
 	go get -u golang.org/x/lint/golint
+	go get -u golang.org/x/tools/cmd/cover
+	go get -u golang.org/x/tools/cmd/goimports
 	go get -u github.com/rakyll/embedmd
diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/client.go b/vendor/go.opencensus.io/plugin/ocgrpc/client.go
index 28fddb844..2063b6f76 100644
--- a/vendor/go.opencensus.io/plugin/ocgrpc/client.go
+++ b/vendor/go.opencensus.io/plugin/ocgrpc/client.go
@@ -16,8 +16,8 @@ package ocgrpc
 
 import (
 	"context"
-	"go.opencensus.io/trace"
 
+	"go.opencensus.io/trace"
 	"google.golang.org/grpc/stats"
 )
 
diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/server.go b/vendor/go.opencensus.io/plugin/ocgrpc/server.go
index 15ada839d..8a53e0972 100644
--- a/vendor/go.opencensus.io/plugin/ocgrpc/server.go
+++ b/vendor/go.opencensus.io/plugin/ocgrpc/server.go
@@ -16,9 +16,10 @@ package ocgrpc
 
 import (
 	"context"
-	"go.opencensus.io/trace"
 
 	"google.golang.org/grpc/stats"
+
+	"go.opencensus.io/trace"
 )
 
 // ServerHandler implements gRPC stats.Handler recording OpenCensus stats and
diff --git a/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go b/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go
index fef582756..61bc543d0 100644
--- a/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go
+++ b/vendor/go.opencensus.io/plugin/ocgrpc/trace_common.go
@@ -15,16 +15,16 @@
 package ocgrpc
 
 import (
+	"context"
 	"strings"
 
 	"google.golang.org/grpc/codes"
-
-	"context"
-	"go.opencensus.io/trace"
-	"go.opencensus.io/trace/propagation"
 	"google.golang.org/grpc/metadata"
 	"google.golang.org/grpc/stats"
 	"google.golang.org/grpc/status"
+
+	"go.opencensus.io/trace"
+	"go.opencensus.io/trace/propagation"
 )
 
 const traceContextKey = "grpc-trace-bin"
diff --git a/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go b/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go
index 2f1c7f006..9ad885219 100644
--- a/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go
+++ b/vendor/go.opencensus.io/plugin/ochttp/propagation/b3/b3.go
@@ -68,7 +68,7 @@ func ParseTraceID(tid string) (trace.TraceID, bool) {
 		return trace.TraceID{}, false
 	}
 	b, err := hex.DecodeString(tid)
-	if err != nil {
+	if err != nil || len(b) > 16 {
 		return trace.TraceID{}, false
 	}
 	var traceID trace.TraceID
@@ -90,7 +90,7 @@ func ParseSpanID(sid string) (spanID trace.SpanID, ok bool) {
 		return trace.SpanID{}, false
 	}
 	b, err := hex.DecodeString(sid)
-	if err != nil {
+	if err != nil || len(b) > 8 {
 		return trace.SpanID{}, false
 	}
 	start := 8 - len(b)
diff --git a/vendor/go.opencensus.io/stats/record.go b/vendor/go.opencensus.io/stats/record.go
index ad4691184..2b9728346 100644
--- a/vendor/go.opencensus.io/stats/record.go
+++ b/vendor/go.opencensus.io/stats/record.go
@@ -31,10 +31,19 @@ func init() {
 	}
 }
 
+// Recorder provides an interface for exporting measurement information from
+// the static Record method by using the WithRecorder option.
+type Recorder interface {
+	// Record records a set of measurements associated with the given tags and attachments.
+	// The second argument is a `[]Measurement`.
+	Record(*tag.Map, interface{}, map[string]interface{})
+}
+
 type recordOptions struct {
 	attachments  metricdata.Attachments
 	mutators     []tag.Mutator
 	measurements []Measurement
+	recorder     Recorder
 }
 
 // WithAttachments applies provided exemplar attachments.
@@ -58,6 +67,14 @@ func WithMeasurements(measurements ...Measurement) Options {
 	}
 }
 
+// WithRecorder records the measurements to the specified `Recorder`, rather
+// than to the global metrics recorder.
+func WithRecorder(meter Recorder) Options {
+	return func(ro *recordOptions) {
+		ro.recorder = meter
+	}
+}
+
 // Options apply changes to recordOptions.
 type Options func(*recordOptions)
 
@@ -93,6 +110,9 @@ func RecordWithOptions(ctx context.Context, ros ...Options) error {
 		return nil
 	}
 	recorder := internal.DefaultRecorder
+	if o.recorder != nil {
+		recorder = o.recorder.Record
+	}
 	if recorder == nil {
 		return nil
 	}
diff --git a/vendor/go.opencensus.io/stats/view/export.go b/vendor/go.opencensus.io/stats/view/export.go
index 7cb59718f..73ba11f5b 100644
--- a/vendor/go.opencensus.io/stats/view/export.go
+++ b/vendor/go.opencensus.io/stats/view/export.go
@@ -14,13 +14,6 @@
 
 package view
 
-import "sync"
-
-var (
-	exportersMu sync.RWMutex // guards exporters
-	exporters   = make(map[Exporter]struct{})
-)
-
 // Exporter exports the collected records as view data.
 //
 // The ExportView method should return quickly; if an
@@ -43,16 +36,10 @@ type Exporter interface {
 //
 // Binaries can register exporters, libraries shouldn't register exporters.
 func RegisterExporter(e Exporter) {
-	exportersMu.Lock()
-	defer exportersMu.Unlock()
-
-	exporters[e] = struct{}{}
+	defaultWorker.RegisterExporter(e)
 }
 
 // UnregisterExporter unregisters an exporter.
 func UnregisterExporter(e Exporter) {
-	exportersMu.Lock()
-	defer exportersMu.Unlock()
-
-	delete(exporters, e)
+	defaultWorker.UnregisterExporter(e)
 }
diff --git a/vendor/go.opencensus.io/stats/view/view_to_metric.go b/vendor/go.opencensus.io/stats/view/view_to_metric.go
index 293c1646d..5e1656a1f 100644
--- a/vendor/go.opencensus.io/stats/view/view_to_metric.go
+++ b/vendor/go.opencensus.io/stats/view/view_to_metric.go
@@ -18,6 +18,8 @@ package view
 import (
 	"time"
 
+	"go.opencensus.io/resource"
+
 	"go.opencensus.io/metric/metricdata"
 	"go.opencensus.io/stats"
 )
@@ -125,7 +127,7 @@ func rowToTimeseries(v *viewInternal, row *Row, now time.Time, startTime time.Ti
 	}
 }
 
-func viewToMetric(v *viewInternal, now time.Time, startTime time.Time) *metricdata.Metric {
+func viewToMetric(v *viewInternal, r *resource.Resource, now time.Time, startTime time.Time) *metricdata.Metric {
 	if v.metricDescriptor.Type == metricdata.TypeGaugeInt64 ||
 		v.metricDescriptor.Type == metricdata.TypeGaugeFloat64 {
 		startTime = time.Time{}
@@ -144,6 +146,7 @@ func viewToMetric(v *viewInternal, now time.Time, startTime time.Time) *metricda
 	m := &metricdata.Metric{
 		Descriptor: *v.metricDescriptor,
 		TimeSeries: ts,
+		Resource:   r,
 	}
 	return m
 }
diff --git a/vendor/go.opencensus.io/stats/view/worker.go b/vendor/go.opencensus.io/stats/view/worker.go
index 2f3c018af..ab8bfd46d 100644
--- a/vendor/go.opencensus.io/stats/view/worker.go
+++ b/vendor/go.opencensus.io/stats/view/worker.go
@@ -20,6 +20,8 @@ import (
 	"sync"
 	"time"
 
+	"go.opencensus.io/resource"
+
 	"go.opencensus.io/metric/metricdata"
 	"go.opencensus.io/metric/metricproducer"
 	"go.opencensus.io/stats"
@@ -28,7 +30,7 @@ import (
 )
 
 func init() {
-	defaultWorker = newWorker()
+	defaultWorker = NewMeter().(*worker)
 	go defaultWorker.start()
 	internal.DefaultRecorder = record
 }
@@ -47,8 +49,69 @@ type worker struct {
 	c          chan command
 	quit, done chan bool
 	mu         sync.RWMutex
+	r          *resource.Resource
+
+	exportersMu sync.RWMutex
+	exporters   map[Exporter]struct{}
+}
+
+// Meter defines an interface which allows a single process to maintain
+// multiple sets of metrics exports (intended for the advanced case where a
+// single process wants to report metrics about multiple objects, such as
+// multiple databases or HTTP services).
+//
+// Note that this is an advanced use case, and the static functions in this
+// module should cover the common use cases.
+type Meter interface {
+	stats.Recorder
+	// Find returns a registered view associated with this name.
+	// If no registered view is found, nil is returned.
+	Find(name string) *View
+	// Register begins collecting data for the given views.
+	// Once a view is registered, it reports data to the registered exporters.
+	Register(views ...*View) error
+	// Unregister the given views. Data will not longer be exported for these views
+	// after Unregister returns.
+	// It is not necessary to unregister from views you expect to collect for the
+	// duration of your program execution.
+	Unregister(views ...*View)
+	// SetReportingPeriod sets the interval between reporting aggregated views in
+	// the program. If duration is less than or equal to zero, it enables the
+	// default behavior.
+	//
+	// Note: each exporter makes different promises about what the lowest supported
+	// duration is. For example, the Stackdriver exporter recommends a value no
+	// lower than 1 minute. Consult each exporter per your needs.
+	SetReportingPeriod(time.Duration)
+
+	// RegisterExporter registers an exporter.
+	// Collected data will be reported via all the
+	// registered exporters. Once you no longer
+	// want data to be exported, invoke UnregisterExporter
+	// with the previously registered exporter.
+	//
+	// Binaries can register exporters, libraries shouldn't register exporters.
+	RegisterExporter(Exporter)
+	// UnregisterExporter unregisters an exporter.
+	UnregisterExporter(Exporter)
+	// SetResource may be used to set the Resource associated with this registry.
+	// This is intended to be used in cases where a single process exports metrics
+	// for multiple Resources, typically in a multi-tenant situation.
+	SetResource(*resource.Resource)
+
+	// Start causes the Meter to start processing Record calls and aggregating
+	// statistics as well as exporting data.
+	Start()
+	// Stop causes the Meter to stop processing calls and terminate data export.
+	Stop()
+
+	// RetrieveData gets a snapshot of the data collected for the the view registered
+	// with the given name. It is intended for testing only.
+	RetrieveData(viewName string) ([]*Row, error)
 }
 
+var _ Meter = (*worker)(nil)
+
 var defaultWorker *worker
 
 var defaultReportingDuration = 10 * time.Second
@@ -56,11 +119,17 @@ var defaultReportingDuration = 10 * time.Second
 // Find returns a registered view associated with this name.
 // If no registered view is found, nil is returned.
 func Find(name string) (v *View) {
+	return defaultWorker.Find(name)
+}
+
+// Find returns a registered view associated with this name.
+// If no registered view is found, nil is returned.
+func (w *worker) Find(name string) (v *View) {
 	req := &getViewByNameReq{
 		name: name,
 		c:    make(chan *getViewByNameResp),
 	}
-	defaultWorker.c <- req
+	w.c <- req
 	resp := <-req.c
 	return resp.v
 }
@@ -68,11 +137,17 @@ func Find(name string) (v *View) {
 // Register begins collecting data for the given views.
 // Once a view is registered, it reports data to the registered exporters.
 func Register(views ...*View) error {
+	return defaultWorker.Register(views...)
+}
+
+// Register begins collecting data for the given views.
+// Once a view is registered, it reports data to the registered exporters.
+func (w *worker) Register(views ...*View) error {
 	req := &registerViewReq{
 		views: views,
 		err:   make(chan error),
 	}
-	defaultWorker.c <- req
+	w.c <- req
 	return <-req.err
 }
 
@@ -81,6 +156,14 @@ func Register(views ...*View) error {
 // It is not necessary to unregister from views you expect to collect for the
 // duration of your program execution.
 func Unregister(views ...*View) {
+	defaultWorker.Unregister(views...)
+}
+
+// Unregister the given views. Data will not longer be exported for these views
+// after Unregister returns.
+// It is not necessary to unregister from views you expect to collect for the
+// duration of your program execution.
+func (w *worker) Unregister(views ...*View) {
 	names := make([]string, len(views))
 	for i := range views {
 		names[i] = views[i].Name
@@ -89,31 +172,42 @@ func Unregister(views ...*View) {
 		views: names,
 		done:  make(chan struct{}),
 	}
-	defaultWorker.c <- req
+	w.c <- req
 	<-req.done
 }
 
 // RetrieveData gets a snapshot of the data collected for the the view registered
 // with the given name. It is intended for testing only.
 func RetrieveData(viewName string) ([]*Row, error) {
+	return defaultWorker.RetrieveData(viewName)
+}
+
+// RetrieveData gets a snapshot of the data collected for the the view registered
+// with the given name. It is intended for testing only.
+func (w *worker) RetrieveData(viewName string) ([]*Row, error) {
 	req := &retrieveDataReq{
 		now: time.Now(),
 		v:   viewName,
 		c:   make(chan *retrieveDataResp),
 	}
-	defaultWorker.c <- req
+	w.c <- req
 	resp := <-req.c
 	return resp.rows, resp.err
 }
 
 func record(tags *tag.Map, ms interface{}, attachments map[string]interface{}) {
+	defaultWorker.Record(tags, ms, attachments)
+}
+
+// Record records a set of measurements ms associated with the given tags and attachments.
+func (w *worker) Record(tags *tag.Map, ms interface{}, attachments map[string]interface{}) {
 	req := &recordReq{
 		tm:          tags,
 		ms:          ms.([]stats.Measurement),
 		attachments: attachments,
 		t:           time.Now(),
 	}
-	defaultWorker.c <- req
+	w.c <- req
 }
 
 // SetReportingPeriod sets the interval between reporting aggregated views in
@@ -124,17 +218,31 @@ func record(tags *tag.Map, ms interface{}, attachments map[string]interface{}) {
 // duration is. For example, the Stackdriver exporter recommends a value no
 // lower than 1 minute. Consult each exporter per your needs.
 func SetReportingPeriod(d time.Duration) {
+	defaultWorker.SetReportingPeriod(d)
+}
+
+// SetReportingPeriod sets the interval between reporting aggregated views in
+// the program. If duration is less than or equal to zero, it enables the
+// default behavior.
+//
+// Note: each exporter makes different promises about what the lowest supported
+// duration is. For example, the Stackdriver exporter recommends a value no
+// lower than 1 minute. Consult each exporter per your needs.
+func (w *worker) SetReportingPeriod(d time.Duration) {
 	// TODO(acetechnologist): ensure that the duration d is more than a certain
 	// value. e.g. 1s
 	req := &setReportingPeriodReq{
 		d: d,
 		c: make(chan bool),
 	}
-	defaultWorker.c <- req
+	w.c <- req
 	<-req.c // don't return until the timer is set to the new duration.
 }
 
-func newWorker() *worker {
+// NewMeter constructs a Meter instance. You should only need to use this if
+// you need to separate out Measurement recordings and View aggregations within
+// a single process.
+func NewMeter() Meter {
 	return &worker{
 		measures:   make(map[string]*measureRef),
 		views:      make(map[string]*viewInternal),
@@ -143,9 +251,23 @@ func newWorker() *worker {
 		c:          make(chan command, 1024),
 		quit:       make(chan bool),
 		done:       make(chan bool),
+
+		exporters: make(map[Exporter]struct{}),
 	}
 }
 
+// SetResource associates all data collected by this Meter with the specified
+// resource. This resource is reported when using metricexport.ReadAndExport;
+// it is not provided when used with ExportView/RegisterExporter, because that
+// interface does not provide a means for reporting the Resource.
+func (w *worker) SetResource(r *resource.Resource) {
+	w.r = r
+}
+
+func (w *worker) Start() {
+	go w.start()
+}
+
 func (w *worker) start() {
 	prodMgr := metricproducer.GlobalManager()
 	prodMgr.AddProducer(w)
@@ -155,7 +277,7 @@ func (w *worker) start() {
 		case cmd := <-w.c:
 			cmd.handleCommand(w)
 		case <-w.timer.C:
-			w.reportUsage(time.Now())
+			w.reportUsage()
 		case <-w.quit:
 			w.timer.Stop()
 			close(w.c)
@@ -165,7 +287,7 @@ func (w *worker) start() {
 	}
 }
 
-func (w *worker) stop() {
+func (w *worker) Stop() {
 	prodMgr := metricproducer.GlobalManager()
 	prodMgr.DeleteProducer(w)
 
@@ -202,44 +324,45 @@ func (w *worker) tryRegisterView(v *View) (*viewInternal, error) {
 		return x, nil
 	}
 	w.views[vi.view.Name] = vi
+	w.startTimes[vi] = time.Now()
 	ref := w.getMeasureRef(vi.view.Measure.Name())
 	ref.views[vi] = struct{}{}
 	return vi, nil
 }
 
-func (w *worker) unregisterView(viewName string) {
+func (w *worker) unregisterView(v *viewInternal) {
 	w.mu.Lock()
 	defer w.mu.Unlock()
-	delete(w.views, viewName)
+	delete(w.views, v.view.Name)
+	delete(w.startTimes, v)
+	if measure := w.measures[v.view.Measure.Name()]; measure != nil {
+		delete(measure.views, v)
+	}
 }
 
-func (w *worker) reportView(v *viewInternal, now time.Time) {
+func (w *worker) reportView(v *viewInternal) {
 	if !v.isSubscribed() {
 		return
 	}
 	rows := v.collectedRows()
-	_, ok := w.startTimes[v]
-	if !ok {
-		w.startTimes[v] = now
-	}
 	viewData := &Data{
 		View:  v.view,
 		Start: w.startTimes[v],
 		End:   time.Now(),
 		Rows:  rows,
 	}
-	exportersMu.Lock()
-	for e := range exporters {
+	w.exportersMu.Lock()
+	defer w.exportersMu.Unlock()
+	for e := range w.exporters {
 		e.ExportView(viewData)
 	}
-	exportersMu.Unlock()
 }
 
-func (w *worker) reportUsage(now time.Time) {
+func (w *worker) reportUsage() {
 	w.mu.Lock()
 	defer w.mu.Unlock()
 	for _, v := range w.views {
-		w.reportView(v, now)
+		w.reportView(v)
 	}
 }
 
@@ -248,11 +371,6 @@ func (w *worker) toMetric(v *viewInternal, now time.Time) *metricdata.Metric {
 		return nil
 	}
 
-	_, ok := w.startTimes[v]
-	if !ok {
-		w.startTimes[v] = now
-	}
-
 	var startTime time.Time
 	if v.metricDescriptor.Type == metricdata.TypeGaugeInt64 ||
 		v.metricDescriptor.Type == metricdata.TypeGaugeFloat64 {
@@ -261,7 +379,7 @@ func (w *worker) toMetric(v *viewInternal, now time.Time) *metricdata.Metric {
 		startTime = w.startTimes[v]
 	}
 
-	return viewToMetric(v, now, startTime)
+	return viewToMetric(v, w.r, now, startTime)
 }
 
 // Read reads all view data and returns them as metrics.
@@ -279,3 +397,17 @@ func (w *worker) Read() []*metricdata.Metric {
 	}
 	return metrics
 }
+
+func (w *worker) RegisterExporter(e Exporter) {
+	w.exportersMu.Lock()
+	defer w.exportersMu.Unlock()
+
+	w.exporters[e] = struct{}{}
+}
+
+func (w *worker) UnregisterExporter(e Exporter) {
+	w.exportersMu.Lock()
+	defer w.exportersMu.Unlock()
+
+	delete(w.exporters, e)
+}
diff --git a/vendor/go.opencensus.io/stats/view/worker_commands.go b/vendor/go.opencensus.io/stats/view/worker_commands.go
index 0267e179a..9ac4cc059 100644
--- a/vendor/go.opencensus.io/stats/view/worker_commands.go
+++ b/vendor/go.opencensus.io/stats/view/worker_commands.go
@@ -95,7 +95,7 @@ func (cmd *unregisterFromViewReq) handleCommand(w *worker) {
 		}
 
 		// Report pending data for this view before removing it.
-		w.reportView(vi, time.Now())
+		w.reportView(vi)
 
 		vi.unsubscribe()
 		if !vi.isSubscribed() {
@@ -103,7 +103,7 @@ func (cmd *unregisterFromViewReq) handleCommand(w *worker) {
 			// The collected data can be cleared.
 			vi.clearRows()
 		}
-		w.unregisterView(name)
+		w.unregisterView(vi)
 	}
 	cmd.done <- struct{}{}
 }
@@ -163,7 +163,7 @@ func (cmd *recordReq) handleCommand(w *worker) {
 		}
 		ref := w.getMeasureRef(m.Measure().Name())
 		for v := range ref.views {
-			v.addSample(cmd.tm, m.Value(), cmd.attachments, time.Now())
+			v.addSample(cmd.tm, m.Value(), cmd.attachments, cmd.t)
 		}
 	}
 }
diff --git a/vendor/go.opencensus.io/trace/lrumap.go b/vendor/go.opencensus.io/trace/lrumap.go
index dc7a295c7..908c2497e 100644
--- a/vendor/go.opencensus.io/trace/lrumap.go
+++ b/vendor/go.opencensus.io/trace/lrumap.go
@@ -44,7 +44,7 @@ func (lm lruMap) len() int {
 }
 
 func (lm lruMap) keys() []interface{} {
-	keys := []interface{}{}
+	keys := make([]interface{}, len(lm.cacheKeys))
 	for k := range lm.cacheKeys {
 		keys = append(keys, k)
 	}
diff --git a/vendor/go.opencensus.io/trace/trace.go b/vendor/go.opencensus.io/trace/trace.go
index 3f8977b41..125e2cd90 100644
--- a/vendor/go.opencensus.io/trace/trace.go
+++ b/vendor/go.opencensus.io/trace/trace.go
@@ -345,7 +345,7 @@ func (s *Span) SetStatus(status Status) {
 }
 
 func (s *Span) interfaceArrayToLinksArray() []Link {
-	linksArr := make([]Link, 0)
+	linksArr := make([]Link, 0, len(s.links.queue))
 	for _, value := range s.links.queue {
 		linksArr = append(linksArr, value.(Link))
 	}
@@ -353,7 +353,7 @@ func (s *Span) interfaceArrayToLinksArray() []Link {
 }
 
 func (s *Span) interfaceArrayToMessageEventArray() []MessageEvent {
-	messageEventArr := make([]MessageEvent, 0)
+	messageEventArr := make([]MessageEvent, 0, len(s.messageEvents.queue))
 	for _, value := range s.messageEvents.queue {
 		messageEventArr = append(messageEventArr, value.(MessageEvent))
 	}
@@ -361,7 +361,7 @@ func (s *Span) interfaceArrayToMessageEventArray() []MessageEvent {
 }
 
 func (s *Span) interfaceArrayToAnnotationArray() []Annotation {
-	annotationArr := make([]Annotation, 0)
+	annotationArr := make([]Annotation, 0, len(s.annotations.queue))
 	for _, value := range s.annotations.queue {
 		annotationArr = append(annotationArr, value.(Annotation))
 	}
@@ -369,7 +369,7 @@ func (s *Span) interfaceArrayToAnnotationArray() []Annotation {
 }
 
 func (s *Span) lruAttributesToAttributeMap() map[string]interface{} {
-	attributes := make(map[string]interface{})
+	attributes := make(map[string]interface{}, s.lruAttributes.len())
 	for _, key := range s.lruAttributes.keys() {
 		value, ok := s.lruAttributes.get(key)
 		if ok {
@@ -420,7 +420,7 @@ func (s *Span) lazyPrintfInternal(attributes []Attribute, format string, a ...in
 	var m map[string]interface{}
 	s.mu.Lock()
 	if len(attributes) != 0 {
-		m = make(map[string]interface{})
+		m = make(map[string]interface{}, len(attributes))
 		copyAttributes(m, attributes)
 	}
 	s.annotations.add(Annotation{
@@ -436,7 +436,7 @@ func (s *Span) printStringInternal(attributes []Attribute, str string) {
 	var a map[string]interface{}
 	s.mu.Lock()
 	if len(attributes) != 0 {
-		a = make(map[string]interface{})
+		a = make(map[string]interface{}, len(attributes))
 		copyAttributes(a, attributes)
 	}
 	s.annotations.add(Annotation{
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 5f4764038..b29adf6da 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -255,7 +255,7 @@ github.com/spf13/pflag
 # github.com/tsenart/vegeta v12.7.1-0.20190725001342-b5f4fca92137+incompatible
 ## explicit
 github.com/tsenart/vegeta/lib
-# go.opencensus.io v0.22.3
+# go.opencensus.io v0.22.4
 ## explicit
 go.opencensus.io
 go.opencensus.io/internal


Test patch:
diff --git a/metrics/config_test.go b/metrics/config_test.go
index dede19e4a..8f9857918 100644
--- a/metrics/config_test.go
+++ b/metrics/config_test.go
@@ -1045,11 +1045,11 @@ func TestStackdriverRecord(t *testing.T) {
 				{Measure: servedCount, Aggregation: view.Count()},
 				{Measure: statCount, Aggregation: view.Count()},
 			}
-			err = view.Register(v...)
+			err = RegisterResourceView(v...)
 			if err != nil {
 				t.Errorf("Failed to register %+v in stats backend: %v", v, err)
 			}
-			defer view.Unregister(v...)
+			defer UnregisterResourceView(v...)
 
 			// Try recording each metric and checking the result.
 			Record(ctx, servedCount.M(1))
diff --git a/metrics/metricstest/metricstest.go b/metrics/metricstest/metricstest.go
index 7cf8df8cc..9d74c4019 100644
--- a/metrics/metricstest/metricstest.go
+++ b/metrics/metricstest/metricstest.go
@@ -17,8 +17,10 @@ limitations under the License.
 package metricstest
 
 import (
+	"fmt"
 	"reflect"
 
+	"go.opencensus.io/metric/metricproducer"
 	"go.opencensus.io/stats/view"
 	"knative.dev/pkg/test"
 )
@@ -56,14 +58,17 @@ func CheckStatsNotReported(t test.T, names ...string) {
 // reported are tagged with the tags in wantTags and that wantValue matches reported count.
 func CheckCountData(t test.T, name string, wantTags map[string]string, wantValue int64) {
 	t.Helper()
-	if row := checkExactlyOneRow(t, name); row != nil {
-		checkRowTags(t, row, name, wantTags)
+	row, err := checkExactlyOneRow(t, name)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	checkRowTags(t, row, name, wantTags)
 
-		if s, ok := row.Data.(*view.CountData); !ok {
-			t.Error("want CountData", "metric", name, "got", reflect.TypeOf(row.Data))
-		} else if s.Value != wantValue {
-			t.Error("Wrong value", "metric", name, "value", s.Value, "want", wantValue)
-		}
+	if s, ok := row.Data.(*view.CountData); !ok {
+		t.Error("want CountData", "metric", name, "got", reflect.TypeOf(row.Data))
+	} else if s.Value != wantValue {
+		t.Error("Wrong value", "metric", name, "value", s.Value, "want", wantValue)
 	}
 }
 
@@ -72,21 +77,24 @@ func CheckCountData(t test.T, name string, wantTags map[string]string, wantValue
 // It also checks that expectedMin and expectedMax match the minimum and maximum reported values, respectively.
 func CheckDistributionData(t test.T, name string, wantTags map[string]string, expectedCount int64, expectedMin float64, expectedMax float64) {
 	t.Helper()
-	if row := checkExactlyOneRow(t, name); row != nil {
-		checkRowTags(t, row, name, wantTags)
+	row, err := checkExactlyOneRow(t, name)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	checkRowTags(t, row, name, wantTags)
 
-		if s, ok := row.Data.(*view.DistributionData); !ok {
-			t.Error("want DistributionData", "metric", name, "got", reflect.TypeOf(row.Data))
-		} else {
-			if s.Count != expectedCount {
-				t.Error("reporter count wrong", "metric", name, "got", s.Count, "want", expectedCount)
-			}
-			if s.Min != expectedMin {
-				t.Error("reporter count wrong", "metric", name, "got", s.Min, "want", expectedMin)
-			}
-			if s.Max != expectedMax {
-				t.Error("reporter count wrong", "metric", name, "got", s.Max, "want", expectedMax)
-			}
+	if s, ok := row.Data.(*view.DistributionData); !ok {
+		t.Error("want DistributionData", "metric", name, "got", reflect.TypeOf(row.Data))
+	} else {
+		if s.Count != expectedCount {
+			t.Error("reporter count wrong", "metric", name, "got", s.Count, "want", expectedCount)
+		}
+		if s.Min != expectedMin {
+			t.Error("reporter count wrong", "metric", name, "got", s.Min, "want", expectedMin)
+		}
+		if s.Max != expectedMax {
+			t.Error("reporter count wrong", "metric", name, "got", s.Max, "want", expectedMax)
 		}
 	}
 }
@@ -95,15 +103,19 @@ func CheckDistributionData(t test.T, name string, wantTags map[string]string, ex
 // are tagged with the tags in wantTags and that expectedCount number of records were reported.
 func CheckDistributionCount(t test.T, name string, wantTags map[string]string, expectedCount int64) {
 	t.Helper()
-	if row := checkExactlyOneRow(t, name); row != nil {
-		checkRowTags(t, row, name, wantTags)
+	row, err := checkExactlyOneRow(t, name)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	checkRowTags(t, row, name, wantTags)
 
-		if s, ok := row.Data.(*view.DistributionData); !ok {
-			t.Error("want DistributionData", "metric", name, "got", reflect.TypeOf(row.Data))
-		} else if s.Count != expectedCount {
-			t.Error("reporter count wrong", "metric", name, "got", s.Count, "want", expectedCount)
-		}
+	if s, ok := row.Data.(*view.DistributionData); !ok {
+		t.Error("want DistributionData", "metric", name, "got", reflect.TypeOf(row.Data))
+	} else if s.Count != expectedCount {
+		t.Error("reporter count wrong", "metric", name, "got", s.Count, "want", expectedCount)
 	}
+
 }
 
 // CheckLastValueData checks the view with a name matching string name to verify that the LastValueData stats
@@ -125,14 +137,17 @@ func CheckLastValueData(t test.T, name string, wantTags map[string]string, wantV
 // reported are tagged with the tags in wantTags and that wantValue matches the reported sum.
 func CheckSumData(t test.T, name string, wantTags map[string]string, wantValue float64) {
 	t.Helper()
-	if row := checkExactlyOneRow(t, name); row != nil {
-		checkRowTags(t, row, name, wantTags)
+	row, err := checkExactlyOneRow(t, name)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	checkRowTags(t, row, name, wantTags)
 
-		if s, ok := row.Data.(*view.SumData); !ok {
-			t.Error("Wrong type", "metric", name, "got", reflect.TypeOf(row.Data), "want", "SumData")
-		} else if s.Value != wantValue {
-			t.Error("Wrong sumdata", "metric", name, "got", s.Value, "want", wantValue)
-		}
+	if s, ok := row.Data.(*view.SumData); !ok {
+		t.Error("Wrong type", "metric", name, "got", reflect.TypeOf(row.Data), "want", "SumData")
+	} else if s.Value != wantValue {
+		t.Error("Wrong sumdata", "metric", name, "got", s.Value, "want", wantValue)
 	}
 }
 
@@ -166,19 +181,29 @@ func lastRow(t test.T, name string) *view.Row {
 	return d[len(d)-1]
 }
 
-func checkExactlyOneRow(t test.T, name string) *view.Row {
-	t.Helper()
-	d, err := view.RetrieveData(name)
-	if err != nil {
-		t.Error("Reporter.Report() error", "metric", name, "error", err)
-		return nil
+func checkExactlyOneRow(t test.T, name string) (*view.Row, error) {
+	// view.Meter implements (and is exposed by) metricproducer.GetAll. Since
+	// this is a test, reach around and cast these to view.Meter.
+	var retval *view.Row
+	for _, producer := range metricproducer.GlobalManager().GetAll() {
+		meter := producer.(view.Meter)
+
+		d, err := meter.RetrieveData(name)
+		if err != nil || len(d) == 0 {
+			continue
+		}
+		if len(d) > 1 {
+			return nil, fmt.Errorf("expected 1 row for metric %q got %d", name, len(d))
+		}
+		if retval != nil {
+			return nil, fmt.Errorf("got 2 rows from different meters: %+v, %+v", *retval, d[0])
+		}
+		retval = d[0]
 	}
-	if len(d) != 1 {
-		t.Error("Reporter.Report() wrong length", "metric", name, "got", len(d), "want", 1)
-		return nil
+	if retval == nil {
+		return nil, fmt.Errorf("could not find row for %q", name)
 	}
-
-	return d[0]
+	return retval, nil
 }
 
 func checkRowTags(t test.T, row *view.Row, name string, wantTags map[string]string) {
diff --git a/reconciler/reconcile_common_test.go b/reconciler/reconcile_common_test.go
index b1956f94e..bda8540ac 100644
--- a/reconciler/reconcile_common_test.go
+++ b/reconciler/reconcile_common_test.go
@@ -19,6 +19,7 @@ package reconciler
 import (
 	"context"
 	"testing"
+	"time"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -99,13 +100,43 @@ func TestPostProcessReconcileBumpsGeneration(t *testing.T) {
 	resource := makeResource()
 
 	krShape := duckv1.KRShaped(resource)
-	PostProcessReconcile(context.Background(), krShape)
+	PostProcessReconcile(context.Background(), krShape, krShape)
 
 	if resource.Status.ObservedGeneration != resource.Generation {
-		t.Errorf("Expected observed generation bump got=%d want=%d", resource.Status.ObservedGeneration, resource.Generation)
+		t.Errorf("Expected observed generation bump got=%d want=%d",
+			resource.Status.ObservedGeneration, resource.Generation)
 	}
 
 	if krShape.GetStatus().ObservedGeneration != krShape.GetGeneration() {
-		t.Errorf("Expected observed generation bump got=%d want=%d", resource.Status.ObservedGeneration, resource.Generation)
+		t.Errorf("Expected observed generation bump got=%d want=%d",
+			resource.Status.ObservedGeneration, resource.Generation)
+	}
+}
+
+func TestPostProcessReconcileUpdatesTransitionTimes(t *testing.T) {
+	oldNow := apis.VolatileTime{Inner: metav1.NewTime(time.Now())}
+	resource := makeResource()
+	oldResource := makeResource()
+	// initialize old conditions with oldNow
+	oldResource.Status.Conditions[0].LastTransitionTime = oldNow
+	oldResource.Status.Conditions[1].LastTransitionTime = oldNow
+	// change the second condition, but keep the old timestamp.
+	resource.Status.Conditions[1].LastTransitionTime = oldNow
+	resource.Status.Conditions[1].Status = corev1.ConditionFalse
+
+	new := duckv1.KRShaped(resource)
+	old := duckv1.KRShaped(oldResource)
+	PostProcessReconcile(context.Background(), new, old)
+
+	unchangedCond := resource.Status.Conditions[0]
+	if unchangedCond.LastTransitionTime != oldNow {
+		t.Errorf("Expected unchanged condition to keep old timestamp. Got=%v Want=%v",
+			unchangedCond.LastTransitionTime, oldNow)
+	}
+
+	changedCond := resource.Status.Conditions[1]
+	if changedCond.LastTransitionTime == oldNow {
+		t.Errorf("Expected changed condition to get a new timestamp. Got=%v Want=%v",
+			changedCond.LastTransitionTime, oldNow)
 	}
 }