Repository: etcd-io/etcd
File: server/etcdserver/api/membership/cluster.go (Go)

Code before:
		isLearner.Set(0)
	}

	if c.be != nil {
		m := c.members[id]
		if shouldApplyV3 {
			m.RaftAttributes.IsLearner = false
			c.updateMembershipMetric(id, true)
			c.be.MustSaveMemberToBackend(m)

			c.lg.Info(
				"promote member",
				zap.String("cluster-id", c.cid.String()),
				zap.String("local-member-id", c.localID.String()),
				zap.String("promoted-member-id", id.String()),
			)
		} else {
			// Workaround the issues which have already been affected by
			// https://github.com/etcd-io/etcd/issues/19557. The learner
			// promotion request had been applied to v3store, but not saved
			// to v2snapshot yet when in 3.5. Once upgrading to 3.6, the
			// patch here ensure the issue can be automatically fixed.
			if m.IsLearner {
				m.RaftAttributes.IsLearner = false
				c.lg.Info("Forcibly apply member promotion request", zap.String("member", fmt.Sprintf("%+v", *m)))
				c.be.MustSaveMemberToBackend(m)
			} else {
				c.lg.Info(
					"ignore already promoted member",
					zap.String("cluster-id", c.cid.String()),
					zap.String("local-member-id", c.localID.String()),
				)
			}
		}
	} else {
		c.lg.Info(
			"ignore already promoted member due to backend being nil",
			zap.String("cluster-id", c.cid.String()),
			zap.String("local-member-id", c.localID.String()),
		)
	}
}

// SyncLearnerPromotionIfNeeded provides a workaround solution to fix the issues
// which have already been affected by https://github.com/etcd-io/etcd/issues/19557.
func (c *RaftCluster) SyncLearnerPromotionIfNeeded() {
	c.Lock()
	defer c.Unlock()

	v2Members, _ := membersFromStore(c.lg, c.v2store)
	v3Members, _ := c.be.MustReadMembersFromBackend()

Review comment (suggestion):
MustSaveMemberToBackend will use `LockInsideApply` which will flush the CI. I don't remember what is state of CI when `shouldApplyV3`, but I assume it's pointing to the raftindex of the promote request. 

I think this is the place to call `MustHackySaveMemberToBackend`

Code after:
		isLearner.Set(0)
	}

	if c.be != nil {
		m := c.members[id]
		if shouldApplyV3 {
			m.RaftAttributes.IsLearner = false
			c.updateMembershipMetric(id, true)
			c.be.MustSaveMemberToBackend(m)

			c.lg.Info(
				"promote member",
				zap.String("cluster-id", c.cid.String()),
				zap.String("local-member-id", c.localID.String()),
				zap.String("promoted-member-id", id.String()),
			)
		} else {
			// Workaround the issues which have already been affected by
			// https://github.com/etcd-io/etcd/issues/19557. The learner
			// promotion request had been applied to v3store, but not saved
			// to v2snapshot yet when in 3.5. Once upgrading to 3.6, the
			// patch here ensure the issue can be automatically fixed.
			if m.IsLearner {
				m.RaftAttributes.IsLearner = false
				c.lg.Info("Forcibly apply member promotion request", zap.String("member", fmt.Sprintf("%+v", *m)))
				c.be.MustHackySaveMemberToBackend(m)
			} else {
				c.lg.Info(
					"ignore already promoted member",
					zap.String("cluster-id", c.cid.String()),
					zap.String("local-member-id", c.localID.String()),
				)
			}
		}
	} else {
		c.lg.Info(
			"ignore already promoted member due to backend being nil",
			zap.String("cluster-id", c.cid.String()),
			zap.String("local-member-id", c.localID.String()),
		)
	}
}

// SyncLearnerPromotionIfNeeded provides a workaround solution to fix the issues
// which have already been affected by https://github.com/etcd-io/etcd/issues/19557.
func (c *RaftCluster) SyncLearnerPromotionIfNeeded() {
	c.Lock()
	defer c.Unlock()

	v2Members, _ := membersFromStore(c.lg, c.v2store)
	v3Members, _ := c.be.MustReadMembersFromBackend()